/*   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.common.XBeanDebug;

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

public class StscChecker {
    public static void checkAll() {
        // walk the tree of types
        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.redefinedGlobalTypes()));
        allSeenTypes.addAll(Arrays.asList(state.globalTypes()));

        for (int i = 0; i < allSeenTypes.size(); i++) {
            SchemaType gType = allSeenTypes.get(i);
            // option to turn off particle restriction checking
            // Don't check doc types for restriction.
            if (!state.noPvr() && !gType.isDocumentType()) {
                checkRestriction((SchemaTypeImpl) gType);
            }
            checkFields((SchemaTypeImpl) gType);
            allSeenTypes.addAll(Arrays.asList(gType.getAnonymousTypes()));
        }

        checkSubstitutionGroups(state.globalElements());
    }

    /**
     * The following code checks rule #5 of http://www.w3.org/TR/xmlschema-1/#coss-ct
     * as well as attribute + element default/fixed validity.<p>
     * Checks that xs:NOTATION is not used directly
     */
    public static void checkFields(SchemaTypeImpl sType) {
        if (sType.isSimpleType()) {
            return;
        }

        XmlObject location = sType.getParseObject();

        SchemaAttributeModel sAttrModel = sType.getAttributeModel();
        if (sAttrModel != null) {
            SchemaLocalAttribute[] sAttrs = sAttrModel.getAttributes();
            QName idAttr = null;
            for (SchemaLocalAttribute sAttr : sAttrs) {
                XmlObject attrLocation = ((SchemaLocalAttributeImpl) sAttr)._parseObject;
                if (XmlID.type.isAssignableFrom(sAttr.getType())) {
                    if (idAttr == null) {
                        idAttr = sAttr.getName();
                    } else {
                        StscState.get().error(XmlErrorCodes.ATTR_GROUP_PROPERTIES$TWO_IDS,
                            new Object[]{QNameHelper.pretty(idAttr), sAttr.getName()},
                            attrLocation != null ? attrLocation : location);
                    }
                    if (sAttr.getDefaultText() != null) {
                        StscState.get().error(XmlErrorCodes.ATTR_PROPERTIES$ID_FIXED_OR_DEFAULT,
                            null, attrLocation != null ? attrLocation : location);
                    }
                } else if (XmlNOTATION.type.isAssignableFrom(sAttr.getType())) {
                    if (sAttr.getType().getBuiltinTypeCode() == SchemaType.BTC_NOTATION) {
                        StscState.get().recover(XmlErrorCodes.ATTR_NOTATION_TYPE_FORBIDDEN,
                            new Object[]{QNameHelper.pretty(sAttr.getName())},
                            attrLocation != null ? attrLocation : location);
                    } else {
                        if (sAttr.getType().getSimpleVariety() == SchemaType.UNION) {
                            SchemaType[] members = sAttr.getType().getUnionConstituentTypes();
                            for (SchemaType member : members) {
                                if (member.getBuiltinTypeCode() == SchemaType.BTC_NOTATION) {
                                    StscState.get().recover(XmlErrorCodes.ATTR_NOTATION_TYPE_FORBIDDEN,
                                        new Object[]{QNameHelper.pretty(sAttr.getName())},
                                        attrLocation != null ? attrLocation : location);
                                }
                            }
                        }
                        // Check that the Schema in which this is present doesn't have a targetNS
                        boolean hasNS;
                        if (sType.isAttributeType()) {
                            hasNS = sAttr.getName().getNamespaceURI().length() > 0;
                        } else {
                            SchemaType t = sType;
                            while (t.getOuterType() != null) {
                                t = t.getOuterType();
                            }
                            if (t.isDocumentType()) {
                                hasNS = t.getDocumentElementName().getNamespaceURI().length() > 0;
                            } else {
                                hasNS = t.getName().getNamespaceURI().length() > 0;
                            }
                        }
                        if (hasNS) {
                            StscState.get().warning(XmlErrorCodes.ATTR_COMPATIBILITY_TARGETNS,
                                new Object[]{QNameHelper.pretty(sAttr.getName())},
                                attrLocation != null ? attrLocation : location);
                        }
                    }
                } else {
                    String valueConstraint = sAttr.getDefaultText();
                    if (valueConstraint != null) {
                        try {
                            XmlAnySimpleType val = sAttr.getDefaultValue();
                            if (!val.validate()) {
                                throw new Exception();
                            }

                            SchemaPropertyImpl sProp = (SchemaPropertyImpl) sType.getAttributeProperty(sAttr.getName());
                            if (sProp != null && sProp.getDefaultText() != null) {
                                sProp.setDefaultValue(new XmlValueRef(val));
                            }
                        } catch (Exception e) {
                            // move to 'fixed' or 'default' attribute on the attribute definition
                            String constraintName = (sAttr.isFixed() ? "fixed" : "default");
                            XmlObject constraintLocation = location;
                            if (attrLocation != null) {
                                constraintLocation = attrLocation.selectAttribute("", constraintName);
                                if (constraintLocation == null) {
                                    constraintLocation = attrLocation;
                                }
                            }

                            StscState.get().error(XmlErrorCodes.ATTR_PROPERTIES$CONSTRAINT_VALID,
                                new Object[]{QNameHelper.pretty(sAttr.getName()),
                                    constraintName,
                                    valueConstraint,
                                    QNameHelper.pretty(sAttr.getType().getName())},
                                constraintLocation);
                        }
                    }
                }
            }
        }

        checkElementDefaults(sType.getContentModel(), location, sType);
    }

    /**
     * Checks the default values of elements.<p/>
     * Also checks that the type of elements is not one of ID, IDREF, IDREFS, ENTITY, ENTITIES or
     * NOTATION as per XMLSchema part 2.
     */
    private static void checkElementDefaults(SchemaParticle model, XmlObject location, SchemaType parentType) {
        if (model == null) {
            return;
        }
        switch (model.getParticleType()) {
            case SchemaParticle.SEQUENCE:
            case SchemaParticle.CHOICE:
            case SchemaParticle.ALL:
                SchemaParticle[] children = model.getParticleChildren();
                for (SchemaParticle child : children) {
                    checkElementDefaults(child, location, parentType);
                }
                break;
            case SchemaParticle.ELEMENT:
                String valueConstraint = model.getDefaultText();
                if (valueConstraint != null) {
                    if (model.getType().isSimpleType() || model.getType().getContentType() == SchemaType.SIMPLE_CONTENT) {
                        try {
                            XmlAnySimpleType val = model.getDefaultValue();
                            XmlOptions opt = new XmlOptions();
                            opt.setValidateTextOnly();
                            if (!val.validate(opt)) {
                                throw new Exception();
                            }

                            SchemaPropertyImpl sProp = (SchemaPropertyImpl) parentType.getElementProperty(model.getName());
                            if (sProp != null && sProp.getDefaultText() != null) {
                                sProp.setDefaultValue(new XmlValueRef(val));
                            }
                        } catch (Exception e) {
                            // move to 'fixed' or 'default' attribute on the element definition
                            String constraintName = (model.isFixed() ? "fixed" : "default");
                            XmlObject constraintLocation = location.selectAttribute("", constraintName);

                            StscState.get().error(XmlErrorCodes.ELEM_PROPERTIES$CONSTRAINT_VALID,
                                new Object[]{QNameHelper.pretty(model.getName()),
                                    constraintName,
                                    valueConstraint,
                                    QNameHelper.pretty(model.getType().getName())},
                                (constraintLocation == null ? location : constraintLocation));
                        }
                    } else if (model.getType().getContentType() == SchemaType.MIXED_CONTENT) {
                        if (!model.getType().getContentModel().isSkippable()) {
                            String constraintName = (model.isFixed() ? "fixed" : "default");
                            XmlObject constraintLocation = location.selectAttribute("", constraintName);

                            StscState.get().error(XmlErrorCodes.ELEM_DEFAULT_VALID$MIXED_AND_EMPTIABLE,
                                new Object[]{QNameHelper.pretty(model.getName()),
                                    constraintName,
                                    valueConstraint},
                                (constraintLocation == null ? location : constraintLocation));
                        } else {
                            // Element Default Valid (Immediate): cos-valid-default.2.2.2
                            // no need to validate the value; type is a xs:string
                            SchemaPropertyImpl sProp = (SchemaPropertyImpl) parentType.getElementProperty(model.getName());
                            if (sProp != null && sProp.getDefaultText() != null) {
                                sProp.setDefaultValue(new XmlValueRef(XmlString.type.newValue(valueConstraint)));
                            }
                        }
                    } else if (model.getType().getContentType() == SchemaType.ELEMENT_CONTENT) {
                        XmlObject constraintLocation = location.selectAttribute("", "default");
                        StscState.get().error(XmlErrorCodes.ELEM_DEFAULT_VALID$SIMPLE_TYPE_OR_MIXED,
                            new Object[]{QNameHelper.pretty(model.getName()),
                                valueConstraint,
                                "element"},
                            (constraintLocation == null ? location : constraintLocation));
                    } else if (model.getType().getContentType() == SchemaType.EMPTY_CONTENT) {
                        XmlObject constraintLocation = location.selectAttribute("", "default");
                        StscState.get().error(XmlErrorCodes.ELEM_DEFAULT_VALID$SIMPLE_TYPE_OR_MIXED,
                            new Object[]{QNameHelper.pretty(model.getName()),
                                valueConstraint,
                                "empty"},
                            (constraintLocation == null ? location : constraintLocation));
                    }
                }
                // Checks if the type is one of the "attribute-specific" types
                String warningType = null;
                if (BuiltinSchemaTypeSystem.ST_ID.isAssignableFrom(model.getType())) {
                    warningType = BuiltinSchemaTypeSystem.ST_ID.getName().getLocalPart();
                } else if (BuiltinSchemaTypeSystem.ST_IDREF.isAssignableFrom(model.getType())) {
                    warningType = BuiltinSchemaTypeSystem.ST_IDREF.getName().getLocalPart();
                } else if (BuiltinSchemaTypeSystem.ST_IDREFS.isAssignableFrom(model.getType())) {
                    warningType = BuiltinSchemaTypeSystem.ST_IDREFS.getName().getLocalPart();
                } else if (BuiltinSchemaTypeSystem.ST_ENTITY.isAssignableFrom(model.getType())) {
                    warningType = BuiltinSchemaTypeSystem.ST_ENTITY.getName().getLocalPart();
                } else if (BuiltinSchemaTypeSystem.ST_ENTITIES.isAssignableFrom(model.getType())) {
                    warningType = BuiltinSchemaTypeSystem.ST_ENTITIES.getName().getLocalPart();
                } else if (BuiltinSchemaTypeSystem.ST_NOTATION.isAssignableFrom(model.getType())) {
                    if (model.getType().getBuiltinTypeCode() == SchemaType.BTC_NOTATION) {
                        StscState.get().recover(XmlErrorCodes.ELEM_NOTATION_TYPE_FORBIDDEN,
                            new Object[]{QNameHelper.pretty(model.getName())},
                            ((SchemaLocalElementImpl) model)._parseObject == null ? location :
                                ((SchemaLocalElementImpl) model)._parseObject.selectAttribute("", "type"));
                    } else {
                        if (model.getType().getSimpleVariety() == SchemaType.UNION) {
                            SchemaType[] members = model.getType().getUnionConstituentTypes();
                            for (SchemaType member : members) {
                                if (member.getBuiltinTypeCode() == SchemaType.BTC_NOTATION) {
                                    StscState.get().recover(XmlErrorCodes.ELEM_NOTATION_TYPE_FORBIDDEN,
                                        new Object[]{QNameHelper.pretty(model.getName())},
                                        ((SchemaLocalElementImpl) model)._parseObject == null ? location :
                                            ((SchemaLocalElementImpl) model)._parseObject.selectAttribute("", "type"));
                                }
                            }
                        }
                        warningType = BuiltinSchemaTypeSystem.ST_NOTATION.getName().getLocalPart();
                    }

                    // Check that the Schema in which this is present doesn't have a targetNS
                    boolean hasNS;
                    SchemaType t = parentType;
                    while (t.getOuterType() != null) {
                        t = t.getOuterType();
                    }
                    if (t.isDocumentType()) {
                        hasNS = t.getDocumentElementName().getNamespaceURI().length() > 0;
                    } else {
                        hasNS = t.getName().getNamespaceURI().length() > 0;
                    }
                    if (hasNS) {
                        StscState.get().warning(XmlErrorCodes.ELEM_COMPATIBILITY_TARGETNS,
                            new Object[]{QNameHelper.pretty(model.getName())},
                            ((SchemaLocalElementImpl) model)._parseObject == null ? location :
                                ((SchemaLocalElementImpl) model)._parseObject);
                    }
                }

                if (warningType != null) {
                    StscState.get().warning(XmlErrorCodes.ELEM_COMPATIBILITY_TYPE, new Object[]
                            {QNameHelper.pretty(model.getName()), warningType},
                        ((SchemaLocalElementImpl) model)._parseObject == null ? location :
                            ((SchemaLocalElementImpl) model)._parseObject.selectAttribute("", "type"));
                }

                break;

            default:
                // nothing to do.
                break;
        }
    }

    /**
     * The following code only checks rule #5 of http://www.w3.org/TR/xmlschema-1/#derivation-ok-restriction
     * (Everything else can and should be done in StscResolver, because we can give more detailed line # info there
     */
    public static boolean checkRestriction(SchemaTypeImpl sType) {
        if (sType.getDerivationType() == SchemaType.DT_RESTRICTION && !sType.isSimpleType()) {
            StscState state = StscState.get();

            // we don't remember very precise line number information, but it's better than nothin.
            XmlObject location = sType.getParseObject();

            SchemaType baseType = sType.getBaseType();
            assert (baseType != null);
            if (baseType.isSimpleType()) {
                state.error(XmlErrorCodes.SCHEMA_COMPLEX_TYPE$COMPLEX_CONTENT,
                    new Object[]{QNameHelper.pretty(baseType.getName())},
                    location);
                return false;
            }

            // 5 The appropriate case among the following must be true:
            switch (sType.getContentType()) {
                case SchemaType.SIMPLE_CONTENT:
                    // 5.1 If the {content type} of the complex type definition is a simple type definition, then one of the following must be true:
                    switch (baseType.getContentType()) {
                        case SchemaType.SIMPLE_CONTENT:
                            // 5.1.1 The {content type} of the {base type definition} must be a simple type definition of which the {content type} is a �valid restriction� as defined in Derivation Valid (Restriction, Simple) (�3.14.6).
                            SchemaType cType = sType.getContentBasedOnType();
                            if (cType != baseType) {
                                // We have to check that the contentType is legally derived
                                // from the base simple type in the hierarchy
                                SchemaType bType = baseType;
                                while (bType != null && !bType.isSimpleType()) {
                                    bType = bType.getContentBasedOnType();
                                }
                                if (bType != null && !bType.isAssignableFrom(cType)) {
                                    state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$SC_NOT_DERIVED,
                                        null, location);
                                    return false;
                                }
                            }
                            break;

                        case SchemaType.MIXED_CONTENT:
                            // 5.1.2 The {base type definition} must be mixed and have a particle which is �emptiable� as defined in Particle Emptiable (�3.9.6).
                            if (baseType.getContentModel() != null && !baseType.getContentModel().isSkippable()) {
                                state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$SC_AND_MIXED_EMPTIABLE,
                                    null, location);
                                return false;
                            }
                            break;

                        default:
                            state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$SC_AND_SIMPLE_TYPE_OR_MIXED,
                                null, location);
                            return false;
                    }
                    break;

                case SchemaType.EMPTY_CONTENT:
                    // 5.2 If the {content type} of the complex type itself is empty , then one of the following must be true:
                    switch (baseType.getContentType()) {
                        case SchemaType.EMPTY_CONTENT:
                            // 5.2.1 The {content type} of the {base type definition} must also be empty.
                            break;
                        case SchemaType.MIXED_CONTENT:
                        case SchemaType.ELEMENT_CONTENT:
                            // 5.2.2 The {content type} of the {base type definition} must be elementOnly or mixed and have a particle which is �emptiable� as defined in Particle Emptiable (�3.9.6).
                            if (baseType.getContentModel() != null && !baseType.getContentModel().isSkippable()) {
                                state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$EMPTY_AND_ELEMENT_OR_MIXED_EMPTIABLE,
                                    null, location);
                                return false;
                            }
                            break;
                        default:
                            state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$EMPTY_AND_NOT_SIMPLE,
                                null, location);
                            return false;
                    }
                    break;

                case SchemaType.MIXED_CONTENT:
                    // 5.3 If the {content type} of the {base type definition} is mixed...
                    if (baseType.getContentType() != SchemaType.MIXED_CONTENT) {
                        state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$ELEMENT_OR_MIXED_AND_MIXED,
                            null, location);
                        return false;
                    }

                    // FALLTHROUGH
                case SchemaType.ELEMENT_CONTENT:
                    // 5.3 ... or the {content type} of the complex type definition itself is element-only,...
                    if (baseType.getContentType() == SchemaType.EMPTY_CONTENT) {
                        state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$ELEMENT_OR_MIXED_AND_EMPTY,
                            null, location);
                        return false;
                    }
                    if (baseType.getContentType() == SchemaType.SIMPLE_CONTENT) {
                        state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$ELEMENT_OR_MIXED_AND_SIMPLE,
                            null, location);
                        return false;
                    }

                    // 5.3 ... then the particle of the complex type definition itself must be a �valid restriction� of the particle of the {content type} of the {base type definition}
                    SchemaParticle baseModel = baseType.getContentModel();
                    SchemaParticle derivedModel = sType.getContentModel();

                    if (derivedModel == null && sType.getDerivationType() == SchemaType.DT_RESTRICTION) {
                        // it is ok to have an empty contentModel if it's a restriction
                        // see Particle Valid (Restriction) (3.9.6) all three bulets 2.2.1
                        return true;
                    } else if (baseModel == null || derivedModel == null) {
                        XBeanDebug.LOG.atTrace().withThrowable(new Exception("Stacktrace"))
                            .log("Null models that weren't caught by EMPTY_CONTENT: {} ({}), {} ({})", baseType, baseModel, sType, derivedModel);
                        state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$ELEMENT_OR_MIXED_AND_VALID, null, location);
                        return false;
                    }

                    // 5.3 ...  as defined in Particle Valid (Restriction) (�3.9.6).
                    List<XmlError> errors = new ArrayList<>();
                    boolean isValid = isParticleValidRestriction(baseModel, derivedModel, errors, location);
                    if (!isValid) {
                        // we only add the last error, because isParticleValidRestriction may add errors
                        // to the collection that it later changes its mind about, or it may (inadvertently)
                        // forget to describe an error into the collection....
                        if (errors.size() == 0) {
                            state.error(XmlErrorCodes.COMPLEX_TYPE_RESTRICTION$ELEMENT_OR_MIXED_AND_VALID, null, location);
                        } else {
                            state.getErrorListener().add(errors.get(errors.size() - 1));
                        }
                        //state.getErrorListener().addAll(errors);
                        return false; // KHK: should return false, right?
                    }
            }
        }
        return true;
    }

    /**
     * This function takes in two schema particle types, a baseModel, and a derived model and returns true if the
     * derivedModel can be egitimately be used for restriction.  Errors are put into the errors collections.
     *
     * @param baseModel    - The base schema particle
     * @param derivedModel - The derived (restricted) schema particle
     * @param errors       - Invalid restriction errors are put into this collection
     * @param context      . the context
     * @return boolean, true if valid restruction, false if invalid restriction
     */
    public static boolean isParticleValidRestriction(SchemaParticle baseModel, SchemaParticle derivedModel, Collection<XmlError> errors, XmlObject context) {
        boolean restrictionValid = false;
        // 1 They are the same particle.
        if (baseModel.equals(derivedModel)) {
            restrictionValid = true;
        } else {
            // Implement table defined in schema spec on restrictions at:
            //   http://www.w3.org/TR/xmlschema-1/#cos-particle-restrict
            switch (baseModel.getParticleType()) {
                case SchemaParticle.ELEMENT:
                    switch (derivedModel.getParticleType()) {
                        case SchemaParticle.ELEMENT:
                            restrictionValid = nameAndTypeOK((SchemaLocalElement) baseModel, (SchemaLocalElement) derivedModel, errors, context);
                            break;
                        case SchemaParticle.WILDCARD:
                        case SchemaParticle.ALL:
                        case SchemaParticle.CHOICE:
                        case SchemaParticle.SEQUENCE:
                            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_RESTRICTION$INVALID_RESTRICTION,
                                new Object[]{printParticle(derivedModel), printParticle(baseModel)}, context));
                            restrictionValid = false;
                            break;
                        default:
                            XBeanDebug.LOG.atDebug().withThrowable(new Exception("stacktrace")).log("Unknown schema type for Derived Type");
                            assert(false);
                    }
                    break;
                case SchemaParticle.WILDCARD:
                    switch (derivedModel.getParticleType()) {
                        case SchemaParticle.ELEMENT:
                            restrictionValid = nsCompat(baseModel, (SchemaLocalElement) derivedModel, errors, context);
                            break;
                        case SchemaParticle.WILDCARD:
                            restrictionValid = nsSubset(baseModel, derivedModel, errors, context);
                            break;
                        case SchemaParticle.ALL:
                        case SchemaParticle.CHOICE:
                        case SchemaParticle.SEQUENCE:
                            restrictionValid = nsRecurseCheckCardinality(baseModel, derivedModel, errors, context);
                            break;
                        default:
                            XBeanDebug.LOG.atDebug().withThrowable(new Exception("stacktrace")).log("Unknown schema type for Derived Type");
                            assert(false);
                    }
                    break;
                case SchemaParticle.ALL:
                    switch (derivedModel.getParticleType()) {
                        case SchemaParticle.ELEMENT:
                            restrictionValid = recurseAsIfGroup(baseModel, derivedModel, errors, context);
                            break;
                        case SchemaParticle.WILDCARD:
                        case SchemaParticle.CHOICE:
                            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_RESTRICTION$INVALID_RESTRICTION,
                                new Object[]{printParticle(derivedModel), printParticle(baseModel)}, context));
                            restrictionValid = false;
                            break;
                        case SchemaParticle.ALL:
                            restrictionValid = recurse(baseModel, derivedModel, errors, context);
                            break;
                        case SchemaParticle.SEQUENCE:
                            restrictionValid = recurseUnordered(baseModel, derivedModel, errors, context);
                            break;
                        default:
                            XBeanDebug.LOG.atDebug().withThrowable(new Exception("stacktrace")).log("Unknown schema type for Derived Type");
                            assert(false);
                    }
                    break;
                case SchemaParticle.CHOICE:
                    switch (derivedModel.getParticleType()) {
                        case SchemaParticle.ELEMENT:
                            restrictionValid = recurseAsIfGroup(baseModel, derivedModel, errors, context);
                            break;
                        case SchemaParticle.WILDCARD:
                        case SchemaParticle.ALL:
                            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_RESTRICTION$INVALID_RESTRICTION,
                                new Object[]{printParticle(derivedModel), printParticle(baseModel)}, context));
                            restrictionValid = false;
                            break;
                        case SchemaParticle.CHOICE:
                            restrictionValid = recurseLax(baseModel, derivedModel, errors, context);
                            break;
                        case SchemaParticle.SEQUENCE:
                            restrictionValid = mapAndSum(baseModel, derivedModel, errors, context);
                            break;
                        default:
                            XBeanDebug.LOG.atDebug().withThrowable(new Exception("stacktrace")).log("Unknown schema type for Derived Type");
                            assert(false);
                    }
                    break;
                case SchemaParticle.SEQUENCE:
                    switch (derivedModel.getParticleType()) {
                        case SchemaParticle.ELEMENT:
                            restrictionValid = recurseAsIfGroup(baseModel, derivedModel, errors, context);
                            break;
                        case SchemaParticle.WILDCARD:
                        case SchemaParticle.ALL:
                        case SchemaParticle.CHOICE:
                            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_RESTRICTION$INVALID_RESTRICTION,
                                new Object[]{printParticle(derivedModel), printParticle(baseModel)}, context));
                            restrictionValid = false;
                            break;
                        case SchemaParticle.SEQUENCE:
                            restrictionValid = recurse(baseModel, derivedModel, errors, context);
                            break;
                        default:
                            XBeanDebug.LOG.atDebug().withThrowable(new Exception("stacktrace")).log("Unknown schema type for Derived Type");
                            assert(false);
                    }
                    break;
                default:
                    XBeanDebug.LOG.atDebug().withThrowable(new Exception("stacktrace")).log("Unknown schema type for Base Type");
                    assert(false);

            }
        }

        return restrictionValid;
    }

    private static boolean mapAndSum(SchemaParticle baseModel, SchemaParticle derivedModel, Collection<XmlError> errors, XmlObject context) {
        // mapAndSum is call if base: CHOICE, derived: SEQUENCE
        assert baseModel.getParticleType() == SchemaParticle.CHOICE;
        assert derivedModel.getParticleType() == SchemaParticle.SEQUENCE;
        // Schema Component Constraint: Particle Derivation OK (Sequence:Choice -- MapAndSum)
        // For a sequence group particle to be a �valid restriction� of a choice group particle all of the following
        // must be true:
        // 1 There is a complete functional mapping from the particles in the {particles} of R to the particles in the
        // {particles} of B such that each particle in the {particles} of R is a �valid restriction� of the particle in
        // the {particles} of B it maps to as defined by Particle Valid (Restriction) (�3.9.6).
        // interpretation:  each particle child in derived should have a match in base.
        // 2 The pair consisting of the product of the {min occurs} of R and the length of its {particles} and unbounded
        // if {max occurs} is unbounded otherwise the product of the {max occurs} of R and the length of its {particles}
        // is a valid restriction of B's occurrence range as defined by Occurrence Range OK (�3.9.6).
        // NOTE: This clause is in principle more restrictive than absolutely necessary, but in practice will cover
        // all the likely cases, and is much easier to specify than the fully general version.
        // NOTE: This case allows the "unfolding" of iterated disjunctions into sequences. It may be particularly useful
        // when the disjunction is an implicit one arising from the use of substitution groups.

        // Map step - for each member of the derived model's particle children search base model's particle children
        //  for match
        SchemaParticle[] derivedParticleArray = derivedModel.getParticleChildren();
        SchemaParticle[] baseParticleArray = baseModel.getParticleChildren();
        for (SchemaParticle derivedParticle : derivedParticleArray) {
            boolean foundMatch = false;
            for (SchemaParticle baseParticle : baseParticleArray) {
                // recurse to check if there is a match
                if (isParticleValidRestriction(baseParticle, derivedParticle, errors, context)) {
                    // if there is a match then no need to check base particles anymore
                    foundMatch = true;
                    break;
                }
            }
            if (!foundMatch) {
                errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_DERIVATION_MAP_AND_SUM$MAP,
                    new Object[]{printParticle(derivedParticle)},
                    context));
                // KHK: if we don't return false now, this error may get swallowed by an error produced below
                return false;
            }
        }

        // Sum step
        BigInteger derivedRangeMin = derivedModel.getMinOccurs().multiply(BigInteger.valueOf(derivedModel.getParticleChildren().length));
        BigInteger derivedRangeMax;
        if (derivedModel.getMaxOccurs() == null) {
            derivedRangeMax = null;
        } else {
            derivedRangeMax = derivedModel.getMaxOccurs().multiply(BigInteger.valueOf(derivedModel.getParticleChildren().length));
        }

        // Now check derivedRange (derivedRangeMin and derivedRangeMax) against base model occurrence range
        // Schema Component Constraint: Occurrence Range OK
        // For a particle's occurrence range to be a valid restriction of another's occurrence range all of the following must be true:
        // 1 Its {min occurs} is greater than or equal to the other's {min occurs}.
        // 2 one of the following must be true:
        //   2.1 The other's {max occurs} is unbounded.
        //   2.2 Both {max occurs} are numbers, and the particle's is less than or equal to the other's.

        boolean mapAndSumValid = true;
        if (derivedRangeMin.compareTo(baseModel.getMinOccurs()) < 0) {
            mapAndSumValid = false;
            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_DERIVATION_MAP_AND_SUM$SUM_MIN_OCCURS_GTE_MIN_OCCURS,
                new Object[]{derivedRangeMin.toString(), baseModel.getMinOccurs().toString()},
                context));
        } else if (baseModel.getMaxOccurs() != null && (derivedRangeMax == null || derivedRangeMax.compareTo(baseModel.getMaxOccurs()) > 0)) {
            mapAndSumValid = false;
            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_DERIVATION_MAP_AND_SUM$SUM_MAX_OCCURS_LTE_MAX_OCCURS,
                new Object[]{derivedRangeMax == null ? "unbounded" : derivedRangeMax.toString(), baseModel.getMaxOccurs().toString()},
                context));
        }

        return mapAndSumValid;
    }

    private static boolean recurseAsIfGroup(SchemaParticle baseModel, SchemaParticle derivedModel, Collection<XmlError> errors, XmlObject context) {
        // recurseAsIfGroup is called if:
        // base: ALL, derived: ELEMENT
        // base: CHOICE, derived: ELEMENT
        // base: SEQUENCE, derived: ELEMENT
        assert (baseModel.getParticleType() == SchemaParticle.ALL && derivedModel.getParticleType() == SchemaParticle.ELEMENT)
               || (baseModel.getParticleType() == SchemaParticle.CHOICE && derivedModel.getParticleType() == SchemaParticle.ELEMENT)
               || (baseModel.getParticleType() == SchemaParticle.SEQUENCE && derivedModel.getParticleType() == SchemaParticle.ELEMENT);
        // Schema Component Constraint: Particle Derivation OK (Elt:All/Choice/Sequence -- RecurseAsIfGroup)

        // For an element declaration particle to be a �valid restriction� of a group particle
        // (all, choice or sequence) a group particle of the variety corresponding to B's, with {min occurs} and
        // {max occurs} of 1 and with {particles} consisting of a single particle the same as the element declaration
        // must be a �valid restriction� of the group as defined by Particle Derivation OK
        // (All:All,Sequence:Sequence -- Recurse) (�3.9.6), Particle Derivation OK (Choice:Choice -- RecurseLax)
        // (�3.9.6) or Particle Derivation OK (All:All,Sequence:Sequence -- Recurse) (�3.9.6), depending on whether
        // the group is all, choice or sequence

        // interpretation:  make a fake group of the right type, with min occurs and max occurs of 1
        SchemaParticleImpl asIfPart = new SchemaParticleImpl();
        asIfPart.setParticleType(baseModel.getParticleType());
        asIfPart.setMinOccurs(BigInteger.ONE);
        asIfPart.setMaxOccurs(BigInteger.ONE);
        asIfPart.setParticleChildren(new SchemaParticle[]{derivedModel});

        // the recurse
        return isParticleValidRestriction(baseModel, asIfPart, errors, context);
    }

    private static boolean recurseLax(SchemaParticle baseModel, SchemaParticle derivedModel, Collection<XmlError> errors, XmlObject context) {
        // recurseLax is called if base: CHOICE, derived: CHOICE
        assert baseModel.getParticleType() == SchemaParticle.CHOICE && derivedModel.getParticleType() == SchemaParticle.CHOICE;
        boolean recurseLaxValid = true;
        //Schema Component Constraint: Particle Derivation OK (Choice:Choice -- RecurseLax)
        // For a choice group particle to be a �valid restriction� of another choice group particle all of the
        // following must be true:
        // 1 R's occurrence range is a valid restriction of B's occurrence range as defined by Occurrence
        // Range OK (�3.9.6);
        // 2 There is a complete �order-preserving� functional mapping from the particles in the {particles} of R
        // to the particles in the {particles} of B such that each particle in the {particles} of R is a
        // �valid restriction� of the particle in the {particles} of B it maps to as defined by
        // Particle Valid (Restriction) (�3.9.6).
        // NOTE: Although the �validation� semantics of a choice group does not depend on the order of its particles,
        // derived choice groups are required to match the order of their base in order to simplify
        // checking that the derivation is OK.
        // interpretation:  check derived choices for match in order, must get an in order match on a base particle,
        //                  don't need to check if base particles are skippable.  a lot like recurse

        if (!occurrenceRangeOK(baseModel, derivedModel, errors, context)) {
            return false;
        }
        // cycle thru both derived particle children and base particle children looking for matches
        //  if the derived particle does not match the base particle then base particle can be skipped

        SchemaParticle[] derivedParticleArray = derivedModel.getParticleChildren();
        SchemaParticle[] baseParticleArray = baseModel.getParticleChildren();
        int i = 0, j = 0;
        while (i < derivedParticleArray.length && j < baseParticleArray.length) {
            SchemaParticle derivedParticle = derivedParticleArray[i];
            SchemaParticle baseParticle = baseParticleArray[j];
            // try to match the two particles by recursing
            if (isParticleValidRestriction(baseParticle, derivedParticle, errors, context)) {
                // cool found a match, increment both indexes
                i++;
                j++;
            } else {
                // did not match, increment the base particle array index only
                // Ok, let's skip this base particle, increment base particle array index only
                j++;
            }
        }

        // ok, got to the end of one of the arrays
        // if at end of base particle array and not at the end of derived particle array then remaining derived
        //  particles must not match
        if (i < derivedParticleArray.length) {
            recurseLaxValid = false;
            //String message = "Found derived particles that are not matched in the base content model.";
            //errors.add(XmlError.forObject(formatDerivedMappingError(message, baseModel, derivedModel), context));
            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_DERIVATION_RECURSE_LAX$MAP,
                new Object[]{printParticles(baseParticleArray, i)},
                context));
        }


        return recurseLaxValid;
    }

    private static boolean recurseUnordered(SchemaParticle baseModel, SchemaParticle derivedModel, Collection<XmlError> errors, XmlObject context) {
        // recurseUnorder is called when base: ALL and derived: SEQ
        assert baseModel.getParticleType() == SchemaParticle.ALL && derivedModel.getParticleType() == SchemaParticle.SEQUENCE;
        boolean recurseUnorderedValid = true;
        // Schema Component Constraint: Particle Derivation OK (Sequence:All -- RecurseUnordered)
        // For a sequence group particle to be a �valid restriction� of an all group particle all of the
        // following must be true:
        // 1 R's occurrence range is a valid restriction of B's occurrence range as defined by
        // Occurrence Range OK (�3.9.6).
        // 2 There is a complete functional mapping from the particles in the {particles} of R to the particles
        // in the {particles} of B such that all of the following must be true:
        // 2.1 No particle in the {particles} of B is mapped to by more than one of the particles in
        // the {particles} of R;
        // 2.2 Each particle in the {particles} of R is a �valid restriction� of the particle in the {particles} of B
        // it maps to as defined by Particle Valid (Restriction) (�3.9.6);
        // 2.3 All particles in the {particles} of B which are not mapped to by any particle in the {particles}
        // of R are �emptiable� as defined by Particle Emptiable (�3.9.6).
        // NOTE: Although this clause allows reordering, because of the limits on the contents of all groups the
        // checking process can still be deterministic.
        // 1, 2.2, and 2.3 are the same as recurse, so do 2.1 and then call recurse

        if (!occurrenceRangeOK(baseModel, derivedModel, errors, context)) {
            return false;
        }

        // read baseParticle array QNames into hashmap
        SchemaParticle[] baseParticles = baseModel.getParticleChildren();
        HashMap<QName, Object> baseParticleMap = new HashMap<>(10);
        final Object MAPPED = new Object();
        // Initialize the hashmap
        for (SchemaParticle particle : baseParticles) {
            baseParticleMap.put(particle.getName(), particle);
        }

        // go thru the sequence (derived model's children) and check off from base particle map
        SchemaParticle[] derivedParticles = derivedModel.getParticleChildren();
        for (SchemaParticle derivedParticle : derivedParticles) {
            Object baseParticle = baseParticleMap.get(derivedParticle.getName());
            if (baseParticle == null) {
                recurseUnorderedValid = false;
                errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_DERIVATION_RECURSE_UNORDERED$MAP,
                    new Object[]{printParticle(derivedParticle)}, context));
                break;
            } else {
                // got a match
                if (baseParticle == MAPPED) {
                    // whoa, this base particle has already been matched (see 2.1 above)
                    recurseUnorderedValid = false;
                    errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_DERIVATION_RECURSE_UNORDERED$MAP_UNIQUE,
                        new Object[]{printParticle(derivedParticle)}, context));
                    break;
                } else {
                    SchemaParticle matchedBaseParticle = (SchemaParticle) baseParticle;
                    if (derivedParticle.getMaxOccurs() == null ||
                        derivedParticle.getMaxOccurs().compareTo(BigInteger.ONE) > 0) {
                        // no derived particles can have a max occurs greater than 1
                        recurseUnorderedValid = false;
                        errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_DERIVATION_RECURSE_UNORDERED$MAP_MAX_OCCURS_1,
                            new Object[]{printParticle(derivedParticle), printMaxOccurs(derivedParticle.getMinOccurs())},
                            context));
                        break;
                    }
                    if (!isParticleValidRestriction(matchedBaseParticle, derivedParticle, errors, context)) {
                        // already have an error
                        recurseUnorderedValid = false;
                        break;
                    }
                    // everything is cool, got a match, update to MAPPED
                    baseParticleMap.put(derivedParticle.getName(), MAPPED);
                }
            }
        }

        // if everything is cool so far then check to see if any base particles are not matched
        if (recurseUnorderedValid) {
            // get all the hashmap keys and loop thru looking for NOT_MAPPED
            Set<QName> baseParticleCollection = baseParticleMap.keySet();
            for (QName baseParticleQName : baseParticleCollection) {
                if (baseParticleMap.get(baseParticleQName) != MAPPED && !((SchemaParticle) baseParticleMap.get(baseParticleQName)).isSkippable()) {
                    // this base particle was not mapped and is not "particle emptiable" (skippable)
                    recurseUnorderedValid = false;
                    errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_DERIVATION_RECURSE_UNORDERED$UNMAPPED_ARE_EMPTIABLE,
                        new Object[]{printParticle((SchemaParticle) baseParticleMap.get(baseParticleQName))},
                        context));
                }
            }
        }

        return recurseUnorderedValid;
    }

    private static boolean recurse(SchemaParticle baseModel, SchemaParticle derivedModel, Collection<XmlError> errors, XmlObject context) {
        // recurse is called when base: ALL derived: ALL or base: SEQUENCE derived: SEQUENCE
        boolean recurseValid = true;
        // For an all or sequence group particle to be a �valid restriction� of another group particle with the same
        // {compositor} all of the following must be true:
        // 1 R's occurrence range is a valid restriction of B's occurrence range as defined by
        // Occurrence Range OK (�3.9.6).
        // 2 There is a complete �order-preserving� functional mapping from the particles in the {particles} of R to
        // the particles in the {particles} of B such that all of the following must be true:
        //   2.1 Each particle in the {particles} of R is a �valid restriction� of the particle in the {particles}
        //   of B it maps to as defined by Particle Valid (Restriction) (�3.9.6).
        //   2.2 All particles in the {particles} of B which are not mapped to by any particle in the {particles}
        //   of R are �emptiable� as defined by Particle Emptiable (�3.9.6).
        // NOTE: Although the �validation� semantics of an all group does not depend on the order of its particles,
        // derived all groups are required to match the order of their base in order to simplify checking that
        // the derivation is OK.
        // [Definition:]  A complete functional mapping is order-preserving if each particle r in the domain R maps
        // to a particle b in the range B which follows (not necessarily immediately) the particle in the range B
        // mapped to by the predecessor of r, if any, where "predecessor" and "follows" are defined with respect to
        // the order of the lists which constitute R and B.

        if (!occurrenceRangeOK(baseModel, derivedModel, errors, context)) {
            // error message is formatted in occurrencRangeOK ...
            return false;
        }
        // cycle thru both derived particle children and base particle children looking for matches
        //  if the derived particle does not match the base particle then base particle can be skipped if it is
        //  skippable (same as "particle emptiable") otherwise is an invalid restriction.
        // after the derived particles have been cycled if there are any base particles left over then they
        //  must be skippable or invalid restriction

        SchemaParticle[] derivedParticleArray = derivedModel.getParticleChildren();
        SchemaParticle[] baseParticleArray = baseModel.getParticleChildren();
        int i = 0, j = 0;
        while (i < derivedParticleArray.length && j < baseParticleArray.length) {
            SchemaParticle derivedParticle = derivedParticleArray[i];
            SchemaParticle baseParticle = baseParticleArray[j];
            // try to match the two particles by recursing
            if (isParticleValidRestriction(baseParticle, derivedParticle, errors, context)) {
                // cool found a match, increment both indexes
                i++;
                j++;
            } else {
                // did not match, increment the base particle array index only
                //  that's ok if the base particle is skippable
                if (baseParticle.isSkippable()) {
                    // Ok, let's skip this base particle, increment base particle array index only
                    j++;
                } else {
                    // whoa, particles are not valid restrictions and base is not skippable - ERROR
                    recurseValid = false;
                    errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_DERIVATION_RECURSE$MAP_VALID,
                        new Object[]{printParticle(derivedParticle), printParticle(derivedModel),
                            printParticle(baseParticle), printParticle(baseModel)},
                        context));
                    break;
                }
            }
        }

        // ok, got to the end of one of the arrays
        // if at end of base particle array and not at the end of derived particle array then remaining derived
        //  particles must not match
        if (i < derivedParticleArray.length) {
            recurseValid = false;
            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_DERIVATION_RECURSE$MAP,
                new Object[]{printParticle(derivedModel), printParticle(baseModel), printParticles(derivedParticleArray, i)},
                context));
        } else {
            // if at end of derived particle array and not at end of base particle array then chck remaining
            //  base particles to assure they are skippable
            if (j < baseParticleArray.length) {
                ArrayList<SchemaParticle> particles = new ArrayList<>(baseParticleArray.length);
                for (int k = j; k < baseParticleArray.length; k++) {
                    if (!baseParticleArray[k].isSkippable()) {
                        particles.add(baseParticleArray[k]);
                    }
                }
                if (particles.size() > 0) {
                    recurseValid = false;
                    errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_DERIVATION_RECURSE$UNMAPPED_ARE_EMPTIABLE,
                        new Object[]{printParticle(baseModel), printParticle(derivedModel), printParticles(particles)}, context));
                }
            }
        }

        return recurseValid;
    }

    private static boolean nsRecurseCheckCardinality(SchemaParticle baseModel, SchemaParticle derivedModel, Collection<XmlError> errors, XmlObject context) {
        // nsRecurseCheckCardinality is called when:
        // base: ANY, derived: ALL
        // base: ANY, derived: CHOICE
        // base: ANY, derived: SEQUENCE
        assert baseModel.getParticleType() == SchemaParticle.WILDCARD;
        assert (derivedModel.getParticleType() == SchemaParticle.ALL)
               || (derivedModel.getParticleType() == SchemaParticle.CHOICE)
               || (derivedModel.getParticleType() == SchemaParticle.SEQUENCE);
        boolean nsRecurseCheckCardinality = true;
        // For a group particle to be a �valid restriction� of a wildcard particle all of the following must be true:
        // 1 Every member of the {particles} of the group is a �valid restriction� of the wildcard as defined by Particle Valid (Restriction) (�3.9.6).
        //  Note:  not positive what this means.  Interpreting to mean that every particle of the group must adhere to wildcard derivation rules
        //         in a recursive manner
        //  Loop thru the children particles of the group and invoke the appropriate function to check for wildcard restriction validity

        // BAU - an errata should be submitted on this clause of the spec, because the
        // spec makes no sense, as the xstc particlesR013.xsd test exemplifies.
        // what we _should_ so is an "as if" on the wildcard, allowing it minOccurs="0" maxOccurs="unbounded"
        // before recursing
        SchemaParticleImpl asIfPart = new SchemaParticleImpl();
        asIfPart.setParticleType(baseModel.getParticleType());
        asIfPart.setWildcardProcess(baseModel.getWildcardProcess());
        asIfPart.setWildcardSet(baseModel.getWildcardSet());
        asIfPart.setMinOccurs(BigInteger.ZERO);
        asIfPart.setMaxOccurs(null);
        asIfPart.setTransitionRules(baseModel.getWildcardSet(), true);
        asIfPart.setTransitionNotes(baseModel.getWildcardSet(), true);

        SchemaParticle[] particleChildren = derivedModel.getParticleChildren();
        for (SchemaParticle particle : particleChildren) {
            switch (particle.getParticleType()) {
                case SchemaParticle.ELEMENT:
                    // Check for valid Wildcard/Element derivation
                    nsRecurseCheckCardinality = nsCompat(asIfPart, (SchemaLocalElement) particle, errors, context);
                    break;
                case SchemaParticle.WILDCARD:
                    // Check for valid Wildcard/Wildcard derivation
                    nsRecurseCheckCardinality = nsSubset(asIfPart, particle, errors, context);
                    break;
                case SchemaParticle.ALL:
                case SchemaParticle.CHOICE:
                case SchemaParticle.SEQUENCE:
                    // Check for valid Wildcard/Group derivation
                    nsRecurseCheckCardinality = nsRecurseCheckCardinality(asIfPart, particle, errors, context);
                    break;
                default:
                    break;
            }
            // If any particle is invalid then break the loop
            if (!nsRecurseCheckCardinality) {
                break;
            }
        }

        // 2 The effective total range of the group, as defined by Effective Total Range (all and sequence) (�3.8.6)
        // (if the group is all or sequence) or Effective Total Range (choice) (�3.8.6) (if it is choice) is a valid
        // restriction of B's occurrence range as defined by Occurrence Range OK (�3.9.6).

        if (nsRecurseCheckCardinality) {
            nsRecurseCheckCardinality = checkGroupOccurrenceOK(baseModel, derivedModel, errors, context);
        }

        return nsRecurseCheckCardinality;
    }

    private static boolean checkGroupOccurrenceOK(SchemaParticle baseModel, SchemaParticle derivedModel, Collection<XmlError> errors, XmlObject context) {
        boolean groupOccurrenceOK = true;
        BigInteger minRange = BigInteger.ZERO;
        BigInteger maxRange = BigInteger.ZERO;
        switch (derivedModel.getParticleType()) {
            case SchemaParticle.ALL:
            case SchemaParticle.SEQUENCE:
                minRange = getEffectiveMinRangeAllSeq(derivedModel);
                maxRange = getEffectiveMaxRangeAllSeq(derivedModel);
                break;
            case SchemaParticle.CHOICE:
                minRange = getEffectiveMinRangeChoice(derivedModel);
                maxRange = getEffectiveMaxRangeChoice(derivedModel);
                break;
            default:
                break;
        }

        // Check min occurs for validity
        // derived min occurs is valid if its {min occurs} is greater than or equal to the other's {min occurs}.
        if (minRange.compareTo(baseModel.getMinOccurs()) < 0) {
            groupOccurrenceOK = false;
            errors.add(XmlError.forObject(XmlErrorCodes.OCCURRENCE_RANGE$MIN_GTE_MIN,
                new Object[]{printParticle(derivedModel), printParticle(baseModel)},
                context));
        }
        // Check max occurs for validity
        // one of the following must be true:
        // The base model's {max occurs} is unbounded.
        // or both {max occurs} are numbers, and the particle's is less than or equal to the other's
        if (baseModel.getMaxOccurs() != null) {
            if (maxRange == null) {
                groupOccurrenceOK = false;
                errors.add(XmlError.forObject(XmlErrorCodes.OCCURRENCE_RANGE$MAX_LTE_MAX,
                    new Object[]{printParticle(derivedModel), printParticle(baseModel)},
                    context));
            } else {
                if (maxRange.compareTo(baseModel.getMaxOccurs()) > 0) {
                    groupOccurrenceOK = false;
                    errors.add(XmlError.forObject(XmlErrorCodes.OCCURRENCE_RANGE$MAX_LTE_MAX,
                        new Object[]{printParticle(derivedModel), printParticle(baseModel)},
                        context));
                }
            }
        }
        return groupOccurrenceOK;
    }

    private static BigInteger getEffectiveMaxRangeChoice(SchemaParticle derivedModel) {
        BigInteger maxRange = BigInteger.ZERO;
        // Schema Component Constraint: Effective Total Range (choice)
        // The effective total range of a particle whose {term} is a group whose {compositor} is choice
        // is a pair of minimum and maximum, as follows:
        // MAXIMUM
        // 1) unbounded if the {max occurs} of any wildcard or element declaration particle in the group's {particles} or
        // the maximum part of the effective total range of any of the group particles in the group's {particles} is
        // unbounded (note: seems to be the same as Max Range All or Sequence),
        // or 2) if any of those is non-zero and the {max occurs} of the particle itself is unbounded,
        // otherwise 3) the product of the particle's {max occurs} and the maximum of the {max occurs} of every
        // wildcard or element declaration particle in the group's {particles} and the *maximum* (note: this is the difference
        // between MaxRange Choice ans MaxRange All or Sequence) part of the
        // effective total range of each of the group particles in the group's {particles}
        // (or 0 if there are no {particles}).

        boolean nonZeroParticleChildFound = false;
        BigInteger maxOccursInWildCardOrElement = BigInteger.ZERO;
        BigInteger maxOccursInGroup = BigInteger.ZERO;
        SchemaParticle[] particleChildren = derivedModel.getParticleChildren();
        for (SchemaParticle particle : particleChildren) {
            switch (particle.getParticleType()) {
                case SchemaParticle.WILDCARD:
                case SchemaParticle.ELEMENT:
                    // if unbounded then maxoccurs will be null
                    if (particle.getMaxOccurs() == null) {
                        maxRange = null;
                    } else {
                        if (particle.getIntMaxOccurs() > 0) {
                            // show tht at least one non-zero particle is found for later test
                            nonZeroParticleChildFound = true;
                            if (particle.getMaxOccurs().compareTo(maxOccursInWildCardOrElement) > 0) {
                                maxOccursInWildCardOrElement = particle.getMaxOccurs();
                            }
                        }
                    }
                    break;
                case SchemaParticle.ALL:
                case SchemaParticle.SEQUENCE:
                    maxRange = getEffectiveMaxRangeAllSeq(particle);
                    if (maxRange != null) {
                        // keep highest maxoccurs found
                        if (maxRange.compareTo(maxOccursInGroup) > 0) {
                            maxOccursInGroup = maxRange;
                        }
                    }
                    break;
                case SchemaParticle.CHOICE:
                    maxRange = getEffectiveMaxRangeChoice(particle);
                    if (maxRange != null) {
                        // keep highest maxoccurs found
                        if (maxRange.compareTo(maxOccursInGroup) > 0) {
                            maxOccursInGroup = maxRange;
                        }
                    }
                    break;
                default:
                    break;
            }
            // if an unbounded has been found then we are done
            if (maxRange == null) {
                break;
            }
        }

        // 1) unbounded if the {max occurs} of any wildcard or element declaration particle in the group's {particles} or
        // the maximum part of the effective total range of any of the group particles in the group's {particles} is
        // unbounded
        if (maxRange != null) {
            // 2) if any of those is non-zero and the {max occurs} of the particle itself is unbounded
            if (nonZeroParticleChildFound && derivedModel.getMaxOccurs() == null) {
                maxRange = null;
            } else {
                // 3) the product of the particle's {max occurs} and the maximum of the {max occurs} of every
                // wildcard or element declaration particle in the group's {particles} and the *maximum*
                // part of the effective total range of each of the group particles in the group's {particles}
                maxRange = derivedModel.getMaxOccurs().multiply(maxOccursInWildCardOrElement.add(maxOccursInGroup));
            }
        }

        return maxRange;
    }

    private static BigInteger getEffectiveMaxRangeAllSeq(SchemaParticle derivedModel) {
        BigInteger maxRange = BigInteger.ZERO;
        // Schema Component Constraint: Effective Total Range (all and sequence)
        // The effective total range of a particle whose {term} is a group whose {compositor} is all or sequence is a
        // pair of minimum and maximum, as follows:
        // MAXIMUM
        // 1) unbounded if the {max occurs} of any wildcard or element declaration particle in the group's {particles} or
        // the maximum part of the effective total range of any of the group particles in the group's {particles} is
        // unbounded, or 2) if any of those is non-zero and the {max occurs} of the particle itself is unbounded, otherwise
        // 3) the product of the particle's {max occurs} and the *sum* of the {max occurs} of every wildcard or element
        // declaration particle in the group's {particles} and the maximum part of the effective total range of each of
        // the group particles in the group's {particles} (or 0 if there are no {particles}).

        boolean nonZeroParticleChildFound = false;
        BigInteger maxOccursTotal = BigInteger.ZERO;
        BigInteger maxOccursInGroup = BigInteger.ZERO;
        SchemaParticle[] particleChildren = derivedModel.getParticleChildren();
        for (SchemaParticle particle : particleChildren) {
            switch (particle.getParticleType()) {
                case SchemaParticle.WILDCARD:
                case SchemaParticle.ELEMENT:
                    // if unbounded then maxoccurs will be null
                    if (particle.getMaxOccurs() == null) {
                        maxRange = null;
                    } else {
                        if (particle.getIntMaxOccurs() > 0) {
                            // show tht at least one non-zero particle is found for later test
                            nonZeroParticleChildFound = true;
                            maxOccursTotal = maxOccursTotal.add(particle.getMaxOccurs());
                        }
                    }
                    break;
                case SchemaParticle.ALL:
                case SchemaParticle.SEQUENCE:
                    maxRange = getEffectiveMaxRangeAllSeq(particle);
                    if (maxRange != null) {
                        // keep highest maxoccurs found
                        if (maxRange.compareTo(maxOccursInGroup) > 0) {
                            maxOccursInGroup = maxRange;
                        }
                    }
                    break;
                case SchemaParticle.CHOICE:
                    maxRange = getEffectiveMaxRangeChoice(particle);
                    if (maxRange != null) {
                        // keep highest maxoccurs found
                        if (maxRange.compareTo(maxOccursInGroup) > 0) {
                            maxOccursInGroup = maxRange;
                        }
                    }
                    break;
                default:
                    break;
            }
            // if an unbounded has been found then we are done
            if (maxRange == null) {
                break;
            }
        }

        // 1) unbounded if the {max occurs} of any wildcard or element declaration particle in the group's {particles} or
        // the maximum part of the effective total range of any of the group particles in the group's {particles} is
        // unbounded
        if (maxRange != null) {
            // 2) if any of those is non-zero and the {max occurs} of the particle itself is unbounded
            if (nonZeroParticleChildFound && derivedModel.getMaxOccurs() == null) {
                maxRange = null;
            } else {
                // 3) the product of the particle's {max occurs} and the sum of the {max occurs} of every wildcard or element
                // declaration particle in the group's {particles} and the maximum part of the effective total range of each of
                // the group particles in the group's {particles}
                maxRange = derivedModel.getMaxOccurs().multiply(maxOccursTotal.add(maxOccursInGroup));
            }
        }

        return maxRange;

    }

    private static BigInteger getEffectiveMinRangeChoice(SchemaParticle derivedModel) {
        // Schema Component Constraint: Effective Total Range (choice)
        // The effective total range of a particle whose {term} is a group whose {compositor} is choice is a pair of
        // minimum and maximum, as follows:
        // MINIMUM
        // The product of the particle's {min occurs}
        // and the *minimum* of the {min occurs} of every wildcard or element
        // declaration particle in the group's {particles} and the minimum part of the effective total range of each of
        // the group particles in the group's {particles} (or 0 if there are no {particles}).
        SchemaParticle[] particleChildren = derivedModel.getParticleChildren();
        if (particleChildren.length == 0) {
            return BigInteger.ZERO;
        }
        BigInteger minRange = null;
        // get the minimum of every wildcard or element
        // total up the effective total range for each group
        for (SchemaParticle particle : particleChildren) {
            switch (particle.getParticleType()) {
                case SchemaParticle.WILDCARD:
                case SchemaParticle.ELEMENT:
                    if (minRange == null || minRange.compareTo(particle.getMinOccurs()) > 0) {
                        minRange = particle.getMinOccurs();
                    }
                    break;
                case SchemaParticle.ALL:
                case SchemaParticle.SEQUENCE:
                    BigInteger mrs = getEffectiveMinRangeAllSeq(particle);
                    if (minRange == null || minRange.compareTo(mrs) > 0) {
                        minRange = mrs;
                    }
                    break;
                case SchemaParticle.CHOICE:
                    BigInteger mrc = getEffectiveMinRangeChoice(particle);
                    if (minRange == null || minRange.compareTo(mrc) > 0) {
                        minRange = mrc;
                    }
                    break;
                default:
                    break;
            }
        }
        if (minRange == null) {
            minRange = BigInteger.ZERO;
        }

        // calculate the total
        minRange = derivedModel.getMinOccurs().multiply(minRange);
        return minRange;
    }

    private static BigInteger getEffectiveMinRangeAllSeq(SchemaParticle derivedModel) {
        BigInteger minRange;
        // Schema Component Constraint: Effective Total Range (all and sequence)
        // The effective total range of a particle whose {term} is a group whose {compositor} is all or sequence is a
        // pair of minimum and maximum, as follows:
        // MINIMUM
        // The product of the particle's {min occurs}
        // and the *sum* of the {min occurs} of every wildcard or element
        // declaration particle in the group's {particles}
        // and the minimum part of the effective total range of each
        // of the group particles in the group's {particles} (or 0 if there are no {particles}).
        SchemaParticle[] particleChildren = derivedModel.getParticleChildren();
        BigInteger particleTotalMinOccurs = BigInteger.ZERO;
        for (SchemaParticle particle : particleChildren) {
            switch (particle.getParticleType()) {
                case SchemaParticle.WILDCARD:
                case SchemaParticle.ELEMENT:
                    particleTotalMinOccurs = particleTotalMinOccurs.add(particle.getMinOccurs());
                    break;
                case SchemaParticle.ALL:
                case SchemaParticle.SEQUENCE:
                    particleTotalMinOccurs = particleTotalMinOccurs.add(getEffectiveMinRangeAllSeq(particle));
                    break;
                case SchemaParticle.CHOICE:
                    particleTotalMinOccurs = particleTotalMinOccurs.add(getEffectiveMinRangeChoice(particle));
                    break;
                default:
                    break;
            }
        }

        minRange = derivedModel.getMinOccurs().multiply(particleTotalMinOccurs);

        return minRange;
    }

    private static boolean nsSubset(SchemaParticle baseModel, SchemaParticle derivedModel, Collection<XmlError> errors, XmlObject context) {
        // nsSubset is called when base: ANY, derived: ANY
        assert baseModel.getParticleType() == SchemaParticle.WILDCARD;
        assert derivedModel.getParticleType() == SchemaParticle.WILDCARD;
        boolean nsSubset;
        // For a wildcard particle to be a �valid restriction� of another wildcard particle all of the following must be true:
        // 1 R's occurrence range must be a valid restriction of B's occurrence range as defined by Occurrence Range OK (�3.9.6).
        if (occurrenceRangeOK(baseModel, derivedModel, errors, context)) {
            // 2 R's {namespace constraint} must be an intensional subset of B's {namespace constraint} as defined
            // by Wildcard Subset (�3.10.6).
            if (baseModel.getWildcardSet().inverse().isDisjoint(derivedModel.getWildcardSet())) {
                nsSubset = true;
            } else {
                nsSubset = false;
                errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_DERIVATION_NS_SUBST$WILDCARD_SUBSET,
                    new Object[]{printParticle(derivedModel), printParticle(baseModel)}, context));
            }
        } else {
            nsSubset = false;
            // error already produced by occurrenceRangeOK
            //errors.add(XmlError.forObject(formatNSIsNotSubsetError(baseModel, derivedModel), context));
        }


        return nsSubset;
    }

    private static boolean nsCompat(SchemaParticle baseModel, SchemaLocalElement derivedElement, Collection<XmlError> errors, XmlObject context) {
        // nsCompat is called when base: ANY, derived: ELEMENT
        assert baseModel.getParticleType() == SchemaParticle.WILDCARD;
        boolean nsCompat;
        // For an element declaration particle to be a �valid restriction� of a wildcard particle all of the following must be true:
        // 1 The element declaration's {target namespace} is �valid� with respect to the wildcard's {namespace constraint}
        // as defined by Wildcard allows Namespace Name (�3.10.4).
        if (baseModel.getWildcardSet().contains(derivedElement.getName())) {
            // 2 R's occurrence range is a valid restriction of B's occurrence range as defined by Occurrence Range OK (�3.9.6).
            nsCompat = occurrenceRangeOK(baseModel, (SchemaParticle) derivedElement, errors, context);
        } else {
            nsCompat = false;
            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_DERIVATION_NS_COMPAT$WILDCARD_VALID,
                new Object[]{printParticle((SchemaParticle) derivedElement), printParticle(baseModel)},
                context));
        }


        return nsCompat;
    }

    private static boolean nameAndTypeOK(SchemaLocalElement baseElement, SchemaLocalElement derivedElement, Collection<XmlError> errors, XmlObject context) {
        // nameAndTypeOK called when base: ELEMENT and derived: ELEMENT

        // Schema Component Constraint: Particle Restriction OK (Elt:Elt -- NameAndTypeOK)
        // 1 The declarations' {name}s and {target namespace}s are the same.
        if (!((SchemaParticle) baseElement).canStartWithElement(derivedElement.getName())) {
            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_RESTRICTION_NAME_AND_TYPE$NAME,
                new Object[]{printParticle((SchemaParticle) derivedElement), printParticle((SchemaParticle) baseElement)}, context));
            return false;
        }

        // 2 Either B's {nillable} is true or R's {nillable} is false.
        if (!baseElement.isNillable() && derivedElement.isNillable()) {
            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_RESTRICTION_NAME_AND_TYPE$NILLABLE,
                new Object[]{printParticle((SchemaParticle) derivedElement), printParticle((SchemaParticle) baseElement)}, context));
            return false;
        }

        // 3 R's occurrence range is a valid restriction of B's occurrence range as defined by Occurrence Range OK (�3.9.6).
        if (!occurrenceRangeOK((SchemaParticle) baseElement, (SchemaParticle) derivedElement, errors, context)) {
            // error already produced
            return false;
        }

        // 4 either B's declaration's {value constraint} is absent, or is not fixed,
        // or R's declaration's {value constraint} is fixed with the same value.
        if (!checkFixed(baseElement, derivedElement, errors, context)) {
            // error already produced
            return false;
        }

        // 5 R's declaration's {identity-constraint definitions} is a subset of B's declaration's {identity-constraint definitions}, if any.
        if (!checkIdentityConstraints(baseElement, derivedElement, errors, context)) {
            // error already produced
            return false;
        }

        // 7 R's {type definition} is validly derived given {extension, list, union} from B's {type definition} as
        // defined by Type Derivation OK (Complex) (�3.4.6) or Type Derivation OK (Simple) (�3.14.6), as appropriate.
        if (!typeDerivationOK(baseElement.getType(), derivedElement.getType(), errors, context)) {
            // error already produced
            return false;
        }

        // 6 R's declaration's {disallowed substitutions} is a superset of B's declaration's {disallowed substitutions}.
        return blockSetOK(baseElement, derivedElement, errors, context);
    }

    private static boolean blockSetOK(SchemaLocalElement baseElement, SchemaLocalElement derivedElement, Collection<XmlError> errors, XmlObject context) {
        if (baseElement.blockRestriction() && !derivedElement.blockRestriction()) {
            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_RESTRICTION_NAME_AND_TYPE$DISALLOWED_SUBSTITUTIONS,
                new Object[]{printParticle((SchemaParticle) derivedElement), "restriction", printParticle((SchemaParticle) baseElement)},
                context));
            return false;
        }
        if (baseElement.blockExtension() && !derivedElement.blockExtension()) {
            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_RESTRICTION_NAME_AND_TYPE$DISALLOWED_SUBSTITUTIONS,
                new Object[]{printParticle((SchemaParticle) derivedElement), "extension", printParticle((SchemaParticle) baseElement)},
                context));
            return false;
        }
        if (baseElement.blockSubstitution() && !derivedElement.blockSubstitution()) {
            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_RESTRICTION_NAME_AND_TYPE$DISALLOWED_SUBSTITUTIONS,
                new Object[]{printParticle((SchemaParticle) derivedElement), "substitution", printParticle((SchemaParticle) baseElement)},
                context));
            return false;
        }
        return true;
    }

    private static boolean typeDerivationOK(SchemaType baseType, SchemaType derivedType, Collection<XmlError> errors, XmlObject context) {
        boolean typeDerivationOK;
        // 1 If B and D are not the same type definition, then the {derivation method} of D must not be in the subset.
        // 2 One of the following must be true:
        // 2.1 B and D must be the same type definition.
        // 2.2 B must be D's {base type definition}.
        // 2.3 All of the following must be true:
        // 2.3.1 D's {base type definition} must not be the �ur-type definition�.
        // 2.3.2 The appropriate case among the following must be true:
        // 2.3.2.1 If D's {base type definition} is complex, then it must be validly derived from B given the subset as defined by this constraint.
        // 2.3.2.2 If D's {base type definition} is simple, then it must be validly derived from B given the subset as defined in Type Derivation OK (Simple) (�3.14.6).
        //   This line will check if derivedType is a subType of baseType (should handle all of the 2.xx checks above)
        if (baseType.isAssignableFrom(derivedType)) {
            // Ok derived type is subtype but need to make sure that all of the derivations between the two types are by
            // Restriction.
            typeDerivationOK = checkAllDerivationsForRestriction(baseType, derivedType, errors, context);
        } else {
            // derived type is not a sub-type of base type
            typeDerivationOK = false;
            errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_RESTRICTION_NAME_AND_TYPE$TYPE_VALID,
                new Object[]{printType(derivedType), printType(baseType)}, context));
        }

        return typeDerivationOK;
    }

    private static boolean checkAllDerivationsForRestriction(SchemaType baseType, SchemaType derivedType, Collection<XmlError> errors, XmlObject context) {
        boolean allDerivationsAreRestrictions = true;
        SchemaType currentType = derivedType;

        // XMLBEANS-66: if baseType is a union, check restriction is of one of the constituant types
        Set<SchemaType> possibleTypes = null;
        if (baseType.getSimpleVariety() == SchemaType.UNION) {
            possibleTypes = new HashSet<>(Arrays.asList(baseType.getUnionConstituentTypes()));
        }

        // run up the types hierarchy from derived Type to base Type and make sure that all are derived by
        //   restriction.  If any are not then this is not a valid restriction.
        while (!baseType.equals(currentType) &&
               possibleTypes != null && !possibleTypes.contains(currentType)) {
            if (currentType.getDerivationType() == SchemaType.DT_RESTRICTION) {
                currentType = currentType.getBaseType();
            } else {
                allDerivationsAreRestrictions = false;
                errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_RESTRICTION_NAME_AND_TYPE$TYPE_RESTRICTED,
                    new Object[]{printType(derivedType), printType(baseType), printType(currentType)}, context));
                break;
            }
        }
        return allDerivationsAreRestrictions;
    }

    private static boolean checkIdentityConstraints(SchemaLocalElement baseElement, SchemaLocalElement derivedElement, Collection<XmlError> errors, XmlObject context) {
        // 5 R's declaration's {identity-constraint definitions} is a subset of B's declaration's {identity-constraint definitions}, if any.
        boolean identityConstraintsOK = true;

        SchemaIdentityConstraint[] baseConstraints = baseElement.getIdentityConstraints();
        SchemaIdentityConstraint[] derivedConstraints = derivedElement.getIdentityConstraints();
        // cycle thru derived's identity constraints and check each to assure they in the array of base constraints
        for (SchemaIdentityConstraint derivedConstraint : derivedConstraints) {
            if (checkForIdentityConstraintExistence(baseConstraints, derivedConstraint)) {
                identityConstraintsOK = false;
                errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_RESTRICTION_NAME_AND_TYPE$IDENTITY_CONSTRAINTS,
                    new Object[]{printParticle((SchemaParticle) derivedElement), printParticle((SchemaParticle) baseElement)},
                    context));
                break;
            }
        }
        return identityConstraintsOK;
    }

    private static boolean checkForIdentityConstraintExistence(SchemaIdentityConstraint[] baseConstraints, SchemaIdentityConstraint derivedConstraint) {
        // spin thru the base identity constraints check to see if derived constraint exists
        boolean identityConstraintExists = false;
        for (SchemaIdentityConstraint baseConstraint : baseConstraints) {
            if (baseConstraint.getName().equals(derivedConstraint.getName())) {
                identityConstraintExists = true;
                break;
            }
        }
        return identityConstraintExists;
    }


    private static boolean checkFixed(SchemaLocalElement baseModel, SchemaLocalElement derivedModel, Collection<XmlError> errors, XmlObject context) {
        // 4 either B's declaration's {value constraint} is absent, or is not fixed,
        // or R's declaration's {value constraint} is fixed with the same value.
        boolean checkFixed;
        if (baseModel.isFixed()) {
            if (baseModel.getDefaultText().equals(derivedModel.getDefaultText())) {
                //  R's declaration's {value constraint} is fixed with the same value.
                checkFixed = true;
            } else {
                // The derived element has a fixed value that is different than the base element
                errors.add(XmlError.forObject(XmlErrorCodes.PARTICLE_RESTRICTION_NAME_AND_TYPE$FIXED,
                    new Object[]{printParticle((SchemaParticle) derivedModel), derivedModel.getDefaultText(),
                        printParticle((SchemaParticle) baseModel), baseModel.getDefaultText()},
                    context));
                checkFixed = false;
            }
        } else {
            //  B's declaration's {value constraint} is absent, or is not fixed,
            checkFixed = true;
        }
        return checkFixed;
    }

    private static boolean occurrenceRangeOK(SchemaParticle baseParticle, SchemaParticle derivedParticle, Collection<XmlError> errors, XmlObject context) {
        boolean occurrenceRangeOK;
        // Note: in the following comments (from the schema spec) other is the baseModel
        // 1 Its {min occurs} is greater than or equal to the other's {min occurs}.
        if (derivedParticle.getMinOccurs().compareTo(baseParticle.getMinOccurs()) >= 0) {
            // 2 one of the following must be true:
            // 2.1 The other's {max occurs} is unbounded.
            if (baseParticle.getMaxOccurs() == null) {
                occurrenceRangeOK = true;
            } else {
                // 2.2 Both {max occurs} are numbers, and the particle's is less than or equal to the other's.
                if (derivedParticle.getMaxOccurs() != null && baseParticle.getMaxOccurs() != null &&
                    derivedParticle.getMaxOccurs().compareTo(baseParticle.getMaxOccurs()) <= 0) {
                    occurrenceRangeOK = true;
                } else {
                    occurrenceRangeOK = false;
                    errors.add(XmlError.forObject(XmlErrorCodes.OCCURRENCE_RANGE$MAX_LTE_MAX,
                        new Object[]{printParticle(derivedParticle), printMaxOccurs(derivedParticle.getMaxOccurs()),
                            printParticle(baseParticle), printMaxOccurs(baseParticle.getMaxOccurs())},
                        context));
                }
            }
        } else {
            occurrenceRangeOK = false;
            errors.add(XmlError.forObject(XmlErrorCodes.OCCURRENCE_RANGE$MIN_GTE_MIN,
                new Object[]{printParticle(derivedParticle), derivedParticle.getMinOccurs().toString(),
                    printParticle(baseParticle), baseParticle.getMinOccurs().toString()},
                context));
        }
        return occurrenceRangeOK;
    }

    private static String printParticles(List<SchemaParticle> parts) {
        return printParticles(parts.toArray(new SchemaParticle[0]));
    }

    private static String printParticles(SchemaParticle[] parts) {
        return printParticles(parts, 0, parts.length);
    }

    private static String printParticles(SchemaParticle[] parts, int start) {
        return printParticles(parts, start, parts.length);
    }

    private static String printParticles(SchemaParticle[] parts, int start, int end) {
        StringBuilder buf = new StringBuilder(parts.length * 30);
        for (int i = start; i < end; ) {
            buf.append(printParticle(parts[i]));
            if (++i != end) {
                buf.append(", ");
            }
        }
        return buf.toString();
    }

    private static String printParticle(SchemaParticle part) {
        switch (part.getParticleType()) {
            case SchemaParticle.ALL:
                return "<all>";
            case SchemaParticle.CHOICE:
                return "<choice>";
            case SchemaParticle.ELEMENT:
                return "<element name=\"" + QNameHelper.pretty(part.getName()) + "\">";
            case SchemaParticle.SEQUENCE:
                return "<sequence>";
            case SchemaParticle.WILDCARD:
                return "<any>";
            default:
                return "??";
        }
    }

    private static String printMaxOccurs(BigInteger bi) {
        if (bi == null) {
            return "unbounded";
        }
        return bi.toString();
    }

    private static String printType(SchemaType type) {
        if (type.getName() != null) {
            return QNameHelper.pretty(type.getName());
        }
        return type.toString();
    }

    private static void checkSubstitutionGroups(SchemaGlobalElement[] elts) {
        StscState state = StscState.get();

        for (SchemaGlobalElement elt : elts) {
            SchemaGlobalElement head = elt.substitutionGroup();

            if (head != null) {
                SchemaType headType = head.getType();
                SchemaType tailType = elt.getType();
                XmlObject parseTree = ((SchemaGlobalElementImpl) elt)._parseObject;

                if (!headType.isAssignableFrom(tailType)) {
                    state.error(XmlErrorCodes.ELEM_PROPERTIES$SUBSTITUTION_VALID,
                        new Object[]{QNameHelper.pretty(elt.getName()),
                            QNameHelper.pretty(head.getName())},
                        parseTree);
                } else if (head.finalExtension() && head.finalRestriction()) {
                    state.error(XmlErrorCodes.ELEM_PROPERTIES$SUBSTITUTION_FINAL,
                        new Object[]{QNameHelper.pretty(elt.getName()),
                            QNameHelper.pretty(head.getName()),
                            "#all"}, parseTree);
                } else if (!headType.equals(tailType)) {
                    if (head.finalExtension() &&
                        tailType.getDerivationType() == SchemaType.DT_EXTENSION) {
                        state.error(XmlErrorCodes.ELEM_PROPERTIES$SUBSTITUTION_FINAL,
                            new Object[]{QNameHelper.pretty(elt.getName()),
                                QNameHelper.pretty(head.getName()),
                                "extension"}, parseTree);
                    } else if (head.finalRestriction() &&
                               tailType.getDerivationType() == SchemaType.DT_RESTRICTION) {
                        state.error(XmlErrorCodes.ELEM_PROPERTIES$SUBSTITUTION_FINAL,
                            new Object[]{QNameHelper.pretty(elt.getName()),
                                QNameHelper.pretty(head.getName()),
                                "restriction"}, parseTree);
                    }
                }
            }

        }
    }
}
