/*   Copyright 2004 The Apache Software Foundation
 *
 *   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.apache.xmlbeans.impl.schema;

import org.apache.xmlbeans.*;
import org.apache.xmlbeans.impl.common.NameUtil;

import javax.xml.namespace.QName;
import java.math.BigInteger;
import java.util.*;

public class StscJavaizer {

    /**
     * XMLBEANS-307
     * if enumeration count is greater than 3668,
     * xmlbeans scomp will fail with a code too large error
     */
    private static final int MAX_ENUM_COUNT = 3668;

    /**
     * Does a topo walk of all the types to resolve them.
     */
    public static void javaizeAllTypes(boolean javaize) {
        StscState state = StscState.get();

        List<SchemaType> allSeenTypes = new ArrayList<>();
        allSeenTypes.addAll(Arrays.asList(state.documentTypes()));
        allSeenTypes.addAll(Arrays.asList(state.attributeTypes()));
        allSeenTypes.addAll(Arrays.asList(state.globalTypes()));

        // First distribute the global names among the top entities.
        if (javaize) {
            assignGlobalJavaNames(allSeenTypes);
        }

        // now fully javaize everything deeply.
        for (int i = 0; i < allSeenTypes.size(); i++) {
            SchemaType gType = allSeenTypes.get(i);
            if (javaize) {
                javaizeType((SchemaTypeImpl) gType);
                String className = gType.getFullJavaName();
                if (className != null) {
                    state.addClassname(className.replace('$', '.'), gType);
                }
            } else {
                skipJavaizingType((SchemaTypeImpl) gType);
            }
            allSeenTypes.addAll(Arrays.asList(gType.getAnonymousTypes()));
            // We need to javaize the anonymous types defined inside redefined types
            // since redefined type do not get a Java class of their own.
            // The exception is complex types derived by restriction, since in this case
            // anonymous types are not inherited
            addAnonymousTypesFromRedefinition(gType, allSeenTypes);
        }
    }

    static void assignGlobalJavaNames(Collection<SchemaType> schemaTypes) {
        HashSet<String> usedNames = new HashSet<>();
        StscState state = StscState.get();

        for (SchemaType schemaType : schemaTypes) {
            SchemaTypeImpl sImpl = (SchemaTypeImpl) schemaType;
            QName topName = findTopName(sImpl);
            String pickedName = state.getJavaname(topName, sImpl.isDocumentType() ?
                BindingConfig.QNAME_DOCUMENT_TYPE : BindingConfig.QNAME_TYPE);
            if (sImpl.isUnjavaized()) {
                sImpl.setFullJavaName(pickFullJavaClassName(usedNames, findTopName(sImpl), pickedName, sImpl.isDocumentType(), sImpl.isAttributeType()));
                sImpl.setFullJavaImplName(pickFullJavaImplName(usedNames, sImpl.getFullJavaName()));

                setUserTypes(sImpl, state);

                setExtensions(sImpl, state);
            }
        }

        verifyInterfaceNameCollisions(usedNames, state);
    }

    private static void verifyInterfaceNameCollisions(Set<String> usedNames, StscState state) {
        BindingConfig config = state.getBindingConfig();
        if (config == null) {
            return;
        }

        InterfaceExtension[] exts = config.getInterfaceExtensions();
        for (InterfaceExtension ext : exts) {
            if (usedNames.contains(ext.getInterface().toLowerCase(Locale.ROOT))) {
                state.error("InterfaceExtension interface '" + ext.getInterface() + "' creates a name collision with one of the generated interfaces or classes.", XmlError.SEVERITY_ERROR, null);
            }

            String handler = ext.getStaticHandler();
            if (handler != null && usedNames.contains(handler.toLowerCase(Locale.ROOT))) {
                state.error("InterfaceExtension handler class '" + handler + "' creates a name collision with one of the generated interfaces or classes.", XmlError.SEVERITY_ERROR, null);
            }
        }

        PrePostExtension[] prepost = config.getPrePostExtensions();
        for (PrePostExtension prePostExtension : prepost) {
            String handler = prePostExtension.getStaticHandler();
            if (handler != null && usedNames.contains(handler.toLowerCase(Locale.ROOT))) {
                state.error("PrePostExtension handler class '" + handler + "' creates a name collision with one of the generated interfaces or classes.", XmlError.SEVERITY_ERROR, null);
            }
        }
    }

    private static void setUserTypes(SchemaTypeImpl sImpl, StscState state) {
        BindingConfig config = state.getBindingConfig();

        if (config != null) {
            UserType utype = config.lookupUserTypeForQName(sImpl.getName());
            if (utype != null) {
                sImpl.setUserTypeName(utype.getJavaName());
                sImpl.setUserTypeHandlerName(utype.getStaticHandler());
            }
        }
    }

    private static void setExtensions(SchemaTypeImpl sImpl, StscState state) {
        String javaName = sImpl.getFullJavaName();
        BindingConfig config = state.getBindingConfig();

        if (javaName != null && config != null) {
            sImpl.setInterfaceExtensions(config.getInterfaceExtensions(javaName));
            sImpl.setPrePostExtension(config.getPrePostExtension(javaName));
        }
    }

    private static boolean isStringType(SchemaType type) {
        if (type == null || type.getSimpleVariety() != SchemaType.ATOMIC) {
            return false;
        }
        return (type.getPrimitiveType().getBuiltinTypeCode() == SchemaType.BTC_STRING);
    }

    static String pickConstantName(Set<String> usedNames, String words) {
        String base = NameUtil.upperCaseUnderbar(words);

        if (base.length() == 0) {
            base = "X";
        }

        if (base.startsWith("INT_")) {
            // reserved for int codes
            base = "X_" + base;
        }

        String uniqName;
        int index = 1;
        for (uniqName = base; usedNames.contains(uniqName); ) {
            index++;
            uniqName = base + "_" + index;
        }

        usedNames.add(uniqName);

        return uniqName;
    }

    static void skipJavaizingType(SchemaTypeImpl sImpl) {
        if (sImpl.isJavaized()) {
            return;
        }

        SchemaTypeImpl baseType = (SchemaTypeImpl) sImpl.getBaseType();
        if (baseType != null) {
            skipJavaizingType(baseType);
        }

        sImpl.startJavaizing();
        secondPassProcessType(sImpl);
        sImpl.finishJavaizing();
    }

    static void secondPassProcessType(SchemaTypeImpl sImpl) {
        if (!isStringType(sImpl)) {
            return;
        }

        XmlAnySimpleType[] enumVals = sImpl.getEnumerationValues();

        // if this is an enumerated string type, values are to be
        // javaized as constants.
        if (enumVals == null) {
            return;
        }

        // ERROR is found at > 3668
        if (enumVals.length > MAX_ENUM_COUNT) {
            StscState.get().warning("SchemaType Enumeration found with too many enumeration values to create a Java " +
                                    "enumeration. The base SchemaType \"" + sImpl.getBaseEnumType() + "\" will be used instead", XmlError.SEVERITY_WARNING, null);
            return;
        }
        SchemaType basedOn = sImpl.getBaseEnumType();
        if (basedOn == null) {
            return;
        }

        SchemaStringEnumEntry[] entryArray = new SchemaStringEnumEntry[enumVals.length];
        if (basedOn == sImpl) {
            Set<String> usedNames = new HashSet<>();
            for (int i = 0; i < enumVals.length; i++) {
                String val = enumVals[i].getStringValue();
                entryArray[i] = new SchemaStringEnumEntryImpl(val, i + 1, pickConstantName(usedNames, val));
            }
        } else {
            for (int i = 0; i < enumVals.length; i++) {
                String val = enumVals[i].getStringValue();
                entryArray[i] = basedOn.enumEntryForString(val);
            }
        }
        sImpl.setStringEnumEntries(entryArray);
    }

    static void javaizeType(SchemaTypeImpl sImpl) {
        if (sImpl.isJavaized()) {
            return;
        }

        SchemaTypeImpl baseType = (SchemaTypeImpl) sImpl.getBaseType();
        if (baseType != null) {
            javaizeType(baseType);
        }
        if (sImpl.getContentBasedOnType() != null && sImpl.getContentBasedOnType() != baseType) {
            javaizeType((SchemaTypeImpl) sImpl.getContentBasedOnType());
        }

        sImpl.startJavaizing();

        sImpl.setCompiled(true);

        secondPassProcessType(sImpl);

        if (!sImpl.isSimpleType()) {
            SchemaProperty[] eltProps = sImpl.getElementProperties();
            SchemaProperty[] attrProps = sImpl.getAttributeProperties();

            // Handing out java names - this permits us to avoid collisions.
            Set<String> usedPropNames = new HashSet<>();

            // First, copy all used property names from base, since these
            // cannnot be changed at this point and they may be arbitrary
            // because of derivation by restriction and the "nopvr" switch
            SchemaProperty[] baseProps = baseType.getProperties();
            for (SchemaProperty baseProp : baseProps) {
                String name = baseProp.getJavaPropertyName();
                assert !usedPropNames.contains(name);
                usedPropNames.add(name);
            }

            // count in the methods from extension interfaces
            avoidExtensionMethods(usedPropNames, sImpl);

            // Assign names in two passes: first inherited names, then others.
            for (boolean doInherited = true; ; doInherited = false) {
                if (eltProps.length > 0) {
                    assignJavaPropertyNames(usedPropNames, eltProps, baseType, doInherited);
                }

                assignJavaPropertyNames(usedPropNames, attrProps, baseType, doInherited);

                if (!doInherited) {
                    break;
                }
            }

            SchemaProperty[] allprops = sImpl.getProperties();

            // determine whether order insensitive
            boolean insensitive = isPropertyModelOrderInsensitive(allprops);

            // Fill in the java type codes now.
            // This depends on recursive type information, so it's done in typechecking
            assignJavaTypeCodes(allprops);

            sImpl.setOrderSensitive(!insensitive);
        }

        // assign java type names to anonymous types
        // for redefined types, this step was performed when javaizing the redefinition
        if (sImpl.getFullJavaName() != null || sImpl.getOuterType() != null) {
            assignJavaAnonymousTypeNames(sImpl);
        }

        sImpl.finishJavaizing();
    }

    private static final String[] PREFIXES = new String[]{"get", "xget", "isNil", "isSet", "sizeOf", "set",
        "xset", "addNew", "setNil", "unset", "insert", "add", "insertNew", "addNew", "remove"};

    private static void avoidExtensionMethods(Set<String> usedPropNames, SchemaTypeImpl sImpl) {
        InterfaceExtension[] exts = sImpl.getInterfaceExtensions();
        if (exts != null) {
            for (InterfaceExtension ext : exts) {
                InterfaceExtension.MethodSignature[] methods = ext.getMethods();
                for (InterfaceExtension.MethodSignature method : methods) {
                    String methodName = method.getName();
                    for (String prefix : PREFIXES) {
                        if (methodName.startsWith(prefix)) {
                            usedPropNames.add(methodName.substring(prefix.length()));
                        }
                    }
                }
            }
        }
    }

    static void assignJavaAnonymousTypeNames(SchemaTypeImpl outerType) {
        Set<String> usedTypeNames = new HashSet<>();
        SchemaType[] anonymousTypes = outerType.getAnonymousTypes();
        StscState state = StscState.get();

        int nrOfAnonTypes = anonymousTypes.length;
        if (outerType.isRedefinition()) {
            // We have to add the anonymous types for redefinitions to the list
            // since they don't have another outer class
            ArrayList<SchemaType> list = new ArrayList<>();
            addAnonymousTypesFromRedefinition(outerType, list);
            if (list.size() > 0) {
                SchemaType[] temp = new SchemaType[nrOfAnonTypes + list.size()];
                list.toArray(temp);
                System.arraycopy(anonymousTypes, 0, temp, list.size(), nrOfAnonTypes);
                anonymousTypes = temp;
            }
        }

        // Because we generate nested java interfaces, and nested
        // interface names must not be the same as an ancestor, use up
        // the ancestors

        for (SchemaType scanOuterType = outerType;
             scanOuterType != null;
             scanOuterType = scanOuterType.getOuterType()) {
            usedTypeNames.add(scanOuterType.getShortJavaName());
        }

        for (SchemaType scanOuterType = outerType;
             scanOuterType != null;
             scanOuterType = scanOuterType.getOuterType()) {
            usedTypeNames.add(scanOuterType.getShortJavaImplName());
        }

        // and because things are problematic if an inner type name
        // is the same as a top-level package name, also get rid of that
        // collision
        usedTypeNames.add(getOutermostPackage(outerType.getFullJavaName()));

        // assign names
        for (int i = 0; i < anonymousTypes.length; i++) {
            SchemaTypeImpl sImpl = (SchemaTypeImpl) anonymousTypes[i];
            if (sImpl == null) // already handled in first pass
            {
                continue;
            }
            if (sImpl.isSkippedAnonymousType()) {
                continue;
            }
            String localname = null;
            String javaname;

            SchemaField containerField = sImpl.getContainerField();
            if (containerField != null) {
                QName qname = sImpl.getContainerField().getName();
                localname = qname.getLocalPart();
                javaname = state.getJavaname(sImpl.getContainerField().getName(), BindingConfig.QNAME_TYPE);
            } else {
                // not defined inside an Elt or Attr: must be a nested simple type
                switch (sImpl.getOuterType().getSimpleVariety()) {
                    case SchemaType.UNION:
                        javaname = "Member";
                        break;
                    case SchemaType.LIST:
                        javaname = "Item";
                        break;
                    case SchemaType.ATOMIC:
                    default:
                        assert (false) : "Weird type " + sImpl.toString();
                        javaname = "Base";
                        break;
                }
            }

            if (i < nrOfAnonTypes) {
                sImpl.setShortJavaName(
                    pickInnerJavaClassName(usedTypeNames, localname, javaname));
                sImpl.setShortJavaImplName(
                    pickInnerJavaImplName(usedTypeNames, localname, javaname == null ? null : javaname + "Impl"));
            } else {
                // This comes from redefined types, so we have to compute the
                // full name here
                sImpl.setFullJavaName(outerType.getFullJavaName() + "$" +
                                      pickInnerJavaClassName(usedTypeNames, localname, javaname));
                sImpl.setFullJavaImplName(outerType.getFullJavaImplName() + "$" +
                                          pickInnerJavaImplName(usedTypeNames, localname, javaname == null ? null : javaname + "Impl"));
            }

            // TODO(radup) why is this inside this loop here?
            setExtensions(sImpl, state);
        }
    }

    static void assignJavaPropertyNames(Set<String> usedNames, SchemaProperty[] props, SchemaType baseType, boolean doInherited) {
        StscState state = StscState.get();

        // two passes: first deal with inherited properties, then with new ones.
        // this ensures that we match up with base class definitions cleanly
        // BUGBUG(radup) We have to look for particles that have been removed
        // in the derivation tree for this type using derivation by restriction,
        // because they have not been removed in Java and may collide with
        // this type's properties.

        for (SchemaProperty prop : props) {
            SchemaPropertyImpl sImpl = (SchemaPropertyImpl) prop;

            SchemaProperty baseProp =
                (sImpl.isAttribute() ?
                    baseType.getAttributeProperty(sImpl.getName()) :
                    baseType.getElementProperty(sImpl.getName()));

            if ((baseProp == null) == doInherited) {
                continue;
            }

            QName propQName = sImpl.getName();

            String theName;

            if (baseProp == null) {
                theName = pickJavaPropertyName(usedNames, propQName.getLocalPart(),
                    state.getJavaname(propQName, sImpl.isAttribute() ? BindingConfig.QNAME_ACCESSOR_ATTRIBUTE :
                        BindingConfig.QNAME_ACCESSOR_ELEMENT));
            } else {
                theName = baseProp.getJavaPropertyName();
            }

            sImpl.setJavaPropertyName(theName);

            boolean isArray = (sImpl.getMaxOccurs() == null ||
                               sImpl.getMaxOccurs().compareTo(BigInteger.ONE) > 0);
            boolean isSingleton = !isArray && (sImpl.getMaxOccurs().signum() > 0);
            boolean isOption = isSingleton && (sImpl.getMinOccurs().signum() == 0);
            SchemaType javaBasedOnType = sImpl.getType();

            if (baseProp != null) {
                if (baseProp.extendsJavaArray()) {
                    isSingleton = false;
                    isOption = false;
                    isArray = true;
                }
                if (baseProp.extendsJavaSingleton()) {
                    isSingleton = true;
                }
                if (baseProp.extendsJavaOption()) {
                    isOption = true;
                }
                javaBasedOnType = baseProp.javaBasedOnType();
            }

            sImpl.setExtendsJava(javaBasedOnType.getRef(), isSingleton, isOption, isArray);
        }

    }

    static void assignJavaTypeCodes(SchemaProperty[] properties) {
        for (SchemaProperty property : properties) {
            SchemaPropertyImpl sImpl = (SchemaPropertyImpl) property;
            SchemaType sType = sImpl.javaBasedOnType();
            sImpl.setJavaTypeCode(javaTypeCodeForType(sType));
        }
    }

    static int javaTypeCodeInCommon(SchemaType[] types) {
        if (types == null || types.length == 0) {
            return SchemaProperty.XML_OBJECT;
        }

        int code = javaTypeCodeForType(types[0]);
        if (code == SchemaProperty.JAVA_OBJECT) {
            return code;
        }
        for (int i = 1; i < types.length; i++) {
            // if any two are different, the answer is java.lang.Object
            if (code != javaTypeCodeForType(types[i])) {
                return SchemaProperty.JAVA_OBJECT;
            }
        }
        return code;
    }

    static int javaTypeCodeForType(SchemaType sType) {
        if (!sType.isSimpleType()) {
            return SchemaProperty.XML_OBJECT;
        }

        if (((SchemaTypeImpl) sType).getUserTypeHandlerName() != null) {
            return SchemaProperty.JAVA_USER;
        }

        if (sType.getSimpleVariety() == SchemaType.UNION) {
            // see if we can find an interesting common base type, e.g., for string enums
            SchemaType baseType = sType.getUnionCommonBaseType();
            if (baseType != null && !baseType.isURType()) {
                sType = baseType;
            } else {
                return javaTypeCodeInCommon(sType.getUnionConstituentTypes());
            }
        }

        if (sType.getSimpleVariety() == SchemaType.LIST) {
            return SchemaProperty.JAVA_LIST;
        }

        if (sType.isURType()) {
            return SchemaProperty.XML_OBJECT;
        }

        switch (sType.getPrimitiveType().getBuiltinTypeCode()) {
            case SchemaType.BTC_ANY_SIMPLE:
                // return SchemaProperty.XML_OBJECT;
                return SchemaProperty.JAVA_STRING;

            case SchemaType.BTC_BOOLEAN:
                return SchemaProperty.JAVA_BOOLEAN;

            case SchemaType.BTC_BASE_64_BINARY:
                return SchemaProperty.JAVA_BYTE_ARRAY;

            case SchemaType.BTC_HEX_BINARY:
                return SchemaProperty.JAVA_BYTE_ARRAY;

            case SchemaType.BTC_ANY_URI:
                return SchemaProperty.JAVA_STRING;

            case SchemaType.BTC_QNAME:
                return SchemaProperty.JAVA_QNAME;

            case SchemaType.BTC_NOTATION:
                return SchemaProperty.XML_OBJECT;

            case SchemaType.BTC_FLOAT:
                return SchemaProperty.JAVA_FLOAT;

            case SchemaType.BTC_DOUBLE:
                return SchemaProperty.JAVA_DOUBLE;

            case SchemaType.BTC_DECIMAL:
                switch (sType.getDecimalSize()) {
                    case SchemaType.SIZE_BYTE:
                        return SchemaProperty.JAVA_BYTE;
                    case SchemaType.SIZE_SHORT:
                        return SchemaProperty.JAVA_SHORT;
                    case SchemaType.SIZE_INT:
                        return SchemaProperty.JAVA_INT;
                    case SchemaType.SIZE_LONG:
                        return SchemaProperty.JAVA_LONG;
                    case SchemaType.SIZE_BIG_INTEGER:
                        return SchemaProperty.JAVA_BIG_INTEGER;
                    case SchemaType.SIZE_BIG_DECIMAL:
                    default:
                        return SchemaProperty.JAVA_BIG_DECIMAL;
                }

            case SchemaType.BTC_STRING:
                if (isStringType(sType.getBaseEnumType())) {
                    // This is necessary for local types, etc.
                    // schema enums with > ~3668 cause a Java Src file to be created
                    // that cannot be compiled due to JVM restrictions
                    // FIXFIX: http://issues.apache.org/jira/browse/XMLBEANS-307
                    // FIXFIX: XMLBeans scomp throws error "code too large"
                    if (sType.getEnumerationValues() != null &&
                        sType.getEnumerationValues().length > MAX_ENUM_COUNT) {
                        return SchemaProperty.JAVA_STRING;
                    } else {
                        return SchemaProperty.JAVA_ENUM;
                    }
                }
                return SchemaProperty.JAVA_STRING;

            case SchemaType.BTC_DURATION:
                return SchemaProperty.JAVA_GDURATION;

            case SchemaType.BTC_DATE_TIME:
            case SchemaType.BTC_DATE:
                // return SchemaProperty.JAVA_DATE; // converted to calendar

            case SchemaType.BTC_TIME:
            case SchemaType.BTC_G_YEAR_MONTH:
            case SchemaType.BTC_G_YEAR:
            case SchemaType.BTC_G_MONTH_DAY:
            case SchemaType.BTC_G_DAY:
            case SchemaType.BTC_G_MONTH:
                // return SchemaProperty.JAVA_GDATE; // converted to calendar (JAX-B)
                return SchemaProperty.JAVA_CALENDAR;

            default:
                assert (false) : "unrecognized code " + sType.getPrimitiveType().getBuiltinTypeCode();
                throw new IllegalStateException("unrecognized code " + sType.getPrimitiveType().getBuiltinTypeCode() + " of " + sType.getPrimitiveType().getName());
        }
    }

    static boolean isPropertyModelOrderInsensitive(SchemaProperty[] properties) {
        for (SchemaProperty prop : properties) {
            if (prop.hasNillable() == SchemaProperty.VARIABLE) {
                return false;
            }
            if (prop.hasDefault() == SchemaProperty.VARIABLE) {
                return false;
            }
            if (prop.hasFixed() == SchemaProperty.VARIABLE) {
                return false;
            }
            if (prop.hasDefault() != SchemaProperty.NEVER &&
                prop.getDefaultText() == null) {
                return false;
            }
        }
        return true;
    }

    static boolean protectReservedGlobalClassNames(String name) {
        int i = name.lastIndexOf('.');
        String lastSegment = name.substring(i + 1);
        return lastSegment.endsWith("Document") && !lastSegment.equals("Document");
    }

    static boolean protectReservedInnerClassNames(String name) {
        return (name.equals("Enum") || name.equals("Factory"));
    }

    static String[] PROTECTED_PROPERTIES = {
        "StringValue",
        "BooleanValue",
        "ByteValue",
        "ShortValue",
        "IntValue",
        "LongValue",
        "BigIntegerValue",
        "BigDecimalValue",
        "FloatValue",
        "DoubleValue",
        "ByteArrayValue",
        "EnumValue",
        "CalendarValue",
        "DateValue",
        "GDateValue",
        "GDurationValue",
        "QNameValue",
        "ListValue",
        "ObjectValue",
        "Class",
    };
    static Set<String> PROTECTED_PROPERTIES_SET = new HashSet<>(Arrays.asList(PROTECTED_PROPERTIES));

    static boolean protectReservedPropertyNames(String name) {
        return PROTECTED_PROPERTIES_SET.contains(name) ||
               (name.endsWith("Array") && !name.equals("Array"));
    }

    static String pickFullJavaClassName(Set<String> usedNames, QName qName, String configname, boolean isDocument, boolean isAttrType) {
        String base;
        boolean protect;

        if (configname != null && configname.indexOf('.') >= 0) {
            // a configname with dots defines the fully qualified java class name
            base = configname;
            protect = protectReservedGlobalClassNames(base);
        } else {
            StscState state = StscState.get();
            String uri = qName.getNamespaceURI();

            base = NameUtil.getClassNameFromQName(qName);

            // Check to see if we have a mapping from namespace URI to Java package
            // name. If so, apply the mapped package prefix at the beginning of
            // the base name

            String pkgPrefix = state.getPackageOverride(uri);

            if (pkgPrefix != null) {
                // Form the new qualified class name from the new package name
                // and the old class name
                base = pkgPrefix + "." + base.substring(base.lastIndexOf('.') + 1);
            }

            // See if there is a prefix...
            String javaPrefix = state.getJavaPrefix(uri);
            if (javaPrefix != null) {
                base = base.substring(0, base.lastIndexOf('.') + 1) + javaPrefix + base.substring(base.lastIndexOf('.') + 1);
            }

            // a configname without dots may override the shortname part.
            if (configname != null) {
                base = base.substring(0, base.lastIndexOf('.') + 1) + configname;
            }

            protect = protectReservedGlobalClassNames(base);
            if (configname == null) {
                // add special suffix
                if (isDocument) {
                    base = base + "Document";
                } else if (isAttrType) {
                    base = base + "Attribute";
                }

                // add configured suffix
                String javaSuffix = state.getJavaSuffix(uri);
                if (javaSuffix != null) {
                    base = base + javaSuffix;
                }
            }
        }

        String outermostPkg = getOutermostPackage(base);

        int index = 1;
        String uniqName;
        if (protect) {
            uniqName = base + index;
        } else {
            uniqName = base;
        }
        while (usedNames.contains(uniqName.toLowerCase(Locale.ROOT)) || uniqName.equals(outermostPkg)) {
            index++;
            uniqName = base + index;
        }

        usedNames.add(uniqName.toLowerCase(Locale.ROOT));

        return uniqName;
    }

    static String getOutermostPackage(String fqcn) {
        if (fqcn == null) {
            return "";
        }

        // remove class name
        int lastdot = fqcn.indexOf('.');
        if (lastdot < 0) {
            return "";
        }

        // remove outer package names
        return fqcn.substring(0, lastdot);
    }

    static String pickFullJavaImplName(Set<String> usedNames, String intfName) {
        // Strip off the package from the class name so we can replace it
        String className = intfName;
        String pkgName = null;
        int index = intfName.lastIndexOf('.');
        if (index >= 0) {
            className = intfName.substring(index + 1);
            pkgName = intfName.substring(0, index);
        }

        // Form the new qualified class name from the new package name
        // and the old class name
        String base = pkgName + ".impl." + className + "Impl";

        index = 1;
        String uniqName = base;
        while (usedNames.contains(uniqName.toLowerCase(Locale.ROOT))) {
            index++;
            uniqName = base + index;
        }

        usedNames.add(uniqName.toLowerCase(Locale.ROOT));

        return uniqName;
    }

    static String pickJavaPropertyName(Set<String> usedNames, String localName, String javaName) {
        if (javaName == null) {
            javaName = NameUtil.upperCamelCase(localName);
        }
        boolean protect = protectReservedPropertyNames(javaName);
        String uniqName;
        int index = 1;
        if (protect) {
            uniqName = javaName + index;
        } else {
            uniqName = javaName;
        }
        while (usedNames.contains(uniqName)) {
            index++;
            uniqName = javaName + index;
        }

        usedNames.add(uniqName);

        return uniqName;
    }

    static String pickInnerJavaClassName(Set<String> usedNames, String localName, String javaName) {
        if (javaName == null) {
            javaName = NameUtil.upperCamelCase(localName);
        }
        boolean protect = protectReservedInnerClassNames(javaName);
        String uniqName;
        int index = 1;
        if (protect) {
            uniqName = javaName + index;
        } else {
            uniqName = javaName;
        }
        while (usedNames.contains(uniqName)) {
            index++;
            uniqName = javaName + index;
        }

        usedNames.add(uniqName);

        return uniqName;
    }

    static String pickInnerJavaImplName(Set<String> usedNames, String localName, String javaName) {
        if (javaName == null) {
            javaName = NameUtil.upperCamelCase(localName) + "Impl";
        }
        String uniqName = javaName;
        int index = 1;
        while (usedNames.contains(uniqName)) {
            index++;
            uniqName = javaName + index;
        }

        usedNames.add(uniqName);

        return uniqName;
    }

    static QName findTopName(SchemaType sType) {
        if (sType.getName() != null) {
            return sType.getName();
        }

        if (sType.isDocumentType()) {
            // A document type must have a content model consisting of a single elt
            if (sType.getContentModel() == null || sType.getContentModel().getParticleType() != SchemaParticle.ELEMENT) {
                throw new IllegalStateException();
            }
            return (sType.getDocumentElementName());
        }

        if (sType.isAttributeType()) {
            if (sType.getAttributeModel() == null || sType.getAttributeModel().getAttributes().length != 1) {
                throw new IllegalStateException();
            }
            return sType.getAttributeTypeAttributeName();
        }

        SchemaField sElt = sType.getContainerField();
        assert (sElt != null);
        assert (sType.getOuterType() == null);
        return sElt.getName();
    }

    static void addAnonymousTypesFromRedefinition(SchemaType sType, List<SchemaType> result) {
        while (((SchemaTypeImpl) sType).isRedefinition() &&
               (sType.getDerivationType() == SchemaType.DT_EXTENSION ||
                sType.isSimpleType())) {
            sType = sType.getBaseType();
            SchemaType[] newAnonTypes = sType.getAnonymousTypes();
            if (newAnonTypes.length > 0) {
                result.addAll(Arrays.asList(newAnonTypes));
            }
        }
    }
}
