/*
 * Copyright 2007 Werner Guttmann
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.exolab.castor.builder;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.castor.xml.JavaNaming;
import org.exolab.castor.builder.binding.ExtendedBinding;
import org.exolab.castor.builder.binding.XMLBindingComponent;
import org.exolab.castor.builder.binding.XPathHelper;
import org.exolab.castor.builder.binding.xml.Exclude;
import org.exolab.castor.builder.conflict.strategy.ClassNameConflictResolver;
import org.exolab.castor.builder.conflict.strategy.XPATHClassNameConflictResolver;
import org.exolab.castor.xml.schema.Annotated;
import org.exolab.castor.xml.schema.ElementDecl;
import org.exolab.castor.xml.schema.Group;
import org.exolab.castor.xml.schema.ModelGroup;
import org.exolab.castor.xml.schema.Order;
import org.exolab.castor.xml.schema.XMLType;
import org.exolab.javasource.JClass;

/**
 * A registry for maintaing information about {@link JClass} instances already processed.
 * 
 * @author <a href="mailto:werner DOT guttmann AT gmx DOT net">Werner Guttmann</a>
 * @since 1.1
 */
public class JClassRegistry {

  /**
   * Logger instance used for all logging functionality.
   */
  private static final Log LOG = LogFactory.getLog(JClassRegistry.class);

  /**
   * Registry for holding a set of global element definitions.
   */
  private Set<String> _globalElements = new HashSet<String>();

  /**
   * Registry for mapping an XPATH location to a {@link JClass} instance generated for the XML
   * artefact uniquely identified by the XPATH.
   */
  private Map<String, JClass> _xpathToJClass = new HashMap<String, JClass>();

  /**
   * Registry for recording naming collisions, keyed by the local part of an otherwise rooted XPATH.
   */
  private Map<String, List<String>> _localNames = new HashMap<String, List<String>>();

  /**
   * Registry for recording naming collisions, keyed by the typed local part of an otherwise rooted
   * XPATH.
   */
  private Map<String, String> _typedLocalNames = new HashMap<String, String>();

  /**
   * JavaNaming to be used.
   * 
   * @since 1.1.3
   */
  private JavaNaming _javaNaming;


  /**
   * Class name conflict resolver.
   */
  private ClassNameConflictResolver _classNameConflictResolver =
      new XPATHClassNameConflictResolver();

  /**
   * Registers the XPATH identifier for a global element definition for further use.
   * 
   * @param xpath The XPATH identifier of a global element.
   */
  public void prebindGlobalElement(final String xpath) {
    _globalElements.add(xpath);
  }

  /**
   * Creates an instance of this class, providing the class anme conflict resolver to be used during
   * automatic class name conflict resolution (for local element conflicts).
   * 
   * @param resolver {@link ClassNameConflictResolver} instance to be used
   * @param javaNaming the {@link JavaNaming} to use (must not be null).
   */
  public JClassRegistry(final ClassNameConflictResolver resolver, final JavaNaming javaNaming) {
    _classNameConflictResolver = resolver;
    _javaNaming = javaNaming;
  }

  /**
   * Registers a {@link JClass} instance for a given XPATH.
   * 
   * @param jClass The {@link JClass} instance to register.
   * @param component Container for the {@link Annotated} instance referred to by the XPATH.
   * @param mode Whether we register JClass instances in 'field' or 'class'mode.
   */
  public void bind(final JClass jClass, final XMLBindingComponent component, final String mode) {

    Annotated annotated = component.getAnnotated();

    // String xPath = XPathHelper.getSchemaLocation(annotated);
    String xPath = XPathHelper.getSchemaLocation(annotated, true);
    String localXPath = getLocalXPath(xPath);
    String untypedXPath = xPath;

    // TODO: it could happen that we silently ignore a custom package as defined in a binding - FIX
    // !

    // get local name
    String localName = getLocalName(xPath);
    String typedLocalName = localName;

    if (annotated instanceof ElementDecl) {
      ElementDecl element = (ElementDecl) annotated;
      String typexPath = XPathHelper.getSchemaLocation(element.getType());
      xPath += "[" + typexPath + "]";
      typedLocalName += "[" + typexPath + "]";
    } else if (annotated instanceof Group) {
      Group group = (Group) annotated;
      if (group.getOrder() == Order.choice && !_globalElements.contains("/" + localXPath)) {
        xPath += "/#choice";
      }
    }

    ExtendedBinding binding = component.getBinding();
    if (binding != null) {
      // deal with explicit exclusions
      if (binding.existsExclusion(typedLocalName)) {
        Exclude exclusion = binding.getExclusion(typedLocalName);
        if (exclusion.getClassName() != null) {
          LOG.info("Dealing with exclusion for local element " + xPath + " as per binding file.");
          jClass.changeLocalName(exclusion.getClassName());
        }
        return;
      }

      // deal with explicit forces
      if (binding.existsForce(localName)) {

        List<String> localNamesList = _localNames.get(localName);
        memorizeCollision(xPath, localName, localNamesList);

        LOG.info(
            "Changing class name for local element " + xPath + " as per binding file (force).");
        checkAndChange(jClass, annotated, untypedXPath, typedLocalName);
        return;
      }
    }


    String jClassLocalName = jClass.getLocalName();
    String expectedClassNameDerivedFromXPath = _javaNaming.toJavaClassName(localName);
    if (!jClassLocalName.equals(expectedClassNameDerivedFromXPath)) {
      if (component.createGroupItem()) {
        xPath += "/#item";
      }
      _xpathToJClass.put(xPath, jClass);
      return;
    }

    if (mode.equals("field")) {
      if (annotated instanceof ModelGroup) {
        ModelGroup group = (ModelGroup) annotated;
        final boolean isReference = group.isReference();
        if (isReference) {
          return;
        }
      }

      if (annotated instanceof ElementDecl) {
        ElementDecl element = (ElementDecl) annotated;

        final boolean isReference = element.isReference();
        if (isReference) {
          ElementDecl referredElement = element.getReference();
          // if that global element definition is a substitution head,
          // we now
          // need to do work out the global element's type, and use
          // its
          // JClass instance to defer the type of the member currently
          // processed
          Enumeration<?> possibleSubstitutes = referredElement.getSubstitutionGroupMembers();
          if (possibleSubstitutes.hasMoreElements()) {
            XMLType referredType = referredElement.getType();
            String xPathType = XPathHelper.getSchemaLocation(referredType);
            JClass typeJClass = _xpathToJClass.get(xPathType);
            if (typeJClass != null) {
              jClass.changeLocalName(typeJClass.getLocalName());
            } else {
              // manually deriving class name for referenced type
              XMLBindingComponent temp = component;
              temp.setView(referredType);
              jClass.changeLocalName(temp.getJavaClassName());
              component.setView(annotated);
            }
            // String typeXPath = XPathHelper
            // .getSchemaLocation(referredElement);
            // JClass referredJClass = (JClass) _xpathToJClass
            // .get(typeXPath + "_class");
            // jClass.changeLocalName(referredJClass.getSuperClass()
            // .getLocalName());
          }
          return;
        }
      }
    }

    final boolean alreadyProcessed = _xpathToJClass.containsKey(xPath);

    // if already processed, change the JClass instance accordingly
    if (alreadyProcessed) {
      JClass jClassAlreadyProcessed = _xpathToJClass.get(xPath);
      jClass.changeLocalName(jClassAlreadyProcessed.getLocalName());
      return;
    }

    // register JClass instance for XPATH
    _xpathToJClass.put(xPath, jClass);

    if (LOG.isDebugEnabled()) {
      LOG.debug("Binding JClass[" + jClass.getName() + "] for XML schema structure " + xPath);
    }

    // global elements don't need to change
    final boolean isGlobalElement = _globalElements.contains(untypedXPath);
    if (isGlobalElement) {
      return;
    }

    // resolve references to global elements
    if (mode.equals("field") && annotated instanceof ElementDecl) {
      ElementDecl element = (ElementDecl) annotated;
      final boolean isReference = element.isReference();
      if (isReference) {
        ElementDecl referredElement = element.getReference();
        // if that global element definition is a substitution head, we
        // now
        // need to do work out the global element's type, and use its
        // JClass instance to defer the type of the member currently
        // processed
        Enumeration<?> possibleSubstitutes = referredElement.getSubstitutionGroupMembers();
        if (possibleSubstitutes.hasMoreElements()) {
          String typeXPath = XPathHelper.getSchemaLocation(referredElement);
          JClass referredJClass = _xpathToJClass.get(typeXPath + "_class");
          jClass.changeLocalName(referredJClass.getSuperClass().getLocalName());
        }
        return;
      }
    }

    // resolve conflict with a global element
    final boolean conflictExistsWithGlobalElement = _globalElements.contains("/" + localXPath);
    if (conflictExistsWithGlobalElement) {
      LOG.info("Resolving conflict for local element " + xPath + " against global element.");

      checkAndChange(jClass, annotated, untypedXPath, typedLocalName);

      // remember that we had a collision for this local element
      List<String> localNamesList = _localNames.get(localName);
      memorizeCollision(xPath, localName, localNamesList);
      return;
    }

    List<String> localNamesList = _localNames.get(localName);
    memorizeCollision(xPath, localName, localNamesList);
    if (localNamesList == null) {
      String typedJClassName = _typedLocalNames.get(typedLocalName);
      if (typedJClassName == null) {
        _typedLocalNames.put(typedLocalName, jClass.getName());
      }
    } else {
      LOG.info("Resolving conflict for local element " + xPath
          + " against another local element of the same name.");
      checkAndChange(jClass, annotated, untypedXPath, typedLocalName);
    }
  }

  /**
   * Memorize that we have a collision for the 'local name' given.
   * 
   * @param xPath Full (typed) XPATH identifier for the local element definition.
   * @param localName Local element name
   * @param localNamesList Collection store for collisions for that 'local name'.
   */
  private void memorizeCollision(final String xPath, final String localName,
      final List<String> localNamesList) {
    // resolve conflict with another element
    if (localNamesList == null) {
      // this name never occured before
      List<String> arrayList = new ArrayList<String>();
      arrayList.add(xPath);
      _localNames.put(localName, arrayList);

    } else {
      // this entry should be renamed
      if (!localNamesList.contains(xPath)) {
        localNamesList.add(xPath);
      }
    }
  }

  /**
   * Check and change (suggested) class name.
   * 
   * @param jClass {@link JClass} instance
   * @param annotated {@link Annotated} instance
   * @param untypedXPath blah
   * @param typedLocalName blah
   */
  private void checkAndChange(final JClass jClass, final Annotated annotated,
      final String untypedXPath, final String typedLocalName) {

    // check whether we have seen that typed local name already
    String typedJClassName = _typedLocalNames.get(typedLocalName);
    if (typedJClassName != null) {
      // if so, simple re-use it by changing the local class name
      String localClassName = typedJClassName.substring(typedJClassName.lastIndexOf(".") + 1);
      jClass.changeLocalName(localClassName);
    } else {
      // 'calculate' a new class name
      changeClassInfoAsResultOfConflict(jClass, untypedXPath, typedLocalName, annotated);

      // store it for further use
      _typedLocalNames.put(typedLocalName, jClass.getName());
    }
  }

  /**
   * Returns the local name of rooted XPATH expression.
   * 
   * @param xPath An (rooted) XPATH expression
   * @return the local name
   */
  private String getLocalName(final String xPath) {
    String localName = xPath.substring(xPath.lastIndexOf("/") + 1);
    if (localName.startsWith(ExtendedBinding.COMPLEXTYPE_ID)
        || localName.startsWith(ExtendedBinding.SIMPLETYPE_ID)
        || localName.startsWith(ExtendedBinding.ENUMTYPE_ID)
        || localName.startsWith(ExtendedBinding.GROUP_ID)) {
      localName = localName.substring(localName.indexOf(":") + 1);
    }
    return localName;
  }

  /**
   * Returns the local part of rooted XPATH expression.
   * 
   * @param xPath An (rooted) XPATH expression
   * @return the local part
   */
  private String getLocalXPath(final String xPath) {
    return xPath.substring(xPath.lastIndexOf("/") + 1);
  }

  /**
   * Changes the JClass' internal class name, as a result of an XPATH expression uniquely
   * identifying an XML artefact within an XML schema.
   * 
   * @param jClass The {@link JClass} instance whose local name should be changed.
   * @param xpath XPATH expression used to defer the new local class name
   * @param typedXPath Typed XPATH expression used to defer the new local class name
   * @param annotated {@link Annotated} instance
   */
  private void changeClassInfoAsResultOfConflict(final JClass jClass, final String xpath,
      final String typedXPath, final Annotated annotated) {
    _classNameConflictResolver.changeClassInfoAsResultOfConflict(jClass, xpath, typedXPath,
        annotated);
  }

  /**
   * Sets the {@link ClassNameConflictResolver} insatnce to be used.
   * 
   * @param conflictResolver {@link ClassNameConflictResolver} insatnce to be used.
   */
  public void setClassNameConflictResolver(final ClassNameConflictResolver conflictResolver) {
    _classNameConflictResolver = conflictResolver;
  }

  /**
   * Utility method to gather and output statistical information about naming collisions occurred
   * during source code generation.
   * 
   * @param binding {@link XMLBindingComponent} instance
   */
  public void printStatistics(final XMLBindingComponent binding) {
    Iterator<String> keyIterator = _localNames.keySet().iterator();
    LOG.info("*** Summary ***");
    if (binding.getBinding() != null && binding.getBinding().getForces() != null
        && !binding.getBinding().getForces().isEmpty()) {
      Iterator<String> forceIterator = binding.getBinding().getForces().iterator();
      LOG.info("The following 'forces' have been enabled:");
      while (forceIterator.hasNext()) {
        String forceValue = forceIterator.next();
        LOG.info(forceValue);
      }
    }
    if (keyIterator.hasNext()) {
      LOG.info("Local name conflicts encountered for the following element definitions");
      while (keyIterator.hasNext()) {
        String localName = keyIterator.next();
        List<String> collisions = _localNames.get(localName);
        if (collisions.size() > 1 && !ofTheSameType(collisions)) {
          LOG.info(localName + ", with the following (element) definitions being involved:");
          for (Iterator<String> iter = collisions.iterator(); iter.hasNext();) {
            String xPath = iter.next();
            LOG.info(xPath);
          }
        }
      }
    }

    keyIterator = _localNames.keySet().iterator();
    if (keyIterator.hasNext()) {
      StringBuilder xmlFragment = new StringBuilder(32);
      xmlFragment.append("<forces>\n");
      while (keyIterator.hasNext()) {
        String localName = keyIterator.next();
        List<String> collisions = _localNames.get(localName);
        if (collisions.size() > 1 && !ofTheSameType(collisions)) {
          xmlFragment.append("   <force>");
          xmlFragment.append(localName);
          xmlFragment.append("</force>\n");
        }
      }
      xmlFragment.append("</forces>");

      LOG.info(xmlFragment.toString());
    }

  }

  /**
   * Indicates whether all XPATH entries within the list of collisions are of the same type.
   * 
   * @param collisions The list of XPATH (collidings) for a local element name
   * @return True if all are of the same type.
   */
  private boolean ofTheSameType(final List<String> collisions) {
    boolean allSame = true;
    Iterator<String> iterator = collisions.iterator();
    String typeString = null;
    while (iterator.hasNext()) {
      String xPath = iterator.next();
      String newTypeString = xPath.substring(xPath.indexOf("[") + 1, xPath.indexOf("]"));
      if (typeString != null) {
        if (!typeString.equals(newTypeString)) {
          allSame = false;
          break;
        }
      } else {
        typeString = newTypeString;
      }
    }
    return allSame;
  }

}
