/*   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.QNameHelper;
import org.apache.xmlbeans.impl.xb.xsdschema.*;
import org.apache.xmlbeans.impl.xb.xsdschema.AnyDocument.Any;
import org.apache.xmlbeans.impl.xb.xsdschema.SchemaDocument.Schema;

import javax.xml.namespace.QName;
import java.math.BigInteger;
import java.util.*;
import java.util.function.BinaryOperator;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StscComplexTypeResolver {
    public static Group getContentModel(ComplexType parseCt) {
        return getContentModel(parseCt::getAll, parseCt::getSequence, parseCt::getChoice, parseCt::getGroup);
    }

    public static Group getContentModel(ComplexRestrictionType parseRest) {
        return getContentModel(parseRest::getAll, parseRest::getSequence, parseRest::getChoice, parseRest::getGroup);
    }

    public static Group getContentModel(ExtensionType parseExt) {
        return getContentModel(parseExt::getAll, parseExt::getSequence, parseExt::getChoice, parseExt::getGroup);
    }

    @SafeVarargs
    private static Group getContentModel(Supplier<? extends Group>... grps) {
        return Stream.of(grps).map(Supplier::get).filter(Objects::nonNull).findFirst().orElse(null);
    }

    static Schema getSchema(XmlObject o) {
        try (XmlCursor c = o.newCursor()) {
            while (c.toParent()) {
                o = c.getObject();

                if (o.schemaType().equals(Schema.type)) {
                    return (Schema) o;
                }
            }
        }

        return null;
    }

    public static void resolveComplexType(SchemaTypeImpl sImpl) {
        ComplexType parseCt = (ComplexType) sImpl.getParseObject();
        StscState state = StscState.get();
        Schema schema = getSchema(parseCt);

        // Set abstract & final flags
        boolean abs = parseCt.isSetAbstract() && parseCt.getAbstract();
        boolean finalExt = false;
        boolean finalRest = false;
        boolean finalList = false;
        boolean finalUnion = false;

        Object ds = null;
        if (parseCt.isSetFinal()) {
            ds = parseCt.getFinal();
        }
        // Inspect the final default attribute on the schema
        else if (schema != null && schema.isSetFinalDefault()) {
            ds = schema.getFinalDefault();
        }

        if (ds != null) {
            if (ds instanceof String && ds.equals("#all")) {
                // #ALL value
                finalExt = finalRest = finalList = finalUnion = true;
            } else if (ds instanceof List) {
                List<?> dsl = (List<?>) ds;
                finalExt = dsl.contains("extension");
                finalRest = dsl.contains("restriction");

// Since complex types don't participate in list and unions, these can remain
// false.  Perhaps we should throw an error.

//                if (((List)ds).contains("list"))
//                    finalList = true;
//
//                if (((List)ds).contains("union"))
//                    finalUnion = true;
            }
        }

        sImpl.setAbstractFinal(abs, finalExt, finalRest, finalList, finalUnion);

        // Set block flags
        boolean blockExt = false;
        boolean blockRest = false;
        Object block = null;

        if (parseCt.isSetBlock()) {
            block = parseCt.getBlock();
        } else if (schema != null && schema.isSetBlockDefault()) {
            block = schema.getBlockDefault();
        }

        if (block != null) {
            if (block instanceof String && block.equals("#all")) {
                // #ALL value
                blockExt = blockRest = true;
            } else if (block instanceof List) {
                List<?> blist = (List<?>) block;
                if (blist.contains("extension")) {
                    blockExt = true;
                }
                if (blist.contains("restriction")) {
                    blockRest = true;
                }
            }
        }

        sImpl.setBlock(blockExt, blockRest);

        // Verify: have simpleContent, complexContent, or direct stuff
        ComplexContentDocument.ComplexContent parseCc = parseCt.getComplexContent();
        SimpleContentDocument.SimpleContent parseSc = parseCt.getSimpleContent();
        final Group parseGroup = getContentModel(parseCt);
        int count =
            (parseCc != null ? 1 : 0) +
            (parseSc != null ? 1 : 0) +
            (parseGroup != null ? 1 : 0);
        if (count > 1) {
            // KHK: s4s should catch this?
            state.error("A complex type must define either a content model, " +
                        "or a simpleContent or complexContent derivation: " +
                        "more than one found.",
                XmlErrorCodes.REDUNDANT_CONTENT_MODEL, parseCt);
            // recovery: treat it as the first of complexContent, simpleContent, model
            if (parseCc != null && parseSc != null) {
                parseSc = null;
            }
        }

        if (parseCc != null) {
            // KHK: s4s should catch this?
            if (parseCc.getExtension() != null && parseCc.getRestriction() != null) {
                state.error("Restriction conflicts with extension", XmlErrorCodes.REDUNDANT_CONTENT_MODEL, parseCc.getRestriction());
            }

            // Mixed can be specified in two places: the rules are that Cc wins over Ct if present
            // http://www.w3.org/TR/xmlschema-1/#c-mve
            boolean mixed = parseCc.isSetMixed() ? parseCc.getMixed() : parseCt.getMixed();

            if (parseCc.getExtension() != null) {
                resolveCcExtension(sImpl, parseCc.getExtension(), mixed);
            } else if (parseCc.getRestriction() != null) {
                resolveCcRestriction(sImpl, parseCc.getRestriction(), mixed);
            } else {
                // KHK: s4s should catch this?
                state.error("Missing restriction or extension", XmlErrorCodes.MISSING_RESTRICTION_OR_EXTENSION, parseCc);
                resolveErrorType(sImpl);
            }
        } else if (parseSc != null) {
            // KHK: s4s should catch this?
            if (parseSc.getExtension() != null && parseSc.getRestriction() != null) {
                state.error("Restriction conflicts with extension", XmlErrorCodes.REDUNDANT_CONTENT_MODEL, parseSc.getRestriction());
            }

            if (parseSc.getExtension() != null) {
                resolveScExtension(sImpl, parseSc.getExtension());
            } else if (parseSc.getRestriction() != null) {
                resolveScRestriction(sImpl, parseSc.getRestriction());
            } else {
                // KHK: s4s should catch this?
                state.error("Missing restriction or extension", XmlErrorCodes.MISSING_RESTRICTION_OR_EXTENSION, parseSc);
                resolveErrorType(sImpl);
            }
        } else {
            resolveBasicComplexType(sImpl);
        }
    }

    @SuppressWarnings("unused")
    static void resolveErrorType(SchemaTypeImpl sImpl) {
        throw new RuntimeException("This type of error recovery not yet implemented.");
    }

    private static SchemaType.Ref[] makeRefArray(Collection<SchemaType> typeList) {
        return typeList.stream().map(SchemaType::getRef).toArray(SchemaType.Ref[]::new);
    }


    static void resolveBasicComplexType(SchemaTypeImpl sImpl) {
        List<SchemaType> anonymousTypes = new ArrayList<>();
        ComplexType parseTree = (ComplexType) sImpl.getParseObject();
        String targetNamespace = sImpl.getTargetNamespace();
        boolean chameleon = (sImpl.getChameleonNamespace() != null);
        Group parseGroup = getContentModel(parseTree);

        if (sImpl.isRedefinition()) {
            StscState.get().error(XmlErrorCodes.SCHEMA_REDEFINE$EXTEND_OR_RESTRICT,
                new Object[]{"<complexType>"}, parseTree);
            // recovery: oh well.
        }

        int particleCode = translateParticleCode(parseGroup);

        // build content model and anonymous types
        SchemaParticle contentModel = translateContentModel(sImpl,
            parseGroup, targetNamespace, chameleon,
            sImpl.getElemFormDefault(), sImpl.getAttFormDefault(),
            particleCode, anonymousTypes, new LinkedHashMap<>(), false, null);

        // detect the nonempty "all" case (empty <all> doesn't count - it needs to be eliminated to match XSD test cases)
        boolean isAll = contentModel != null && contentModel.getParticleType() == SchemaParticle.ALL;

        // build attr model and anonymous types
        SchemaAttributeModelImpl attrModel = new SchemaAttributeModelImpl();
        translateAttributeModel(parseTree, targetNamespace, chameleon, sImpl.getAttFormDefault(),
            anonymousTypes, sImpl, null, attrModel, null, true, null);

        // summarize wildcard information
        WildcardResult wcElt = summarizeEltWildcards(contentModel);
        WildcardResult wcAttr = summarizeAttrWildcards(attrModel);

        // build state machine and verify that content model is deterministic
        if (contentModel != null) {
            buildStateMachine(contentModel);
            if (!StscState.get().noUpa() && !((SchemaParticleImpl) contentModel).isDeterministic()) {
                StscState.get().error(XmlErrorCodes.UNIQUE_PARTICLE_ATTRIBUTION, null, parseGroup);
            }
        }

        // build property model
        // emitDBG("Building content Model for " + sImpl);
        Map<QName, SchemaProperty> elementPropertyModel = buildContentPropertyModelByQName(contentModel, sImpl);

        // add attribute property model
        Map<QName, SchemaProperty> attributePropertyModel = buildAttributePropertyModelByQName(attrModel, sImpl);

        // figure out content type
        int complexVariety =
            parseTree.getMixed()
                ? SchemaType.MIXED_CONTENT
                : contentModel == null
                ? SchemaType.EMPTY_CONTENT
                : SchemaType.ELEMENT_CONTENT;

        // now fill in the actual schema type implementation
        sImpl.setBaseTypeRef(BuiltinSchemaTypeSystem.ST_ANY_TYPE.getRef());
        sImpl.setBaseDepth(BuiltinSchemaTypeSystem.ST_ANY_TYPE.getBaseDepth() + 1);
        sImpl.setDerivationType(SchemaType.DT_EXTENSION);
        sImpl.setComplexTypeVariety(complexVariety);
        sImpl.setContentModel(contentModel, attrModel, elementPropertyModel, attributePropertyModel, isAll);
        sImpl.setAnonymousTypeRefs(makeRefArray(anonymousTypes));
        sImpl.setWildcardSummary(wcElt.typedWildcards, wcElt.hasWildcards, wcAttr.typedWildcards, wcAttr.hasWildcards);
    }

    static void resolveCcRestriction(SchemaTypeImpl sImpl, ComplexRestrictionType parseTree, boolean mixed) {
        StscState state = StscState.get();
        String targetNamespace = sImpl.getTargetNamespace();
        boolean chameleon = (sImpl.getChameleonNamespace() != null);

        // BUGBUG: NOT YET REALLY IMPLEMENTED
        // throw new RuntimeException("Not yet implemented.");

        SchemaTypeImpl baseType;
        if (parseTree.getBase() == null) {
            // KHK: s4s
            state.error("A complexContent must define a base type", XmlErrorCodes.MISSING_BASE, parseTree);
            baseType = null; // recovery: no inheritance.
        } else {
            if (sImpl.isRedefinition()) {
                baseType = state.findRedefinedGlobalType(parseTree.getBase(), sImpl.getChameleonNamespace(), sImpl);
                if (baseType != null && !baseType.getName().equals(sImpl.getName())) {
                    state.error(XmlErrorCodes.SCHEMA_REDEFINE$SAME_TYPE,
                        new Object[]{"<complexType>",
                            QNameHelper.pretty(baseType.getName()),
                            QNameHelper.pretty(sImpl.getName())
                        },
                        parseTree);
                }
            } else {
                baseType = state.findGlobalType(parseTree.getBase(), sImpl.getChameleonNamespace(), targetNamespace);
            }

            if (baseType == null) {
                state.notFoundError(parseTree.getBase(), SchemaType.TYPE, parseTree.xgetBase(), true);
            }
        }

        if (baseType == null) {
            baseType = BuiltinSchemaTypeSystem.ST_ANY_TYPE;
        }

        if (baseType != null && baseType.finalRestriction()) {
            state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$FINAL,
                new Object[]{QNameHelper.pretty(baseType.getName()), QNameHelper.pretty(sImpl.getName())},
                parseTree.xgetBase());
            // recovery: just keep going
        }

        // Recursion
        if (baseType != null) {
            if (!StscResolver.resolveType(baseType)) {
                baseType = null; // circular dependency: no inheritance
            }
        }

        List<SchemaType> anonymousTypes = new ArrayList<>();
        Group parseEg = getContentModel(parseTree);

        // detect the "all" case
        int particleCode = translateParticleCode(parseEg);

        // build content model and anonymous types
        SchemaParticle contentModel = translateContentModel(sImpl,
            parseEg, targetNamespace, chameleon,
            sImpl.getElemFormDefault(), sImpl.getAttFormDefault(),
            particleCode, anonymousTypes, new LinkedHashMap<>(), false, null);

        // detect the nonempty "all" case (empty <all> doesn't count - it needs to be eliminated to match XSD test cases)
        boolean isAll = contentModel != null && contentModel.getParticleType() == SchemaParticle.ALL;

        // build attr model and anonymous types
        SchemaAttributeModelImpl attrModel;
        if (baseType == null) {
            attrModel = new SchemaAttributeModelImpl();
        } else {
            attrModel = new SchemaAttributeModelImpl(baseType.getAttributeModel());
        }
        translateAttributeModel(parseTree, targetNamespace, chameleon, sImpl.getAttFormDefault(),
            anonymousTypes, sImpl, null, attrModel, baseType, false, null);

        // summarize wildcard information
        WildcardResult wcElt = summarizeEltWildcards(contentModel);
        WildcardResult wcAttr = summarizeAttrWildcards(attrModel);

        // build state machine and verify that content model is deterministic
        if (contentModel != null) {
            buildStateMachine(contentModel);
            if (!StscState.get().noUpa() && !((SchemaParticleImpl) contentModel).isDeterministic()) {
                StscState.get().error(XmlErrorCodes.UNIQUE_PARTICLE_ATTRIBUTION, null, parseEg);
            }
        }

        // build property model
        // emitDBG("Building content Model for " + sImpl);
        Map<QName, SchemaProperty> elementPropertyModel = buildContentPropertyModelByQName(contentModel, sImpl);

        // add attribute property model
        Map<QName, SchemaProperty> attributePropertyModel = buildAttributePropertyModelByQName(attrModel, sImpl);

        // compute empty/element/mixed
        // fix for XMLBEANS-414
        int complexVariety = (mixed ? SchemaType.MIXED_CONTENT :
            (contentModel == null ? SchemaType.EMPTY_CONTENT : SchemaType.ELEMENT_CONTENT));

        // now fill in the actual schema type implementation
        if (baseType != null) {
            sImpl.setBaseTypeRef(baseType.getRef());
            sImpl.setBaseDepth(baseType.getBaseDepth() + 1);
        }
        sImpl.setDerivationType(SchemaType.DT_RESTRICTION);
        sImpl.setComplexTypeVariety(complexVariety);
        sImpl.setContentModel(contentModel, attrModel, elementPropertyModel, attributePropertyModel, isAll);
        sImpl.setAnonymousTypeRefs(makeRefArray(anonymousTypes));
        sImpl.setWildcardSummary(wcElt.typedWildcards, wcElt.hasWildcards, wcAttr.typedWildcards, wcAttr.hasWildcards);
    }

    static Map<QName, SchemaType> extractElementModel(SchemaType sType) {
        if (sType == null) {
            return new HashMap<>();
        }
        return Stream.of(sType.getProperties())
            .filter(p -> !p.isAttribute())
            .collect(Collectors.toMap(SchemaProperty::getName, SchemaProperty::getType));
    }

    static void resolveCcExtension(SchemaTypeImpl sImpl, ExtensionType parseTree, boolean mixed) {
        SchemaType baseType;
        StscState state = StscState.get();
        String targetNamespace = sImpl.getTargetNamespace();
        boolean chameleon = (sImpl.getChameleonNamespace() != null);

        if (parseTree.getBase() == null) {
            // KHK: s4s
            state.error("A complexContent must define a base type", XmlErrorCodes.MISSING_BASE, parseTree);
            baseType = null; // recovery: no inheritance.
        } else {
            if (sImpl.isRedefinition()) {
                baseType = state.findRedefinedGlobalType(parseTree.getBase(), sImpl.getChameleonNamespace(), sImpl);
                if (baseType != null && !baseType.getName().equals(sImpl.getName())) {
                    state.error(XmlErrorCodes.SCHEMA_REDEFINE$SAME_TYPE,
                        new Object[]{"<complexType>",
                            QNameHelper.pretty(baseType.getName()),
                            QNameHelper.pretty(sImpl.getName())
                        },
                        parseTree);
                }
            } else {
                baseType = state.findGlobalType(parseTree.getBase(), sImpl.getChameleonNamespace(), targetNamespace);
            }
            if (baseType == null) {
                state.notFoundError(parseTree.getBase(), SchemaType.TYPE, parseTree.xgetBase(), true);
            }
        }

        // Recursion
        if (baseType != null) {
            if (!StscResolver.resolveType((SchemaTypeImpl) baseType)) {
                baseType = null; // circular dependency: no inheritance
            }
        }

        if (baseType != null && baseType.isSimpleType()) {
            state.recover(XmlErrorCodes.SCHEMA_COMPLEX_TYPE$COMPLEX_CONTENT,
                new Object[]{QNameHelper.pretty(baseType.getName())},
                parseTree.xgetBase());
            baseType = null; // recovery: no inheritance.
        }

        if (baseType != null && baseType.finalExtension()) {
            state.error(XmlErrorCodes.COMPLEX_TYPE_EXTENSION$FINAL,
                new Object[]{QNameHelper.pretty(baseType.getName()), QNameHelper.pretty(sImpl.getName())},
                parseTree.xgetBase());
            // recovery: just keep going
        }

        // get base content model
        SchemaParticle baseContentModel = (baseType == null ? null : baseType.getContentModel());
        // TODO: attribute model also

        List<SchemaType> anonymousTypes = new ArrayList<>();
        Map<QName, SchemaType> baseElementModel = extractElementModel(baseType);
        Group parseEg = getContentModel(parseTree);

        if (baseType != null &&
            (baseType.getContentType() == SchemaType.SIMPLE_CONTENT)) {
            if (parseEg != null) {
                // if this type has complexContent, baseType is complexType
                // but with non-empty simpleContent then this type cannot
                // add extra elements
                state.recover(XmlErrorCodes.COMPLEX_TYPE_EXTENSION$EXTENDING_SIMPLE_CONTENT,
                    new Object[]{QNameHelper.pretty(baseType.getName())},
                    parseTree.xgetBase());
                baseType = null; // recovery: no inheritance.
            } else {
                // No extra elements, the type is a complex type with simple content
                resolveScExtensionPart2(sImpl, baseType, parseTree, targetNamespace, chameleon);
                return;
            }
        }

        // build extension model
        SchemaParticle extensionModel = translateContentModel(sImpl,
            parseEg, targetNamespace, chameleon,
            sImpl.getElemFormDefault(), sImpl.getAttFormDefault(),
            translateParticleCode(parseEg), anonymousTypes, baseElementModel, false, null);

        // apply rule #2 near http://www.w3.org/TR/xmlschema-1/#c-mve: empty ext model -> mixed taken from base
        if (extensionModel == null && !mixed) {
            mixed = (baseType != null && baseType.getContentType() == SchemaType.MIXED_CONTENT);
        }

        // apply Derivation Valid (Extension) rule 1.4.2.2
        if (baseType != null && (baseType.getContentType() != SchemaType.EMPTY_CONTENT) &&
            ((baseType.getContentType() == SchemaType.MIXED_CONTENT) != mixed)) {
            state.error(XmlErrorCodes.COMPLEX_TYPE_EXTENSION$BOTH_ELEMEMENT_OR_MIXED, null, parseTree.xgetBase());
            // recovery: just keep going
        }

        // detect the "all" base case
        if (baseType != null && baseType.hasAllContent() && extensionModel != null) {
            // KHK: which rule? cos-particle-extend.2 or cos-all-limited.1.2.  I think the limited one.
            state.error("Cannot extend a type with 'all' content model", XmlErrorCodes.CANNOT_EXTEND_ALL, parseTree.xgetBase());
            extensionModel = null; // recovery: drop extension
        }

        // build content model and anonymous types
        SchemaParticle contentModel = extendContentModel(baseContentModel, extensionModel, parseTree);

        // detect the nonempty "all" case (empty <all> doesn't count - it needs to be eliminated to match XSD test cases)
        boolean isAll = contentModel != null && contentModel.getParticleType() == SchemaParticle.ALL;

        // build attr model and anonymous types
        SchemaAttributeModelImpl attrModel;
        if (baseType == null) {
            attrModel = new SchemaAttributeModelImpl();
        } else {
            attrModel = new SchemaAttributeModelImpl(baseType.getAttributeModel());
        }
        translateAttributeModel(parseTree, targetNamespace, chameleon, sImpl.getAttFormDefault(),
            anonymousTypes, sImpl, null, attrModel, baseType, true, null);

        // summarize wildcard information
        WildcardResult wcElt = summarizeEltWildcards(contentModel);
        WildcardResult wcAttr = summarizeAttrWildcards(attrModel);

        // build state machine and verify that content model is deterministic
        if (contentModel != null) {
            buildStateMachine(contentModel);
            if (!StscState.get().noUpa() && !((SchemaParticleImpl) contentModel).isDeterministic()) {
                StscState.get().error(XmlErrorCodes.UNIQUE_PARTICLE_ATTRIBUTION, null, parseEg);
            }
        }

        // build property model
        // emitDBG("Building content Model for " + sImpl);
        Map<QName, SchemaProperty> elementPropertyModel = buildContentPropertyModelByQName(contentModel, sImpl);

        // add attribute property model
        Map<QName, SchemaProperty> attributePropertyModel = buildAttributePropertyModelByQName(attrModel, sImpl);

        // compute empty/element/mixed
        int complexVariety;
        if (contentModel == null && baseType != null &&
            baseType.getContentType() == SchemaType.SIMPLE_CONTENT) {
            complexVariety = SchemaType.SIMPLE_CONTENT;
            sImpl.setContentBasedOnTypeRef(baseType.getContentBasedOnType().getRef());
        } else {
            complexVariety = (mixed ? SchemaType.MIXED_CONTENT :
                (contentModel == null ? SchemaType.EMPTY_CONTENT : SchemaType.ELEMENT_CONTENT));
        }

        // now fill in the actual schema type implementation
        if (baseType == null) {
            baseType = XmlObject.type;
        }
        sImpl.setBaseTypeRef(baseType.getRef());
        sImpl.setBaseDepth(((SchemaTypeImpl) baseType).getBaseDepth() + 1);
        sImpl.setDerivationType(SchemaType.DT_EXTENSION);
        sImpl.setComplexTypeVariety(complexVariety);
        sImpl.setContentModel(contentModel, attrModel, elementPropertyModel, attributePropertyModel, isAll);
        sImpl.setAnonymousTypeRefs(makeRefArray(anonymousTypes));
        sImpl.setWildcardSummary(wcElt.typedWildcards, wcElt.hasWildcards, wcAttr.typedWildcards, wcAttr.hasWildcards);
    }

    static void resolveScRestriction(SchemaTypeImpl sImpl, SimpleRestrictionType parseTree) {
        SchemaTypeImpl baseType;
        SchemaTypeImpl contentType = null;
        StscState state = StscState.get();
        String targetNamespace = sImpl.getTargetNamespace();
        boolean chameleon = (sImpl.getChameleonNamespace() != null);
        List<SchemaType> anonymousTypes = new ArrayList<>();
        if (parseTree.getSimpleType() != null) {
            LocalSimpleType typedef = parseTree.getSimpleType();
            contentType = StscTranslator.
                translateAnonymousSimpleType(typedef, targetNamespace, chameleon,
                    sImpl.getElemFormDefault(), sImpl.getAttFormDefault(),
                    anonymousTypes, sImpl);
        }
        if (parseTree.getBase() == null) {
            state.error("A simpleContent restriction must define a base type", XmlErrorCodes.MISSING_BASE, parseTree);
            // recovery: extends ANY_SIMPLE type
            baseType = BuiltinSchemaTypeSystem.ST_ANY_SIMPLE;
        } else {
            if (sImpl.isRedefinition()) {
                baseType = state.findRedefinedGlobalType(parseTree.getBase(), sImpl.getChameleonNamespace(), sImpl);
                if (baseType != null && !baseType.getName().equals(sImpl.getName())) {
                    state.error(XmlErrorCodes.SCHEMA_REDEFINE$SAME_TYPE,
                        new Object[]{"<simpleType>",
                            QNameHelper.pretty(baseType.getName()),
                            QNameHelper.pretty(sImpl.getName())
                        },
                        parseTree);
                }
            } else {
                baseType = state.findGlobalType(parseTree.getBase(), sImpl.getChameleonNamespace(), targetNamespace);
            }
        }
        if (baseType == null) {
            state.notFoundError(parseTree.getBase(), SchemaType.TYPE, parseTree.xgetBase(), true);
            // recovery: extends ANY_SIMPLE type
            baseType = BuiltinSchemaTypeSystem.ST_ANY_SIMPLE;
        }

        // Recursion
        StscResolver.resolveType(baseType);
        if (contentType != null) {
            StscResolver.resolveType(contentType);
        } else {
            contentType = baseType;
        }

        if (baseType.isSimpleType()) {
            // src-ct.2: complex types with simple content cannot restrict simple types
            state.recover(XmlErrorCodes.COMPLEX_TYPE_PROPERTIES$SIMPLE_TYPE_EXTENSION,
                new Object[]{QNameHelper.pretty(baseType.getName())},
                parseTree);
            // recovery: extends ANY_SIMPLE type
            baseType = BuiltinSchemaTypeSystem.ST_ANY_SIMPLE;
        }

        if (baseType != null && baseType.finalRestriction()) {
            state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$FINAL,
                new Object[]{QNameHelper.pretty(baseType.getName()), QNameHelper.pretty(sImpl.getName())},
                parseTree.xgetBase());
            // recovery: just keep going
        }

        // build attr model and anonymous types
        SchemaAttributeModelImpl attrModel;
        if (baseType == null) {
            attrModel = new SchemaAttributeModelImpl();
        } else {
            attrModel = new SchemaAttributeModelImpl(baseType.getAttributeModel());
        }
        translateAttributeModel(parseTree, targetNamespace, chameleon, sImpl.getAttFormDefault(),
            anonymousTypes, sImpl, null, attrModel, baseType, false, null);

        // summarize wildcard information
        WildcardResult wcAttr = summarizeAttrWildcards(attrModel);

        // add attribute property model
        Map<QName, SchemaProperty> attributePropertyModel = buildAttributePropertyModelByQName(attrModel, sImpl);

        // now fill in the actual schema type implementation
        if (baseType != null) {
            sImpl.setBaseTypeRef(baseType.getRef());
            sImpl.setBaseDepth(baseType.getBaseDepth() + 1);
        }
        sImpl.setContentBasedOnTypeRef(contentType.getRef());
        sImpl.setDerivationType(SchemaType.DT_RESTRICTION);
        sImpl.setAnonymousTypeRefs(makeRefArray(anonymousTypes));
        sImpl.setWildcardSummary(QNameSet.EMPTY, false, wcAttr.typedWildcards, wcAttr.hasWildcards);
        sImpl.setComplexTypeVariety(SchemaType.SIMPLE_CONTENT);
        sImpl.setContentModel(null, attrModel, null, attributePropertyModel, false);
        sImpl.setSimpleTypeVariety(contentType.getSimpleVariety());
        sImpl.setPrimitiveTypeRef(contentType.getPrimitiveType() == null ? null : contentType.getPrimitiveType().getRef());
        switch (sImpl.getSimpleVariety()) {
            case SchemaType.LIST: {
                SchemaType lit = contentType.getListItemType();
                if (lit != null) {
                    sImpl.setListItemTypeRef(lit.getRef());
                }
                break;
            }

            case SchemaType.UNION:
                sImpl.setUnionMemberTypeRefs(makeRefArray(Arrays.asList(contentType.getUnionMemberTypes())));
                break;
        }

        // deal with facets
        StscSimpleTypeResolver.resolveFacets(sImpl, parseTree, contentType);

        // now compute our intrinsic properties
        StscSimpleTypeResolver.resolveFundamentalFacets(sImpl);
    }

    static void resolveScExtension(SchemaTypeImpl sImpl, SimpleExtensionType parseTree) {
        SchemaTypeImpl baseType;
        StscState state = StscState.get();
        String targetNamespace = sImpl.getTargetNamespace();
        boolean chameleon = (sImpl.getChameleonNamespace() != null);
        if (parseTree.getBase() == null) {
            state.error("A simpleContent extension must define a base type", XmlErrorCodes.MISSING_BASE, parseTree);
            // recovery: extends ANY_SIMPLE type
            baseType = BuiltinSchemaTypeSystem.ST_ANY_SIMPLE;
        } else {
            if (sImpl.isRedefinition()) {
                baseType = state.findRedefinedGlobalType(parseTree.getBase(), sImpl.getChameleonNamespace(), sImpl);
                if (baseType != null && !baseType.getName().equals(sImpl.getName())) {
                    state.error(XmlErrorCodes.SCHEMA_REDEFINE$SAME_TYPE,
                        new Object[]{"<simpleType>",
                            QNameHelper.pretty(baseType.getName()),
                            QNameHelper.pretty(sImpl.getName())
                        },
                        parseTree);
                }
            } else {
                baseType = state.findGlobalType(parseTree.getBase(), sImpl.getChameleonNamespace(), targetNamespace);
            }
            if (baseType == null) {
                state.notFoundError(parseTree.getBase(), SchemaType.TYPE, parseTree.xgetBase(), true);
                // recovery: extends ANY_SIMPLE type
                baseType = BuiltinSchemaTypeSystem.ST_ANY_SIMPLE;
            }
        }

        // Recursion
        StscResolver.resolveType(baseType);

        if (!baseType.isSimpleType() && baseType.getContentType() != SchemaType.SIMPLE_CONTENT) {
            // src-ct.2: complex types with simple content can only extend simple types
            state.error(XmlErrorCodes.SCHEMA_COMPLEX_TYPE$SIMPLE_CONTENT,
                new Object[]{QNameHelper.pretty(baseType.getName())},
                parseTree);
            // recovery: extends ANY_SIMPLE type
            baseType = BuiltinSchemaTypeSystem.ST_ANY_SIMPLE;
        }

        if (baseType != null && baseType.finalExtension()) {
            state.error(XmlErrorCodes.COMPLEX_TYPE_EXTENSION$FINAL,
                new Object[]{QNameHelper.pretty(baseType.getName()), QNameHelper.pretty(sImpl.getName())},
                parseTree.xgetBase());
            // recovery: just keep going
        }

        resolveScExtensionPart2(sImpl, baseType, parseTree, targetNamespace, chameleon);
    }

    static void resolveScExtensionPart2(SchemaTypeImpl sImpl, SchemaType baseType, ExtensionType parseTree,
                                        String targetNamespace, boolean chameleon) {
        // build attr model and anonymous types
        List<SchemaType> anonymousTypes = new ArrayList<>();
        SchemaAttributeModelImpl attrModel;
        attrModel = new SchemaAttributeModelImpl(baseType.getAttributeModel());
        translateAttributeModel(parseTree, targetNamespace, chameleon, sImpl.getAttFormDefault(), anonymousTypes, sImpl, null, attrModel, baseType, true, null);

        // summarize wildcard information
        WildcardResult wcAttr = summarizeAttrWildcards(attrModel);

        // add attribute property model
        Map<QName, SchemaProperty> attributePropertyModel = buildAttributePropertyModelByQName(attrModel, sImpl);

        // now fill in the actual schema type implementation
        sImpl.setBaseTypeRef(baseType.getRef());
        sImpl.setBaseDepth(((SchemaTypeImpl) baseType).getBaseDepth() + 1);
        sImpl.setContentBasedOnTypeRef(baseType.getRef());
        sImpl.setDerivationType(SchemaType.DT_EXTENSION);
        sImpl.setAnonymousTypeRefs(makeRefArray(anonymousTypes));
        sImpl.setWildcardSummary(QNameSet.EMPTY, false, wcAttr.typedWildcards, wcAttr.hasWildcards);
        sImpl.setComplexTypeVariety(SchemaType.SIMPLE_CONTENT);
        sImpl.setContentModel(null, attrModel, null, attributePropertyModel, false);
        sImpl.setSimpleTypeVariety(baseType.getSimpleVariety());
        sImpl.setPrimitiveTypeRef(baseType.getPrimitiveType() == null ? null : baseType.getPrimitiveType().getRef());
        switch (sImpl.getSimpleVariety()) {
            case SchemaType.LIST:
                sImpl.setListItemTypeRef(baseType.getListItemType().getRef());
                break;

            case SchemaType.UNION:
                sImpl.setUnionMemberTypeRefs(makeRefArray(Arrays.asList(baseType.getUnionMemberTypes())));
                break;
        }

        // deal with facets
        StscSimpleTypeResolver.resolveFacets(sImpl, null, (SchemaTypeImpl) baseType);

        // now compute our intrinsic properties
        StscSimpleTypeResolver.resolveFundamentalFacets(sImpl);
    }

    static class WildcardResult {
        WildcardResult(QNameSet typedWildcards, boolean hasWildcards) {
            this.typedWildcards = typedWildcards;
            this.hasWildcards = hasWildcards;
        }

        QNameSet typedWildcards;
        boolean hasWildcards;
    }

    static WildcardResult summarizeAttrWildcards(SchemaAttributeModel attrModel) {
        if (attrModel.getWildcardProcess() == SchemaAttributeModel.NONE) {
            return new WildcardResult(QNameSet.EMPTY, false);
        }
        if (attrModel.getWildcardProcess() == SchemaAttributeModel.SKIP) {
            return new WildcardResult(QNameSet.EMPTY, true);
        }
        return new WildcardResult(attrModel.getWildcardSet(), true);
    }

    static WildcardResult summarizeEltWildcards(SchemaParticle contentModel) {
        if (contentModel == null) {
            return new WildcardResult(QNameSet.EMPTY, false);
        }

        switch (contentModel.getParticleType()) {
            case SchemaParticle.ALL:
            case SchemaParticle.SEQUENCE:
            case SchemaParticle.CHOICE:
                QNameSetBuilder set = new QNameSetBuilder();
                boolean hasWildcards = false;
                for (int i = 0; i < contentModel.countOfParticleChild(); i++) {
                    WildcardResult inner = summarizeEltWildcards(contentModel.getParticleChild(i));
                    set.addAll(inner.typedWildcards);
                    hasWildcards |= inner.hasWildcards;
                }
                return new WildcardResult(set.toQNameSet(), hasWildcards);
            case SchemaParticle.WILDCARD:
                return new WildcardResult(
                    (contentModel.getWildcardProcess() == SchemaParticle.SKIP) ?
                        QNameSet.EMPTY : contentModel.getWildcardSet(), true);
            // otherwise fallthrough

            default:
                return new WildcardResult(QNameSet.EMPTY, false);
        }
    }

    static void translateAttributeModel(XmlObject parseTree,
                                        String targetNamespace, boolean chameleon, String formDefault,
                                        List<SchemaType> anonymousTypes, SchemaType outerType,
                                        Set<QName> seenAttributes, SchemaAttributeModelImpl result,
                                        SchemaType baseType, boolean extension,
                                        SchemaAttributeGroupImpl redefinitionFor) {
        StscState state = StscState.get();
        if (seenAttributes == null) {
            seenAttributes = new HashSet<>();
        }
        boolean seenWildcard = false;
        boolean seenRedefinition = false;
        SchemaAttributeModel baseModel = null;
        if (baseType != null) {
            baseModel = baseType.getAttributeModel();
        }

        try (XmlCursor cur = parseTree.newCursor()) {
            for (boolean more = cur.toFirstChild(); more; more = cur.toNextSibling()) {
                switch (translateAttributeCode(cur.getName())) {
                    case ATTRIBUTE_CODE: {
                        Attribute xsdattr = (Attribute) cur.getObject();

                        SchemaLocalAttribute sAttr = StscTranslator.translateAttribute(xsdattr, targetNamespace, formDefault, chameleon, anonymousTypes, outerType, baseModel, true);
                        if (sAttr == null) {
                            continue;
                        }

                        if (seenAttributes.contains(sAttr.getName())) {
                            state.error(XmlErrorCodes.COMPLEX_TYPE_PROPERTIES$DUPLICATE_ATTRIBUTE,
                                new Object[]{QNameHelper.pretty(sAttr.getName()), QNameHelper.pretty(outerType.getName())},
                                xsdattr.xgetName());
                            continue; // ignore the duplicate attr
                        }

                        seenAttributes.add(sAttr.getName());

                        if (baseModel != null) {
                            SchemaLocalAttribute baseAttr = baseModel.getAttribute(sAttr.getName());
                            if (baseAttr == null) {
                                if (!extension) {
                                    if (!baseModel.getWildcardSet().contains(sAttr.getName())) {
                                        state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$ATTR_IN_BASE_WILDCARD_SET,
                                            new Object[]{QNameHelper.pretty(sAttr.getName()), QNameHelper.pretty(outerType.getName())}, xsdattr);
                                    }
                                }
                            } else {
                                if (extension) {
                                    // KHK: cos-ct-extends.1.2?
                                    if (sAttr.getUse() == SchemaLocalAttribute.PROHIBITED) {
                                        state.error("An extension cannot prohibit an attribute from the base type; use restriction instead.", XmlErrorCodes.DUPLICATE_ATTRIBUTE_NAME, xsdattr.xgetUse());
                                    }
                                } else {
                                    if (sAttr.getUse() != SchemaLocalAttribute.REQUIRED) {
                                        if (baseAttr.getUse() == SchemaLocalAttribute.REQUIRED) {
                                            state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$ATTR_REQUIRED,
                                                new Object[]{QNameHelper.pretty(sAttr.getName()), QNameHelper.pretty(outerType.getName())}, xsdattr);
                                        }

                                        if (sAttr.getUse() == SchemaLocalAttribute.PROHIBITED) {
                                            result.removeProhibitedAttribute(sAttr.getName());
                                        }
                                    }
                                }
                            }
                        }

                        if (sAttr.getUse() != SchemaLocalAttribute.PROHIBITED) {
                            result.addAttribute(sAttr);
                        } else {
                            // attribute is prohibited. If it has an anonymous type remove
                            // it from the list (this will prevent inclusion of any anonymous
                            // types defined within the prohibited attribute which would
                            // otherwise attempt to refer to the prohibited attribute at
                            // save() time)
                            SchemaType attrType = sAttr.getType();
                            if (anonymousTypes != null) {
                                anonymousTypes.remove(attrType);
                            }
                        }

                        if (sAttr.getDefaultText() != null && !sAttr.isFixed()) {
                            if (sAttr.getUse() != SchemaLocalAttribute.OPTIONAL) {
                                state.error(XmlErrorCodes.SCHEMA_ATTR$DEFAULT_AND_USE_OPTIONAL,
                                    new Object[]{QNameHelper.pretty(sAttr.getName())}, xsdattr);
                            }
                        }


                        break;
                    }
                    case ANY_ATTRIBUTE_CODE: {
                        Wildcard xsdwc = (Wildcard) cur.getObject();
                        if (seenWildcard) {
                            // KHK: ?
                            state.error("Only one attribute wildcard allowed", XmlErrorCodes.DUPLICATE_ANY_ATTRIBUTE, xsdwc);
                            continue; // ignore the extra wildcard
                        }
                        seenWildcard = true;
                        NamespaceList nsList = xsdwc.xgetNamespace();
                        String nsText;
                        if (nsList == null) {
                            nsText = "##any";
                        } else {
                            nsText = nsList.getStringValue();
                        }
                        QNameSet wcset = QNameSet.forWildcardNamespaceString(nsText, targetNamespace);

                        if (baseModel != null && !extension) {
                            if (baseModel.getWildcardSet() == null) {
                                state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$BASE_HAS_ATTR_WILDCARD, null, xsdwc);
                                continue; // ignore the extra wildcard
                            } else if (!baseModel.getWildcardSet().containsAll(wcset)) {
                                state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$ATTR_WILDCARD_SUBSET,
                                    new Object[]{nsText}, xsdwc);
                                continue; // ignore the restriction
                            }
                        }

                        int wcprocess = translateWildcardProcess(xsdwc.xgetProcessContents());
                        if (result.getWildcardProcess() == SchemaAttributeModel.NONE) {
                            result.setWildcardSet(wcset);
                            result.setWildcardProcess(wcprocess);
                        } else {
                            if (extension) {
                                result.setWildcardSet(wcset.union(result.getWildcardSet()));
                                result.setWildcardProcess(wcprocess);
                            } else {
                                result.setWildcardSet(wcset.intersect(result.getWildcardSet()));
                                // keep old process
                            }
                        }
                        break;
                    }
                    case ATTRIBUTE_GROUP_CODE: {
                        AttributeGroupRef xsdag = (AttributeGroupRef) cur.getObject();
                        QName ref = xsdag.getRef();
                        if (ref == null) {
                            // KHK: s4s
                            state.error("Attribute group reference must have a ref attribute", XmlErrorCodes.ATTRIBUTE_GROUP_MISSING_REF, xsdag);
                            continue;
                        }
                        SchemaAttributeGroupImpl group;
                        if (redefinitionFor != null) {
                            group = state.findRedefinedAttributeGroup(ref, chameleon ? targetNamespace : null, redefinitionFor);
                            if (group != null &&
                                redefinitionFor.getName().equals(group.getName())) {
                                if (seenRedefinition) {
                                    state.error(XmlErrorCodes.SCHEMA_REDEFINE$ATTR_GROUP_SELF_REF,
                                        new Object[]{QNameHelper.pretty(redefinitionFor.getName())}, xsdag);
                                }
                                seenRedefinition = true;
                            }
                        } else {
                            group = state.findAttributeGroup(ref, chameleon ? targetNamespace : null, targetNamespace);
                        }
                        if (group == null) {
                            state.notFoundError(ref, SchemaType.ATTRIBUTE_GROUP, xsdag.xgetRef(), true);
                            continue;
                        }
                        if (state.isProcessing(group)) {
                            state.error(XmlErrorCodes.SCHEMA_ATTR_GROUP$SELF_REF,
                                new Object[]{QNameHelper.pretty(group.getName())}, group.getParseObject());
                            continue;
                        }
                        String subTargetNamespace = targetNamespace;
                        if (group.getTargetNamespace() != null) {
                            subTargetNamespace = group.getTargetNamespace();
                            chameleon = group.getChameleonNamespace() != null;
                        }

                        state.startProcessing(group);
                        SchemaAttributeGroupImpl nestedRedefinitionFor = null;
                        if (group.isRedefinition()) {
                            nestedRedefinitionFor = group;
                        }
                        translateAttributeModel(group.getParseObject(), subTargetNamespace, chameleon,
                            group.getFormDefault(),
                            anonymousTypes, outerType, seenAttributes, result, baseType,
                            extension, nestedRedefinitionFor);
                        state.finishProcessing(group);
                        break;
                    }
                    default:
                        // skip things that are not part of the attribute model.
                        break;
                }
            }
        }
        // If this is restriction and no wildcard was present, then
        // we have to erase the inherited wildcards
        if (!extension && !seenWildcard) {
            result.setWildcardSet(null);
            result.setWildcardProcess(SchemaAttributeModel.NONE);
        }
    }

    static SchemaParticle extendContentModel(SchemaParticle baseContentModel, SchemaParticle extendedContentModel, XmlObject parseTree) {
        // http://www.w3.org/TR/xmlschema-1/#element-complexContent::extension

        // 2.1 If the explicit content is empty, then the {content type} of the type definition resolved to by the actual value of the base [attribute]
        if (extendedContentModel == null) {
            return baseContentModel;
        }

        // 2.2 If the type definition resolved to by the actual value of the base [attribute] has a {content type} of empty, then a pair of mixed or elementOnly (determined as per clause 1.2.1 above) and the explicit content itself;
        if (baseContentModel == null) {
            return extendedContentModel;
        }

        // 2.3 otherwise a pair of mixed or elementOnly (determined as per clause 1.2.1 above) and a particle whose properties are as follows:
        SchemaParticleImpl sPart = new SchemaParticleImpl();
        sPart.setParticleType(SchemaParticle.SEQUENCE);

        List<SchemaParticle> accumulate = new ArrayList<>();
        addMinusPointlessParticles(accumulate, baseContentModel, SchemaParticle.SEQUENCE);
        addMinusPointlessParticles(accumulate, extendedContentModel, SchemaParticle.SEQUENCE);
        sPart.setMinOccurs(BigInteger.ONE);
        sPart.setMaxOccurs(BigInteger.ONE);
        sPart.setParticleChildren(accumulate.toArray(new SchemaParticle[0]));

        return filterPointlessParticlesAndVerifyAllParticles(sPart, parseTree);
    }

    static BigInteger extractMinOccurs(XmlNonNegativeInteger nni) {
        if (nni == null) {
            return BigInteger.ONE;
        }
        BigInteger result = nni.getBigIntegerValue();
        if (result == null) {
            return BigInteger.ONE;
        }
        return result;
    }

    static BigInteger extractMaxOccurs(AllNNI allNNI) {
        if (allNNI == null) {
            return BigInteger.ONE;
        }

        if (allNNI.instanceType().getPrimitiveType().getBuiltinTypeCode() == SchemaType.BTC_DECIMAL) {
            return ((XmlInteger) allNNI).getBigIntegerValue();
        } else {
            return null;
        }
    }

    private static class RedefinitionForGroup {
        private final SchemaModelGroupImpl group;
        private boolean seenRedefinition = false;

        public RedefinitionForGroup(SchemaModelGroupImpl group) {
            this.group = group;
        }

        public SchemaModelGroupImpl getGroup() {
            return group;
        }

        public boolean isSeenRedefinition() {
            return seenRedefinition;
        }

        public void setSeenRedefinition(boolean seenRedefinition) {
            this.seenRedefinition = seenRedefinition;
        }
    }

    static SchemaParticle translateContentModel(
        SchemaType outerType,
        XmlObject parseTree, String targetNamespace, boolean chameleon,
        String elemFormDefault, String attFormDefault,
        int particleCode, List<SchemaType> anonymousTypes, Map<QName, SchemaType> elementModel,
        boolean allowElt, RedefinitionForGroup redefinitionFor) {
        if (parseTree == null || particleCode == 0) {
            return null;
        }

        StscState state = StscState.get();

        // emitDBG("Translating content model for " + outerType);
        // indentDBG();

        boolean hasChildren = false;
        BigInteger minOccurs;
        BigInteger maxOccurs;
        SchemaModelGroupImpl group = null;

        SchemaParticleImpl sPart;

        if (particleCode == SchemaParticle.ELEMENT) {
            if (!allowElt) {
                state.error("Must be a sequence, choice or all here", XmlErrorCodes.EXPLICIT_GROUP_NEEDED, parseTree);
            }

            // TODO: detect substitution group for this element and construct a choice

            LocalElement parseElt = (LocalElement) parseTree;
            sPart = StscTranslator.translateElement(parseElt, targetNamespace, chameleon,
                elemFormDefault, attFormDefault, anonymousTypes, outerType);
            if (sPart == null) {
                return null;
            }
            minOccurs = extractMinOccurs(parseElt.xgetMinOccurs());
            maxOccurs = extractMaxOccurs(parseElt.xgetMaxOccurs());

            SchemaType oldType = elementModel.get(sPart.getName());
            if (oldType == null) {
                elementModel.put(sPart.getName(), sPart.getType());
            } else if (!sPart.getType().equals(oldType)) {
                state.error(XmlErrorCodes.ELEM_CONSISTANT, new Object[]{QNameHelper.pretty(sPart.getName())}, parseTree);
                return null;
            }
        } else if (particleCode == SchemaParticle.WILDCARD) {
            if (!allowElt) {
                state.error("Must be a sequence, choice or all here", XmlErrorCodes.EXPLICIT_GROUP_NEEDED, parseTree);
            }
            Any parseAny = (Any) parseTree;
            sPart = new SchemaParticleImpl();
            sPart.setParticleType(SchemaParticle.WILDCARD);
            QNameSet wcset;
            NamespaceList nslist = parseAny.xgetNamespace();
            if (nslist == null) {
                wcset = QNameSet.ALL;
            } else {
                wcset = QNameSet.forWildcardNamespaceString(nslist.getStringValue(), targetNamespace);
            }
            sPart.setWildcardSet(wcset);
            sPart.setWildcardProcess(translateWildcardProcess(parseAny.xgetProcessContents()));
            minOccurs = extractMinOccurs(parseAny.xgetMinOccurs());
            maxOccurs = extractMaxOccurs(parseAny.xgetMaxOccurs());
        } else {
            final Group parseGroup = (Group) parseTree;
            sPart = new SchemaParticleImpl();

            // grab min/maxOccurs before dereferencign group ref
            minOccurs = extractMinOccurs(parseGroup.xgetMinOccurs());
            maxOccurs = extractMaxOccurs(parseGroup.xgetMaxOccurs());

            if (particleCode == MODEL_GROUP_CODE) {
                QName ref = parseGroup.getRef();
                if (ref == null) {
                    // KHK: s4s
                    state.error("Group reference must have a ref attribute", XmlErrorCodes.GROUP_MISSING_REF, parseTree);
                    return null;
                }

                if (redefinitionFor != null) {
                    group = state.findRedefinedModelGroup(ref, chameleon ? targetNamespace : null, redefinitionFor.getGroup());
                    if (group != null && group.getName().equals(redefinitionFor.getGroup().getName())) {
                        if (redefinitionFor.isSeenRedefinition()) {
                            state.error(XmlErrorCodes.SCHEMA_REDEFINE$GROUP_SELF_REF,
                                new Object[]{QNameHelper.pretty(group.getName())}, parseTree);
                        }
                        if (!BigInteger.ONE.equals(maxOccurs) || !BigInteger.ONE.equals(minOccurs)) {
                            state.error(XmlErrorCodes.SCHEMA_REDEFINE$GROUP_SELF_REF_MIN_MAX_1,
                                new Object[]{QNameHelper.pretty(group.getName())}, parseTree);
                        }
                        redefinitionFor.setSeenRedefinition(true);
                    }
                } else {
                    group = state.findModelGroup(ref, chameleon ? targetNamespace : null, targetNamespace);
                }
                if (group == null) {
                    state.notFoundError(ref, SchemaType.MODEL_GROUP, ((Group) parseTree).xgetRef(), true);
                    return null;
                }
                if (state.isProcessing(group)) {
                    state.error(XmlErrorCodes.MODEL_GROUP_PROPERTIES$CIRCULAR,
                        new Object[]{QNameHelper.pretty(group.getName())}, group.getParseObject());
                    return null;
                }

                // no go to the child.
                try (XmlCursor cur = group.getParseObject().newCursor()) {
                    for (boolean more = cur.toFirstChild(); more; more = cur.toNextSibling()) {
                        particleCode = translateParticleCode(cur.getName());
                        if (particleCode != 0) {
                            parseTree = cur.getObject();
                            break;
                        }
                    }
                }
                if (particleCode == 0) {
                    // KHK: s4s
                    state.error("Model group " + QNameHelper.pretty(group.getName()) + " is empty", XmlErrorCodes.EXPLICIT_GROUP_NEEDED, group.getParseObject());
                    return null;
                }
                if (particleCode != SchemaParticle.ALL && particleCode != SchemaParticle.SEQUENCE && particleCode != SchemaParticle.CHOICE) {
                    // KHK: s4s
                    state.error("Model group " + QNameHelper.pretty(group.getName()) + " is not a sequence, all, or choice", XmlErrorCodes.EXPLICIT_GROUP_NEEDED, group.getParseObject());
                }

                String newTargetNamespace = group.getTargetNamespace();
                if (newTargetNamespace != null) {
                    targetNamespace = newTargetNamespace;
                }
                elemFormDefault = group.getElemFormDefault();
                attFormDefault = group.getAttFormDefault();
                chameleon = group.getChameleonNamespace() != null;
            }

            switch (particleCode) {
                case SchemaParticle.ALL:
                case SchemaParticle.SEQUENCE:
                case SchemaParticle.CHOICE:
                    sPart.setParticleType(particleCode);
                    hasChildren = true;
                    break;

                default:
                    assert (false);
                    throw new IllegalStateException();
            }
        }

        if (maxOccurs != null && minOccurs.compareTo(maxOccurs) > 0) {
            state.error(XmlErrorCodes.PARTICLE_PROPERTIES$MIN_LTE_MAX, null, parseTree);
            maxOccurs = minOccurs; // remedy: pin max up to min
        }

        if (maxOccurs != null && maxOccurs.compareTo(BigInteger.ONE) < 0) {
            state.warning(XmlErrorCodes.PARTICLE_PROPERTIES$MAX_GTE_1, null, parseTree);

            // remove from the list of anonymous types if it was added
            anonymousTypes.remove(sPart.getType());
            return null; // maxOccurs == minOccurs == 0, same as no particle at all.
        }

        sPart.setMinOccurs(minOccurs);
        sPart.setMaxOccurs(maxOccurs);

        if (group != null) {
            state.startProcessing(group);
            redefinitionFor = null;
            if (group.isRedefinition()) {
                redefinitionFor = new RedefinitionForGroup(group);
            }
        }

        if (hasChildren) {
            List<SchemaParticle> accumulate = new ArrayList<>();
            try (XmlCursor cur = parseTree.newCursor()) {
                for (boolean more = cur.toFirstChild(); more; more = cur.toNextSibling()) {
                    int code = translateParticleCode(cur.getName());
                    if (code == 0) {
                        continue;
                    }
                    addMinusPointlessParticles(accumulate,
                        translateContentModel(outerType,
                            cur.getObject(), targetNamespace, chameleon,
                            elemFormDefault, attFormDefault, code,
                            anonymousTypes, elementModel, true, redefinitionFor),
                        sPart.getParticleType());
                }
            }
            sPart.setParticleChildren(accumulate.toArray(new SchemaParticle[0]));
        }


        SchemaParticle result = filterPointlessParticlesAndVerifyAllParticles(sPart, parseTree);

        if (group != null) {
            state.finishProcessing(group);
        }
        // outdentDBG();
        return result;
    }

    static int translateWildcardProcess(Any.ProcessContents process) {
        if (process == null) {
            return SchemaParticle.STRICT;
        }

        String processValue = process.getStringValue();

        if ("lax".equals(processValue)) {
            return SchemaParticle.LAX;
        }

        if ("skip".equals(processValue)) {
            return SchemaParticle.SKIP;
        }

        return SchemaParticle.STRICT;
    }

    static SchemaParticle filterPointlessParticlesAndVerifyAllParticles(SchemaParticle part, XmlObject parseTree) {
        if (part.getMaxOccurs() != null && part.getMaxOccurs().signum() == 0) {
            return null;
        }

        switch (part.getParticleType()) {
            case SchemaParticle.SEQUENCE:
            case SchemaParticle.ALL:
                if (part.getParticleChildren().length == 0) {
                    return null;
                }
                if (part.isSingleton() && part.countOfParticleChild() == 1) {
                    return part.getParticleChild(0);
                }
                break;

            case SchemaParticle.CHOICE:
                if (part.getParticleChildren().length == 0 &&
                    part.getMinOccurs().compareTo(BigInteger.ZERO) == 0) {
                    return null;
                }
                if (part.isSingleton() && part.countOfParticleChild() == 1) {
                    return part.getParticleChild(0);
                }
                break;

            case SchemaParticle.ELEMENT:
            case SchemaParticle.WILDCARD:
                return part;

            default:
                assert (false);
                throw new IllegalStateException();
        }

        boolean isAll = part.getParticleType() == SchemaParticle.ALL;

        if (isAll) {
            // http://www.w3.org/TR/xmlschema-1/#cos-all-limited
            if (part.getMaxOccurs() == null || part.getMaxOccurs().compareTo(BigInteger.ONE) > 0) {
                // An all group must have maxOccurs <= 1
                // KHK: review
                StscState.get().error(XmlErrorCodes.ALL_GROUP_LIMITED$IN_MIN_MAX_1_PARTICLE, null, parseTree);
            }
        }

        for (int i = 0; i < part.countOfParticleChild(); i++) {
            SchemaParticle child = part.getParticleChild(i);
            if (child.getParticleType() == SchemaParticle.ALL) {
                // An all group is only allowed at the top level of the content model
                // KHK: review
                StscState.get().error(XmlErrorCodes.ALL_GROUP_LIMITED$IN_COMPLEX_TYPE_DEF_PARTICLE, null, parseTree);
            } else if (isAll && (child.getParticleType() != SchemaParticle.ELEMENT || child.getMaxOccurs() == null || child.getMaxOccurs().compareTo(BigInteger.ONE) > 0)) {
                // An all group can contain only element particles with maxOccurs <= 1
                // KHK: review
                StscState.get().error(XmlErrorCodes.ALL_GROUP_LIMITED$CHILD_PARTICLES_MAX_LTE_1, null, parseTree);
            }
        }

        return part;
    }

    static void addMinusPointlessParticles(
        List<SchemaParticle> list, SchemaParticle part, int parentParticleType) {
        if (part == null) {
            return;
        }

        switch (part.getParticleType()) {
            case SchemaParticle.SEQUENCE:
                if (parentParticleType == SchemaParticle.SEQUENCE && part.isSingleton()) {
                    // emitDBG("dropping redundant sequence");
                    list.addAll(Arrays.asList(part.getParticleChildren()));
                    return;
                }
                break;

            case SchemaParticle.CHOICE:
                if (parentParticleType == SchemaParticle.CHOICE && part.isSingleton()) {
                    // emitDBG("dropping redundant choice");
                    list.addAll(Arrays.asList(part.getParticleChildren()));
                    return;
                }
                break;

            case SchemaParticle.ALL:
            default:
        }
        list.add(part);
    }

    private static <T> BinaryOperator<T> throwingMerger() {
        return (u,v) -> { throw new IllegalStateException("Duplicate key "+u.toString()); };
    }

    static Map<QName, SchemaProperty> buildAttributePropertyModelByQName(SchemaAttributeModel attrModel, SchemaType owner) {
        return Stream.of(attrModel.getAttributes())
            .collect(Collectors.toMap(SchemaLocalAttribute::getName, a -> buildUseProperty(a, owner), throwingMerger(), LinkedHashMap::new));
    }

    static Map<QName, SchemaProperty> buildContentPropertyModelByQName(SchemaParticle part, SchemaType owner) {
        if (part == null) {
            return Collections.emptyMap();
        }

        boolean asSequence = false;
        Map<QName, SchemaProperty> model = null;

        switch (part.getParticleType()) {
            case SchemaParticle.ALL:
            case SchemaParticle.SEQUENCE:
                asSequence = true;
                break;
            case SchemaParticle.CHOICE:
                asSequence = false;
                break;
            case SchemaParticle.ELEMENT:
                model = buildElementPropertyModel((SchemaLocalElement) part, owner);
                break;
            case SchemaParticle.WILDCARD:
                model = Collections.emptyMap();
                break;
            default:
                assert (false);
                throw new IllegalStateException();
        }

        if (model == null) {
            // build model for children
            model = new LinkedHashMap<>();
            SchemaParticle[] children = part.getParticleChildren();

            for (SchemaParticle child : children) {
                // indentDBG();
                Map<QName, SchemaProperty> childModel = buildContentPropertyModelByQName(child, owner);
                // outdentDBG();
                for (SchemaProperty iProp : childModel.values()) {
                    SchemaPropertyImpl oProp = (SchemaPropertyImpl) model.get(iProp.getName());
                    if (oProp == null) {
                        if (!asSequence) {
                            ((SchemaPropertyImpl) iProp).setMinOccurs(BigInteger.ZERO);
                        }
                        model.put(iProp.getName(), iProp);
                        continue;
                    }
                    // consistency verified in an earlier step
                    assert (oProp.getType().equals(iProp.getType()));

                    mergeProperties(oProp, iProp, asSequence);
                }
            }

            // finally deal with minOccurs, maxOccurs over whole group
            BigInteger min = part.getMinOccurs();
            BigInteger max = part.getMaxOccurs();

            for (SchemaProperty oProp : model.values()) {
                BigInteger minOccurs = oProp.getMinOccurs();
                BigInteger maxOccurs = oProp.getMaxOccurs();

                minOccurs = minOccurs.multiply(min);
                if (max != null && max.equals(BigInteger.ZERO)) {
                    maxOccurs = BigInteger.ZERO;
                } else if (maxOccurs != null && !maxOccurs.equals(BigInteger.ZERO)) {
                    maxOccurs = max == null ? null : maxOccurs.multiply(max);
                }

                ((SchemaPropertyImpl) oProp).setMinOccurs(minOccurs);
                ((SchemaPropertyImpl) oProp).setMaxOccurs(maxOccurs);
            }
        }

        return model;
    }

    static Map<QName, SchemaProperty> buildElementPropertyModel(SchemaLocalElement epart, SchemaType owner) {
        SchemaProperty sProp = buildUseProperty(epart, owner);
        return Collections.singletonMap(sProp.getName(), sProp);
    }

    static SchemaProperty buildUseProperty(SchemaField use, SchemaType owner) {
        SchemaPropertyImpl sPropImpl = new SchemaPropertyImpl();
        sPropImpl.setName(use.getName());
        sPropImpl.setContainerTypeRef(owner.getRef());
        sPropImpl.setTypeRef(use.getType().getRef());
        sPropImpl.setAttribute(use.isAttribute());
        sPropImpl.setDefault(use.isDefault() ? SchemaProperty.CONSISTENTLY : SchemaProperty.NEVER);
        sPropImpl.setFixed(use.isFixed() ? SchemaProperty.CONSISTENTLY : SchemaProperty.NEVER);
        sPropImpl.setNillable(use.isNillable() ? SchemaProperty.CONSISTENTLY : SchemaProperty.NEVER);
        sPropImpl.setDefaultText(use.getDefaultText());
        sPropImpl.setMinOccurs(use.getMinOccurs());
        sPropImpl.setMaxOccurs(use.getMaxOccurs());

        if(use instanceof SchemaParticle){
            sPropImpl.setDocumentation(((SchemaParticle)use).getDocumentation());
        }

        if (use instanceof SchemaLocalElementImpl) {
            SchemaLocalElementImpl elt = (SchemaLocalElementImpl) use;
            sPropImpl.setAcceptedNames(elt.acceptedStartNames());
        }

        return sPropImpl;
    }

    static void mergeProperties(SchemaPropertyImpl into, SchemaProperty from, boolean asSequence) {
        // minoccur, maxoccur
        BigInteger minOccurs = into.getMinOccurs();
        BigInteger maxOccurs = into.getMaxOccurs();
        if (asSequence) {
            minOccurs = minOccurs.add(from.getMinOccurs());
            if (maxOccurs != null) {
                maxOccurs = (from.getMaxOccurs() == null ? null :
                    maxOccurs.add(from.getMaxOccurs()));
            }
        } else {
            minOccurs = minOccurs.min(from.getMinOccurs());
            if (maxOccurs != null) {
                maxOccurs = (from.getMaxOccurs() == null ? null :
                    maxOccurs.max(from.getMaxOccurs()));
            }
        }
        into.setMinOccurs(minOccurs);
        into.setMaxOccurs(maxOccurs);

        // nillable, default, fixed
        if (from.hasNillable() != into.hasNillable()) {
            into.setNillable(SchemaProperty.VARIABLE);
        }
        if (from.hasDefault() != into.hasDefault()) {
            into.setDefault(SchemaProperty.VARIABLE);
        }
        if (from.hasFixed() != into.hasFixed()) {
            into.setFixed(SchemaProperty.VARIABLE);
        }

        // default value
        if (into.getDefaultText() != null) {
            if (from.getDefaultText() == null ||
                !into.getDefaultText().equals(from.getDefaultText())) {
                into.setDefaultText(null);
            }
        }
    }

    static SchemaParticle[] ensureStateMachine(SchemaParticle[] children) {
        for (SchemaParticle child : children) {
            buildStateMachine(child);
        }
        return children;
    }

    static void buildStateMachine(SchemaParticle contentModel) {
        if (contentModel == null) {
            return;
        }

        SchemaParticleImpl partImpl = (SchemaParticleImpl) contentModel;
        if (partImpl.hasTransitionNotes()) {
            return;
        }

        QNameSetBuilder start = new QNameSetBuilder();
        QNameSetBuilder excludenext = new QNameSetBuilder();
        boolean deterministic = true;
        SchemaParticle[] children;
        boolean canskip = (partImpl.getMinOccurs().signum() == 0);

        switch (partImpl.getParticleType()) {
            case SchemaParticle.ELEMENT:
                // compute start and excludeNext; canskip is already correct
                if (partImpl.hasTransitionRules()) {
                    start.addAll(partImpl.acceptedStartNames());
                } else {
                    start.add(partImpl.getName());
                }

                break;

            case SchemaParticle.WILDCARD:
                // compute start and excludeNext; canskip is already correct
                start.addAll(partImpl.getWildcardSet());
                break;

            case SchemaParticle.SEQUENCE:
                children = ensureStateMachine(partImpl.getParticleChildren());

                // adjust canskip if all children are skippable
                canskip = true;
                for (int i = 0; canskip && i < children.length; i++) {
                    if (!(children[i]).isSkippable()) {
                        canskip = false;
                    }
                }

                // bubble up nondeterministic bit
                for (SchemaParticle child : children) {
                    if (!((SchemaParticleImpl) child).isDeterministic()) {
                        deterministic = false;
                        break;
                    }
                }

                // verify deterministic and compute excludeNext set
                for (int i = 1; i < children.length; i++) {
                    excludenext.addAll(((SchemaParticleImpl) children[i - 1]).getExcludeNextSet());
                    if (deterministic && !excludenext.isDisjoint((children[i]).acceptedStartNames())) {
                        deterministic = false;
                    }
                    if ((children[i]).isSkippable()) {
                        excludenext.addAll((children[i]).acceptedStartNames());
                    } else {
                        excludenext.clear();
                    }
                }

                // next, compute start set
                for (SchemaParticle child : children) {
                    start.addAll(child.acceptedStartNames());
                    if (!child.isSkippable()) {
                        break;
                    }
                }
                break;

            case SchemaParticle.CHOICE:
                children = ensureStateMachine(partImpl.getParticleChildren());

                // adjust canskip if any children are skippable
                canskip = false;
                for (SchemaParticle schemaParticle : children) {
                    if (schemaParticle.isSkippable()) {
                        canskip = true;
                        break;
                    }
                }

                // bubble up nondeterministic bit
                for (SchemaParticle child : children) {
                    if (!((SchemaParticleImpl) child).isDeterministic()) {
                        deterministic = false;
                        break;
                    }
                }

                // compute start and excludeNext sets, verify deterministic
                for (SchemaParticle child : children) {
                    if (deterministic && !start.isDisjoint(child.acceptedStartNames())) {
                        deterministic = false;
                    }
                    start.addAll(child.acceptedStartNames());
                    excludenext.addAll(((SchemaParticleImpl) child).getExcludeNextSet());
                }

                break;

            case SchemaParticle.ALL:
                children = ensureStateMachine(partImpl.getParticleChildren());

                // adjust canskip if all children are skippable
                canskip = true;
                for (SchemaParticle child : children) {
                    if (!child.isSkippable()) {
                        canskip = false;
                        break;
                    }
                }

                // bubble up nondeterministic bit
                for (SchemaParticle child : children) {
                    if (!((SchemaParticleImpl) child).isDeterministic()) {
                        deterministic = false;
                        break;
                    }
                }

                // compute start and excludeNext sets, verify deterministic
                for (SchemaParticle child : children) {
                    if (deterministic && !start.isDisjoint(child.acceptedStartNames())) {
                        deterministic = false;
                    }
                    start.addAll(child.acceptedStartNames());
                    excludenext.addAll(((SchemaParticleImpl) child).getExcludeNextSet());
                }
                if (canskip) {
                    excludenext.addAll(start);
                }

                break;

            default:
                throw new IllegalStateException("Unrecognized schema particle");
        }

        // apply looping logic

        BigInteger minOccurs = partImpl.getMinOccurs();
        BigInteger maxOccurs = partImpl.getMaxOccurs();
        boolean canloop = (maxOccurs == null || maxOccurs.compareTo(BigInteger.ONE) > 0);
        boolean varloop = (maxOccurs == null || minOccurs.compareTo(maxOccurs) < 0);

        if (canloop && deterministic && !excludenext.isDisjoint(start)) {
            // we have a possible looping nondeterminism.
            // let's take some time now to see if it's actually caused
            // by non-unique-particle-attribute or not.
            QNameSet suspectSet = excludenext.intersect(start);

            // compute the set of all particles that could start this group
            Map<SchemaParticle, QNameSet> startMap = new HashMap<>();
            particlesMatchingStart(partImpl, suspectSet, startMap, new QNameSetBuilder());

            // compute the set of all particles that could have been repeated rather than ending this group
            Map<SchemaParticle, QNameSet> afterMap = new HashMap<>();
            particlesMatchingAfter(partImpl, suspectSet, afterMap, new QNameSetBuilder(), true);

            // see if we can find a member of after that is not a member of start
            // if we can, then particle attribution is not unique
            deterministic = afterMapSubsumedByStartMap(startMap, afterMap);
        }

        if (varloop) {
            excludenext.addAll(start);
        }

        canskip = canskip || minOccurs.signum() == 0;

        partImpl.setTransitionRules(start.toQNameSet(), canskip);
        partImpl.setTransitionNotes(excludenext.toQNameSet(), deterministic);
    }

    private static boolean afterMapSubsumedByStartMap(Map<SchemaParticle, QNameSet> startMap, Map<SchemaParticle, QNameSet> afterMap) {
        if (afterMap.size() > startMap.size()) {
            return false;
        }

        if (afterMap.isEmpty()) {
            return true;
        }

        for (SchemaParticle part : startMap.keySet()) {
            if (part.getParticleType() == SchemaParticle.WILDCARD) {
                if (afterMap.containsKey(part)) {
                    QNameSet startSet = startMap.get(part);
                    QNameSet afterSet = afterMap.get(part);
                    if (!startSet.containsAll(afterSet)) {
                        return false;
                    }
                }
            }
            afterMap.remove(part);
            if (afterMap.isEmpty()) {
                return true;
            }
        }
        return false;
    }

    private static void particlesMatchingStart(SchemaParticle part, QNameSetSpecification suspectSet, Map<SchemaParticle, QNameSet> result, QNameSetBuilder eliminate) {
        switch (part.getParticleType()) {
            case SchemaParticle.ELEMENT:
                if (!suspectSet.contains(part.getName())) {
                    return;
                }
                result.put(part, null);
                eliminate.add(part.getName());
                return;

            case SchemaParticle.WILDCARD:
                if (suspectSet.isDisjoint(part.getWildcardSet())) {
                    return;
                }
                result.put(part, part.getWildcardSet().intersect(suspectSet));
                eliminate.addAll(part.getWildcardSet());
                return;

            case SchemaParticle.CHOICE:
            case SchemaParticle.ALL: {
                SchemaParticle[] children = part.getParticleChildren();
                for (SchemaParticle child : children) {
                    particlesMatchingStart(child, suspectSet, result, eliminate);
                }
                return;
            }

            case SchemaParticle.SEQUENCE: {
                SchemaParticle[] children = part.getParticleChildren();
                if (children.length == 0) {
                    return;
                }
                if (!children[0].isSkippable()) {
                    particlesMatchingStart(children[0], suspectSet, result, eliminate);
                    return;
                }
                QNameSetBuilder remainingSuspects = new QNameSetBuilder(suspectSet);
                QNameSetBuilder suspectsToEliminate = new QNameSetBuilder();
                for (SchemaParticle child : children) {
                    particlesMatchingStart(child, remainingSuspects, result, suspectsToEliminate);
                    eliminate.addAll(suspectsToEliminate);
                    if (!child.isSkippable()) {
                        return;
                    }
                    remainingSuspects.removeAll(suspectsToEliminate);
                    if (remainingSuspects.isEmpty()) {
                        return;
                    }
                    suspectsToEliminate.clear();
                }
            }
        }
    }

    private static void particlesMatchingAfter(SchemaParticle part, QNameSetSpecification suspectSet, Map<SchemaParticle, QNameSet> result, QNameSetBuilder eliminate, boolean top) {
        recurse:
        switch (part.getParticleType()) {
            case SchemaParticle.CHOICE:
            case SchemaParticle.ALL: {
                SchemaParticle[] children = part.getParticleChildren();
                for (SchemaParticle child : children) {
                    particlesMatchingAfter(child, suspectSet, result, eliminate, false);
                }
                break;
            }

            case SchemaParticle.SEQUENCE: {
                SchemaParticle[] children = part.getParticleChildren();
                if (children.length == 0) {
                    break;
                }
                if (!children[children.length - 1].isSkippable()) {
                    particlesMatchingAfter(children[0], suspectSet, result, eliminate, false);
                    break;
                }
                QNameSetBuilder remainingSuspects = new QNameSetBuilder(suspectSet);
                QNameSetBuilder suspectsToEliminate = new QNameSetBuilder();
                for (int i = children.length - 1; i >= 0; i--) {
                    particlesMatchingAfter(children[i], remainingSuspects, result, suspectsToEliminate, false);
                    eliminate.addAll(suspectsToEliminate);
                    if (!children[i].isSkippable()) {
                        break recurse;
                    }
                    remainingSuspects.removeAll(suspectsToEliminate);
                    if (remainingSuspects.isEmpty()) {
                        break recurse;
                    }
                    suspectsToEliminate.clear();
                }
                break;
            }
        }

        if (!top) {
            BigInteger minOccurs = part.getMinOccurs();
            BigInteger maxOccurs = part.getMaxOccurs();
            boolean varloop = (maxOccurs == null || minOccurs.compareTo(maxOccurs) < 0);
            if (varloop) {
                particlesMatchingStart(part, suspectSet, result, eliminate);
            }
        }
    }

    private static class CodeForNameEntry {
        CodeForNameEntry(QName name, int code) {
            this.name = name;
            this.code = code;
        }

        public QName name;
        public int code;
    }

    private static final int MODEL_GROUP_CODE = 100;

    private static final CodeForNameEntry[] particleCodes = {
        new CodeForNameEntry(QNameHelper.forLNS("all", "http://www.w3.org/2001/XMLSchema"), SchemaParticle.ALL),
        new CodeForNameEntry(QNameHelper.forLNS("sequence", "http://www.w3.org/2001/XMLSchema"), SchemaParticle.SEQUENCE),
        new CodeForNameEntry(QNameHelper.forLNS("choice", "http://www.w3.org/2001/XMLSchema"), SchemaParticle.CHOICE),
        new CodeForNameEntry(QNameHelper.forLNS("element", "http://www.w3.org/2001/XMLSchema"), SchemaParticle.ELEMENT),
        new CodeForNameEntry(QNameHelper.forLNS("any", "http://www.w3.org/2001/XMLSchema"), SchemaParticle.WILDCARD),
        new CodeForNameEntry(QNameHelper.forLNS("group", "http://www.w3.org/2001/XMLSchema"), MODEL_GROUP_CODE),
    };

    private static final Map<QName, Integer> particleCodeMap = Collections.unmodifiableMap(
        Stream.of(particleCodes).collect(Collectors.toMap(pc -> pc.name, pc -> pc.code)));

    private static int translateParticleCode(Group parseEg) {
        if (parseEg == null) {
            return 0;
        }
        try (XmlCursor c = parseEg.newCursor()) {
            return translateParticleCode(c.getName());
        }
    }

    private static int translateParticleCode(QName name) {
        return particleCodeMap.getOrDefault(name, 0);
    }

    private static final int ATTRIBUTE_CODE = 100;
    private static final int ATTRIBUTE_GROUP_CODE = 101;
    private static final int ANY_ATTRIBUTE_CODE = 102;

    private static final CodeForNameEntry[] attributeCodes = {
        new CodeForNameEntry(QNameHelper.forLNS("attribute", "http://www.w3.org/2001/XMLSchema"), ATTRIBUTE_CODE),
        new CodeForNameEntry(QNameHelper.forLNS("attributeGroup", "http://www.w3.org/2001/XMLSchema"), ATTRIBUTE_GROUP_CODE),
        new CodeForNameEntry(QNameHelper.forLNS("anyAttribute", "http://www.w3.org/2001/XMLSchema"), ANY_ATTRIBUTE_CODE),
    };

    private static final Map<QName,Integer> attributeCodeMap = Collections.unmodifiableMap(
        Stream.of(attributeCodes).collect(Collectors.toMap(ac -> ac.name, ac -> ac.code)));

    static int translateAttributeCode(QName currentName) {
        return attributeCodeMap.getOrDefault(currentName, 0);
    }
}
