/**
 *   Copyright &copy; 2008-2012 NetAllied Systems GmbH, Ravensburg, Germany. 
 *       
 *   Licensed under the MIT Open Source License, 
 *   for details please see LICENSE file or the website
 *   http://www.opensource.org/licenses/mit-license.php
*/
package de.netallied.xsd2cppsax;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.xerces.xs.XSConstants;
import org.apache.xerces.xs.XSElementDeclaration;
import org.apache.xerces.xs.XSNamedMap;
import org.apache.xerces.xs.XSObject;
import org.apache.xerces.xs.XSTypeDefinition;

/**
 * Creates C++ names for XML elements. As element names in XSD are not unique
 * there are several cases how the C++ name will be created see
 * {@link #createElementName(XSElementDeclaration)}.
 * 
 */
public class CppElementNameCreator extends AbstractStackBasedTraverser {

    /** Collected element usage data. */
    private Map<String, ElementUsage> completeElementUsage;

    /** Element to c++ element name mapping. */
    private Map<XSElementDeclaration, CppElementNameParentTypesPair> element2ElementName = new HashMap<XSElementDeclaration, CppElementNameParentTypesPair>();

    /** @see Generator#substitutionGroups */
    private Map<XSElementDeclaration, List<XSElementDeclaration>> substitutionGroups;

    /**
     * c-tor.
     * 
     * @param config
     *            Configuration.
     * @param completeElementUsage
     *            Element usage data.
     */
    public CppElementNameCreator(Config config, Map<String, ElementUsage> completeElementUsage,
            Map<XSElementDeclaration, List<XSElementDeclaration>> substitutionGroups) {
        super(config);
        this.completeElementUsage = completeElementUsage;
        this.substitutionGroups = substitutionGroups;
    }

    /**
     * Collects XSD types which have given element as child.
     * 
     * @param element
     *            Element to collect parents for.
     * @param elementUsage
     *            Element usage data.
     * @return Collected parent types.
     */
    protected SortedSet<XSTypeDefinition> collectParentTypes(XSElementDeclaration element, ElementUsage elementUsage) {
        SortedSet<XSTypeDefinition> parentTypes = new TreeSet<XSTypeDefinition>(new XSTypeComparator());
        for (XSElementDeclaration parent : elementUsage.collectParents()) {
            DirectChildrenCollector childrenCollector = new DirectChildrenCollector();
            Set<XSElementDeclaration> siblings = childrenCollector.collect(parent.getTypeDefinition(),
                    substitutionGroups, new HashSet<XSTypeDefinition>());
            for (XSElementDeclaration sibling : siblings) {
                if (sibling.getName().equals(element.getName())
                        && sibling.getTypeDefinition() == element.getTypeDefinition()) {
                    parentTypes.add(parent.getTypeDefinition());
                }
            }
        }
        return parentTypes;
    }

    /**
     * Counts how many XSD types exist with given name. If out commented code is
     * used, only those types are counted which have a child element with given
     * child name.
     * 
     * @param parentName
     *            Element name to cout types for.
     * 
     * @param childName
     *            Name of child element to reduce number of types.
     * @return Number of types.
     */
    protected int countParentsWithDifferentTypesAndSameNamesWhichHaveGivenChild(String parentName, String childName) {
        List<XSElementDeclaration> elements = completeElementUsage.get(parentName).getElements();
        Set<XSTypeDefinition> types = new HashSet<XSTypeDefinition>();
        for (XSElementDeclaration elem : elements) {
            types.add(elem.getTypeDefinition());
        }
        // Set<XSTypeDefinition> toRemove = new HashSet<XSTypeDefinition>();
        // for (XSTypeDefinition type : types) {
        // DirectChildrenCollector collector = new DirectChildrenCollector();
        // Set<String> children = collector.collect(type);
        // if (children == null || !children.contains(childName)) {
        // toRemove.add(type);
        // }
        // }
        // types.removeAll(toRemove);
        return types.size();
    }

    /**
     * Creates C++ Name to be used in generated code. Must take care of name
     * clashes.
     * 
     * @param element
     *            Element to create name for.
     * @return C++ Name.
     */
    protected CppElementNameParentTypesPair createElementName(XSElementDeclaration element) {
        String name = "";

        ElementUsage elementUsage = completeElementUsage.get(element.getName());
        List<XSElementDeclaration> elements = elementUsage.getElements();
        SortedSet<XSTypeDefinition> types = new TreeSet<XSTypeDefinition>(new XSTypeComparator());
        for (XSElementDeclaration elem : elements) {
            types.add(elem.getTypeDefinition());
        }

        // case 1: element-name has only one type
        if (types.size() == 1) {
            CppElementNameParentTypesPair returnVal = new CppElementNameParentTypesPair();
            returnVal.cppElementName = Util.replaceCharactersForbiddenInCppIdentifiers(element.getName());
            returnVal.parentTypes = new TreeSet<XSTypeDefinition>(new XSTypeComparator());
            for (XSElementDeclaration parent : elementUsage.collectParents()) {
                returnVal.parentTypes.add(parent.getTypeDefinition());
            }
            return returnVal;
        }

        // case 2: type has a name
        String delmi = config.getElementNameDelimiter();
        if (element.getTypeDefinition().getName() != null) {
            String uniqueTypeName = makeXstypeNamespaceUnique(element);
            CppElementNameParentTypesPair returnVal = new CppElementNameParentTypesPair();
            returnVal.cppElementName = Util.replaceCharactersForbiddenInCppIdentifiers(element.getName() + delmi
                    + delmi + uniqueTypeName);
            returnVal.parentTypes = collectParentTypes(element, elementUsage);
            return returnVal;
        }

        // case 3: anonymous type inside a type with a name
        SortedSet<XSTypeDefinition> parentTypes = collectParentTypes(element, elementUsage);
        // XSTypeDefinition exactParentType = findExactParentType(element,
        // parentTypes);
        XSElementDeclaration parentElement = getParentElementFromStack();
        XSTypeDefinition exactParentType = parentElement.getTypeDefinition();
        if (exactParentType != null && exactParentType.getName() != null) {
            String uniqueParentName = makeXstypeNamespaceUnique(parentElement);
            name = uniqueParentName + delmi + delmi + element.getName();
            CppElementNameParentTypesPair returnVal = new CppElementNameParentTypesPair();
            returnVal.cppElementName = Util.replaceCharactersForbiddenInCppIdentifiers(name);
            returnVal.parentTypes = parentTypes;
            return returnVal;
        }

        // case 4: anonymous type inside an element which has been declared as
        // top level element
        // TODO make this element naming case namespace unique
        XSNamedMap elementMap = getModel().getComponents(XSConstants.ELEMENT_DECLARATION);
        for (int i = 0; i < elementMap.getLength(); i++) {
            XSObject item = elementMap.item(i);
            if (item.getType() == XSConstants.ELEMENT_DECLARATION) {
                if (item == element) {
                    name = element.getName();
                    CppElementNameParentTypesPair returnVal = new CppElementNameParentTypesPair();
                    returnVal.cppElementName = Util.replaceCharactersForbiddenInCppIdentifiers(name);
                    returnVal.parentTypes = parentTypes;
                    return returnVal;
                }
            }
        }

        // case 5: anonymous type inside an element which has an not unique name
        // TODO make this element naming case namespace unique
        List<List<XSElementDeclaration>> possibilities = elementUsage.getAncestors();

        name += element.getName();

        if (possibilities.size() > 1 && getElementStack().size() > 1) {
            int i = getElementStack().size() - 2;
            String parentName = getElementStack().get(i).getName();
            String childName = getElementStack().get(i + 1).getName();
            name = parentName + delmi + name;
            while (countParentsWithDifferentTypesAndSameNamesWhichHaveGivenChild(parentName, childName) > 1 && i > 0) {
                i--;
                childName = parentName;
                parentName = getElementStack().get(i).getName();
                name = parentName + delmi + name;
            }
        }
        CppElementNameParentTypesPair returnVal = new CppElementNameParentTypesPair();
        returnVal.cppElementName = Util.replaceCharactersForbiddenInCppIdentifiers(name);
        returnVal.parentTypes = parentTypes;
        return returnVal;
    }

    /**
     * @return created element names.
     */
    public Map<XSElementDeclaration, CppElementNameParentTypesPair> getElementNameMapping() {
        return element2ElementName;
    }

    /**
     * {@inheritDoc}
     * 
     * @see de.netallied.xsd2cppsax.AbstractXSTraverser#handleElement(com.sun.org.apache.xerces.internal.xs.XSElementDeclaration)
     */
    @Override
    public boolean handleElement(XSElementDeclaration element) {
        if (!checkStack(element)) {
            return true;
        }

        getElementStack().push(element);

        if (!element2ElementName.containsKey(element)) {
            CppElementNameParentTypesPair cppNameAndParentsPair = createElementName(element);
            CppElementNameParentTypesPair oldValue = element2ElementName.put(element, cppNameAndParentsPair);
            if (oldValue != null) {
                System.err.println("*** old cpp name: " + oldValue.cppElementName + ", new one: "
                        + cppNameAndParentsPair.cppElementName);
            }
            Map<String, String> elementNameMapping = getConfig().getElementNameMapping();
            if (elementNameMapping.containsKey(cppNameAndParentsPair.cppElementName)) {
                cppNameAndParentsPair.cppElementName = elementNameMapping.get(cppNameAndParentsPair.cppElementName);
            }
        }

        super.handleElement(element);

        getElementStack().pop();
        return true;
    }

    /**
     * Checks if there are other xml namespaces containing a type with same name
     * as given element's type. If this is the case a namespace identifier is
     * added to the type name.
     * 
     * @param element
     *            Element of which type shall be made unique in all known
     *            namespaces.
     * @return unique type name.
     */
    protected String makeXstypeNamespaceUnique(XSElementDeclaration element) {
        ElementUsage elementUsage = completeElementUsage.get(element.getName());
        Set<XSTypeDefinition> types = elementUsage.getTypes();
        Set<XSTypeDefinition> typesWithSameName = new HashSet<XSTypeDefinition>();
        String givenElementTypeName = Util.replaceCharactersForbiddenInCppIdentifiers(element.getTypeDefinition()
                .getName());
        for (XSTypeDefinition type : types) {
            if (type == element.getTypeDefinition() || type.getName() == null) {
                continue;
            }
            String otherTypeName = Util.replaceCharactersForbiddenInCppIdentifiers(type.getName());
            if (type.getName() != null && givenElementTypeName.equals(otherTypeName)) {
                typesWithSameName.add(type);
            }
        }
        String uniqueTypeName = element.getTypeDefinition().getName();
        if (!typesWithSameName.isEmpty()) {
            String targetNamespace = Util.findTargetNamespace(getModel());
            String typeNamespace = element.getTypeDefinition().getNamespace();
            if (!typeNamespace.equals(targetNamespace)) {
                String delmi = config.getElementNameDelimiter();
                Map<String, String> xsNamespaceMapping = config.getXsNamespaceMapping();
                if (xsNamespaceMapping.containsKey(typeNamespace)) {
                    typeNamespace = xsNamespaceMapping.get(typeNamespace);
                }
                uniqueTypeName += delmi + delmi + typeNamespace;
            }
        }
        return uniqueTypeName;
    }

}
