/*
 * Redistribution and use of this software and associated documentation ("Software"), with or
 * without modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright statements and notices. Redistributions
 * must also contain a copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
 * conditions and the following disclaimer in the documentation and/or other materials provided with
 * the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote products derived from this Software
 * without prior written permission of Intalio, Inc. For written permission, please contact
 * info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab" nor may "Exolab" appear in
 * their names without prior written permission of Intalio, Inc. Exolab is a registered trademark of
 * Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTALIO, INC. OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 1999-2003 (C) Intalio, Inc. All Rights Reserved.
 *
 * This file was originally developed by Keith Visco during the course of employment at Intalio Inc.
 * All portions of this file developed by Keith Visco after Jan 19 2005 are Copyright (C) 2005 Keith
 * Visco. All Rights Reserved.
 *
 * $Id: $
 */
package org.exolab.castor.builder;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Properties;

import org.castor.core.constants.cpa.JDOConstants;
import org.exolab.castor.builder.conflictresolution.ClassNameCRStrategy;
import org.exolab.castor.builder.conflictresolution.ClassNameCRStrategyRegistry;
import org.exolab.castor.builder.descriptors.DescriptorSourceFactory;
import org.exolab.castor.builder.descriptors.JDOClassDescriptorFactory;
import org.exolab.castor.builder.factory.MappingFileSourceFactory;
import org.exolab.castor.builder.info.ClassInfo;
import org.exolab.castor.builder.info.nature.JDOClassInfoNature;
import org.exolab.castor.builder.info.nature.XMLInfoNature;
import org.exolab.castor.builder.printing.JClassPrinter;
import org.exolab.castor.builder.printing.JClassPrinterFactoryRegistry;
import org.exolab.castor.mapping.xml.MappingRoot;
import org.exolab.castor.util.dialog.ConsoleDialog;
import org.exolab.javasource.JClass;
import org.exolab.javasource.JComment;
import org.exolab.javasource.JNaming;

/**
 * Writes a single class (and any associated inner classes) to a file.
 * 
 * @author <a href="mailto:kvisco@intalio.com">Keith Visco</a> - Main author.
 * @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a> - Contributions.
 * @author <a href="mailto:nsgreen@thazar.com">Nathan Green</a> - Contributions.
 * @author <a href="mailto:edward.kuns@aspect.com">Edward Kuns</a> - Separated from SourceGenerator
 * @version $Revision: 0000 $ $Date: $
 */
public final class SingleClassGenerator {
  /**
   * The default code header. Please leave "$" and "Id" separated with "+" so that the CVS server
   * does not expand it here.
   */
  private static final String DEFAULT_HEADER = "This class was automatically generated with \n"
      + "<a href=\"" + SourceGenerator.APP_URI + "\">" + SourceGenerator.APP_NAME + " "
      + SourceGenerator.VERSION + "</a>, using an XML Schema.\n$" + "Id" + "$";

  /** Name of the CDR (Class Descriptor Resolver) file. */
  private static final String CDR_FILE = ".castor.cdr";
  /** True if the user should be prompted to overwrite when a file already exists. */
  private boolean _promptForOverwrite = true;
  /** Destination directory where all our output goes. */
  private String _destDir;
  /**
   * Destination directory for all resource files (e.g. .castor.cdr files).
   */
  private String _resourceDestinationDirectory;
  /** The line separator to use for output. */
  private String _lineSeparator = null;
  /** A flag indicating whether or not to create descriptors for the generated classes. */
  private boolean _createDescriptors = true;

  /**
   * A flag indicating whether or not to create JDO descriptors for the generated classes.
   */
  private boolean _createJdoDescriptors = false;

  /** The header at the top of each generated file. */
  private final JComment _header;
  /** Console dialog used to prompt the user when something is wrong. */
  private final ConsoleDialog _dialog;

  /**
   * The DescriptorSourceFactory instance.
   */
  private final DescriptorSourceFactory _descriptorSourceFactory;

  /**
   * The JDOClassDescriptorFactory instance.
   */
  private JDOClassDescriptorFactory _jdoDescriptorSourceFactory;

  /** The MappingFileSourceFactory instance. */
  private final MappingFileSourceFactory _mappingSourceFactory;
  /** The SourceGenerator instance that created us. */
  private final SourceGenerator _sourceGenerator;

  /**
   * The class name conflict error handling strategy to use for resolving class name conflicts.
   */
  private ClassNameCRStrategy _conflictStrategy;

  /**
   * The implementation of {@link JClassPrinter} to use for generating the Java classes and writing
   * them to the file system.
   */
  private JClassPrinter _jClassPrinter;

  /**
   * The registry for {@link ClassNameCRStrategy} implementations.
   */
  private ClassNameCRStrategyRegistry _classNameConflictResolutionStrategyRegistry;


  /**
   * Creates an instance of this class.
   * 
   * @param dialog A ConsoleDialog instance
   * @param sourceGenerator A SourceGenerator instance
   * @param conflictStrategyType Type of the {@link ClassNameCRStrategy} instance to be used.
   * @param jClassPrinterType The string representation of the printer to be used,
   */
  public SingleClassGenerator(final ConsoleDialog dialog, final SourceGenerator sourceGenerator,
      final String conflictStrategyType, final String jClassPrinterType) {
    this._dialog = dialog;
    this._sourceGenerator = sourceGenerator;
    this._header = new JComment(JComment.HEADER_STYLE);
    this._descriptorSourceFactory = new DescriptorSourceFactory(_sourceGenerator);
    this._jdoDescriptorSourceFactory = new JDOClassDescriptorFactory(_sourceGenerator);
    this._mappingSourceFactory = new MappingFileSourceFactory(_sourceGenerator);

    final String strategy =
        sourceGenerator.getProperty(BuilderConfiguration.Property.NAME_CONFLICT_STRATEGIES, "");
    this._classNameConflictResolutionStrategyRegistry = new ClassNameCRStrategyRegistry(strategy);
    createNameConflictStrategy(conflictStrategyType);
    createJClassPrinter(jClassPrinterType);
  }

  /**
   * Creates a JClassPrinter instance from the given string key.
   * 
   * @param classPrinterType The string identifier if the printer,
   */
  private void createJClassPrinter(final String classPrinterType) {
    JClassPrinterFactoryRegistry registry = _sourceGenerator.getJClassPrinterFactoryRegistry();
    this._jClassPrinter = registry.getJClassPrinterFactory(classPrinterType).getJClassPrinter();
  }

  /**
   * Sets the type of the {@link JClassPrinter} instance to be used for {@link JClass} writing.
   * 
   * @param jclassPrinterType The string identifier if the printer,
   */
  public void setJClassPrinterType(final String jclassPrinterType) {
    this.createJClassPrinter(jclassPrinterType);
  }

  /**
   * Sets the destination directory.
   *
   * @param destDir the destination directory.
   */
  public void setDestDir(final String destDir) {
    _destDir = destDir;
    if (_resourceDestinationDirectory == null) {
      _resourceDestinationDirectory = destDir;
    }
  }

  /**
   * Sets the destination directory for generated resources.
   *
   * @param destDir the destination directory.
   */
  public void setResourceDestinationDirectory(final String destinationDirectory) {
    _resourceDestinationDirectory = destinationDirectory;
  }

  /**
   * Sets the line separator to use when printing the source code.
   *
   * @param lineSeparator the line separator to use when printing the source code. This method is
   *        useful if you are generating source on one platform, but will be compiling the source on
   *        a different platform. <B>Note:</B>This can be any string, so be careful. I recommend
   *        either using the default or using one of the following:
   * 
   *        <PRE>
   * windows systems use: "\r\n"
   * unix systems use: "\n"
   * mac systems use: "\r"
   *        </PRE>
   */
  public void setLineSeparator(final String lineSeparator) {
    _lineSeparator = lineSeparator;
  } // -- setLineSeparator

  /**
   * Sets whether or not to create ClassDescriptors for the generated classes. By default,
   * descriptors are generated.
   *
   * @param createDescriptors a boolean, when true indicates to generated ClassDescriptors
   */
  public void setDescriptorCreation(final boolean createDescriptors) {
    _createDescriptors = createDescriptors;
  } // -- setDescriptorCreation


  /**
   * Sets whether or not to create JDOClassDescriptors for the generated classes. By default,
   * descriptors are generated.
   * 
   * @param createJdoDescriptors if true, JDOClassDescriptors are generated.
   */
  public void setJdoDescriptorCreation(final boolean createJdoDescriptors) {
    _createJdoDescriptors = createJdoDescriptors;
  }

  /**
   * Sets whether or not to prompt when we would otherwise overwrite an existing JClass. If set to
   * false, then it is always OK to overwrite an existing class. If set to true, the user will be
   * prompted.
   *
   * @param promptForOverwrite the new value
   */
  public void setPromptForOverwrite(final boolean promptForOverwrite) {
    this._promptForOverwrite = promptForOverwrite;
  } // -- setPromptForOverwrite

  /**
   * Processes the JClass mapped by the provided key unless the JClass has already been processed.
   *
   * @param state SourceGenerator state
   * @param classKeys Enumeration over a collection of keys to ClassInfos
   *
   * @return true if processing is allowed to continue, false if the SourceGenerator state is
   *         STOP_STATUS,
   * @throws IOException If an already existing '.castor.cdr' file can not be loaded or found
   */
  boolean processIfNotAlreadyProcessed(final Enumeration<?> classKeys, final SGStateInfo state)
      throws IOException {
    while (classKeys.hasMoreElements()) {
      ClassInfo classInfo = state.resolve(classKeys.nextElement());
      JClass jClass = classInfo.getJClass();
      if (!state.processed(jClass)
          && (inCurrentSchema(state, classInfo) || _sourceGenerator.getGenerateImportedSchemas())) {
        process(jClass, state);
        if (state.getStatusCode() == SGStateInfo.STOP_STATUS) {
          return false;
        }
      }
    }
    return true;
  }

  /**
   * Indicates whether {@link ClassInfo} instance is defined within target namespace.
   * 
   * @param state The Sourcegenerator state.
   * @param classInfo The {@link ClassInfo} instance to be analyzed.
   * @return True if it's within the targetNamespace
   */
  private boolean inCurrentSchema(final SGStateInfo state, final ClassInfo classInfo) {
    final String targetNamespace = state.getSchema().getTargetNamespace();
    boolean inCurrentSchema = true;
    if (targetNamespace != null) {
      if (classInfo.hasNature(XMLInfoNature.class.getName())) {
        XMLInfoNature xmlNature = new XMLInfoNature(classInfo);
        inCurrentSchema = targetNamespace.equals(xmlNature.getNamespaceURI());
      }
    }
    return inCurrentSchema;
  }

  /**
   * Processes the given JClasses, one by one, stopping if the SourceGenerator state indicates STOP
   * after processing one class.
   *
   * @param classes Array of classes to process
   * @param state SourceGenerator state
   * @return true if processing is allowed to continue, false if the SourceGenerator state is
   *         STOP_STATUS,
   * @throws IOException If an already existing '.castor.cdr' file can not be loaded or found
   */
  boolean process(final JClass[] classes, final SGStateInfo state) throws IOException {
    for (JClass jClass : classes) {
      process(jClass, state);
      if (state.getStatusCode() == SGStateInfo.STOP_STATUS) {
        return false;
      }
    }
    return true;
  }

  /**
   * Processes the given JClass by checking for class name conflicts, and if there are none, making
   * the class as processed and then printing the class and, if appropriate, its class descriptors.
   * <p>
   * If there is a class name conflict, at best the user stops the source generation and at worst
   * the user continues, skipping this class.
   *
   * @param jClass the class to process
   * @param state SourceGenerator state
   * @return true if processing is allowed to continue, false if the SourceGenerator state is
   *         STOP_STATUS,
   * @throws IOException If an already existing '.castor.cdr' file can not be loaded or found
   */
  boolean process(final JClass jClass, final SGStateInfo state) throws IOException {
    if (state.getStatusCode() == SGStateInfo.STOP_STATUS) {
      return false;
    }

    if (state.processed(jClass)) {
      return true;
    }

    // --Make sure this class's name doesn't conflict with a java.lang.* class
    checkNameNotReserved(jClass.getName(), state);

    ClassInfo classInfo = state.resolve(jClass);

    // -- Have we already processed a class with this name?
    JClass conflict = state.getProcessed(jClass.getName());
    if (conflict != null && !state.getSuppressNonFatalWarnings()) {
      SGStateInfo stateAfterResolution =
          _conflictStrategy.dealWithClassNameConflict(state, classInfo, conflict);
      return stateAfterResolution.getStatusCode() != SGStateInfo.STOP_STATUS;
    }

    // -- Mark the current class as processed
    state.markAsProcessed(jClass);

    // -- Print the class
    if (checkAllowPrinting(jClass)) {
      // hack for the moment
      // to avoid the compiler complaining with java.util.Date
      jClass.removeImport("org.exolab.castor.types.Date");
      jClass.setHeader(_header);
      if (_lineSeparator == null) {
        _lineSeparator = System.getProperty("line.separator");
      }
      _jClassPrinter.printClass(jClass, _destDir, _lineSeparator, DEFAULT_HEADER);
    }

    // -- Process and print the class descriptors
    if (classInfo != null) {
      processClassDescriptor(jClass, state, classInfo);
      if (classInfo.hasNature(JDOClassInfoNature.class.getName())) {
        processJDOClassDescriptor(jClass, state, classInfo);
      }
    }

    return state.getStatusCode() != SGStateInfo.STOP_STATUS;
  } // -- processJClass

  /**
   * Processes the Class Descriptor for the provided JClass.
   *
   * @param jClass the classInfo to process
   * @param state SourceGenerator state
   * @param classInfo the XML Schema element declaration
   * @throws IOException If an already existing '.castor.cdr' file can not be loaded or found
   */
  private void processClassDescriptor(final JClass jClass, final SGStateInfo state,
      final ClassInfo classInfo) throws IOException {
    if (_createDescriptors) {
      JClass desc = _descriptorSourceFactory.createSource(classInfo);
      if (checkAllowPrinting(desc)) {
        updateCDRFile(jClass, desc, state, CDR_FILE);
        desc.setHeader(_header);
        if (_lineSeparator == null) {
          _lineSeparator = System.getProperty("line.separator");
        }
        _jClassPrinter.printClass(desc, _destDir, _lineSeparator, DEFAULT_HEADER);
      }
    } else {
      // TODO cleanup mapping file integration (what does this TODO mean?)
      // create a class mapping
      String pkg = state.getPackageName();
      if (pkg == null) {
        pkg = "";
      }
      MappingRoot mapping = state.getMapping(pkg);
      if (mapping == null) {
        mapping = new MappingRoot();
        state.setMapping(pkg, mapping);
      }
      mapping.addClassMapping(_mappingSourceFactory.createMapping(classInfo));
    }
  }

  /**
   * Process/generate JDOClassDescriptors for the given {@link ClassInfo}.
   * 
   * @param jClass a structure to represent Java Source Files. See {@link JClass} for details.
   * @param state the state of the SourceGenerator.
   * @param classInfo the object holding all necessary information to generate the source code for
   *        the JDOClassDescriptor.
   * @throws IOException If an already existing '.castor.cdr' file can not be loaded or found
   */
  private void processJDOClassDescriptor(final JClass jClass, final SGStateInfo state,
      final ClassInfo classInfo) throws IOException {

    if (_createJdoDescriptors) {
      JClass desc = _jdoDescriptorSourceFactory.createSource(classInfo);
      if (checkAllowPrinting(desc)) {
        updateCDRFile(jClass, desc, state, JDOConstants.PKG_CDR_LIST_FILE);
        desc.setHeader(_header);
        if (_lineSeparator == null) {
          _lineSeparator = System.getProperty("line.separator");
        }
        _jClassPrinter.printClass(desc, _destDir, _lineSeparator, DEFAULT_HEADER);
      }
    }
  }

  /**
   * Checks to see if we will write the provided JClass to disk. If we have been configured to not
   * prompt for overwrite, then it is assumed and overwriting an existing file is always OK. If the
   * file does not exist, it is always OK to write it. Only if we are configured to prompt for
   * overwrite and the file already exists do we need to issue a dialog and get the user's
   * permission.
   *
   * @param jClass a JClass to check to see if we can write
   * @return true if we can write out the provided jClass
   */
  private boolean checkAllowPrinting(final JClass jClass) {
    if (!_promptForOverwrite) {
      return true;
    }

    // Check whether there exists already a file with the same name;
    // if not, it is OK to write (aka create) the (new) file
    String filename = jClass.getFilename(_destDir);
    File file = new File(filename);

    if (!file.exists()) {
      return true;
    }

    return _conflictStrategy.dealWithFileOverwrite(filename);
  }

  /**
   * Checks the given name against various naming conflicts. If a conflict is found, then this
   * method generates an appropriate error message and throws an IllegalArgumentException.
   * 
   * @param elementName element name to check against lists of reserved names
   * @param sInfo source generator state
   */
  private void checkNameNotReserved(final String elementName, final SGStateInfo sInfo) {
    if (elementName == null) {
      return;
    }

    String nameToCompare = elementName.substring(0, 1).toUpperCase() + elementName.substring(1);
    if (JNaming.isInJavaLang(nameToCompare)) {
      String err = "'" + nameToCompare
          + "' conflicts with a class in java.lang.* and may cause a conflict during\n"
          + " compilation. If you get this complaint during compilation, you need to\n"
          + " use a mapping file or change the name of the schema element.";
      sInfo.getDialog().notify(err);
    }

    if (JNaming.isReservedByCastor(nameToCompare)) {
      String warn = "'" + nameToCompare + "' might conflict with a field name used"
          + " by Castor.  If you get a complaint\nabout a duplicate name, you will"
          + " need to use a mapping file or change\nthe name of the conflicting"
          + " schema element.";
      sInfo.getDialog().notify(warn);
    }

    final String withoutPackage = nameToCompare.substring(nameToCompare.lastIndexOf('.') + 1);
    if (JNaming.isReservedByWindows(nameToCompare) || JNaming.isReservedByWindows(withoutPackage)) {
      // FIXME We should fail under Windows and warn under other OSes
      String warn = "'" + nameToCompare + "' is reserved by the Windows filesystem and"
          + " cannot be\nused as a class name.  Windows will not allow you to create"
          + " a file with this\nname.  You will have to use a binding file or change"
          + " the name of the conflicting\nschema element.  For more information,"
          + " see\nhttp://msdn.microsoft.com/library/default.asp?"
          + "url=/library/en-us/fileio/fs/naming_a_file.asp";
      sInfo.getDialog().notify(warn);
    }
  }

  /**
   * Updates the CDR (ClassDescriptorResolver) file with the classname->descriptor mapping.
   *
   * @param jClass JClass instance describing the entity class
   * @param jDesc JClass instance describing is *Descriptor class
   * @param sInfo state info
   * @param cdrFileName the filename of the class descriptor resolver (cdr) file
   * @throws IOException If an already existing '.castor.cdr' file can not be found or loaded
   */
  private void updateCDRFile(final JClass jClass, final JClass jDesc, final SGStateInfo sInfo,
      final String cdrFileName) throws IOException {
    String entityFilename = jClass.getFilename(_resourceDestinationDirectory);
    File file = new File(entityFilename);
    File parentDirectory = file.getParentFile();
    File cdrFile = new File(parentDirectory, cdrFileName);
    String cdrFilename = cdrFile.getAbsolutePath();

    Properties props = sInfo.getCDRFile(cdrFilename);

    if (props == null) {
      // check for existing .castor.xml file
      props = new Properties();
      if (cdrFile.exists()) {
        try (FileInputStream fileStream = new FileInputStream(cdrFile)) {
          props.load(fileStream);
        }
      }
      sInfo.setCDRFile(cdrFilename, props);
    }
    props.setProperty(jClass.getName(), jDesc.getName());
  } // -- updateCDRFile

  /**
   * Sets the desired {@link ClassNameCRStrategy} instance type to be used for name conflict
   * resolution.
   * 
   * @param nameConflictStrategy the desired {@link ClassNameCRStrategy} instance type
   */
  public void setNameConflictStrategy(final String nameConflictStrategy) {
    createNameConflictStrategy(nameConflictStrategy);
  }

  /**
   * Creates a new {@link ClassNameCRStrategy} instance by calling the
   * {@link ClassNameConflictResolutionStrategyFactory}.
   * 
   * @param nameConflictStrategy The desired {@link ClassNameCRStrategy} type.
   */
  private void createNameConflictStrategy(final String nameConflictStrategy) {
    this._conflictStrategy = _classNameConflictResolutionStrategyRegistry
        .getClassNameConflictResolutionStrategy(nameConflictStrategy);
    this._conflictStrategy.setConsoleDialog(_dialog);
    this._conflictStrategy.setSingleClassGenerator(this);
  }

  /**
   * Returns the {@link SourceGenerator} instance that created this class.
   * 
   * @return the {@link SourceGenerator} instance that created this class.
   */
  public SourceGenerator getSourceGenerator() {
    return _sourceGenerator;
  }

}
