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

import org.apache.xmlbeans.*;
import org.apache.xmlbeans.impl.common.*;
import org.apache.xmlbeans.impl.schema.SchemaTypeImpl;
import org.apache.xmlbeans.impl.schema.SchemaTypeVisitorImpl;
import org.apache.xmlbeans.impl.util.XsTypeConverter;
import org.apache.xmlbeans.impl.values.*;

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

public final class Validator
    implements ValidatorListener {
    public Validator(
        SchemaType type, SchemaField field, SchemaTypeLoader globalLoader,
        XmlOptions options, Collection<XmlError> defaultErrorListener) {
        options = XmlOptions.maskNull(options);
        _errorListener = options.getErrorListener();
        _treatLaxAsSkip = options.isValidateTreatLaxAsSkip();
        _strict = options.isValidateStrict();

        if (_errorListener == null) {
            _errorListener = defaultErrorListener;
        }

        _constraintEngine = new IdentityConstraint(_errorListener, type.isDocumentType());

        _globalTypes = globalLoader;
        _rootType = type;
        _rootField = field;

        _vc = new ValidatorVC();
    }

    private class ValidatorVC implements ValidationContext {
        // KHK: remove this
        public void invalid(String message) {
            // TODO (dutta) Addtional Attributes for validation error have limited information
            //at this time but will be a part of the second round of refactoring

            Validator.this.emitError(_event, message, null, null,
                XmlValidationError.ATTRIBUTE_TYPE_INVALID);
        }

        public void invalid(String code, Object[] args) {
            // TODO (dutta) Addtional Attributes for validation error have limited information
            //at this time but will be a part of the second round of refactoring

            Validator.this.emitError(_event, code, args, null, null,
                XmlValidationError.ATTRIBUTE_TYPE_INVALID, null);
        }

        Event _event;
    }

    public boolean isValid() {
        return !_invalid && _constraintEngine.isValid();
    }

    // KHK: remove this
    private void emitError(Event event, String message, QName offendingQName,
                           SchemaType expectedSchemaType,
                           int errorType) {
        emitError(event, message, null, null, XmlError.SEVERITY_ERROR, null, offendingQName, expectedSchemaType,
            null, errorType, null);
    }

    private void emitError(Event event, String code, Object[] args, QName offendingQName,
                           SchemaType expectedSchemaType,
                           int errorType, SchemaType badSchemaType) {
        emitError(event, null, code, args, XmlError.SEVERITY_ERROR, null, offendingQName, expectedSchemaType,
            null, errorType, badSchemaType);
    }

    // KHK: remove 'message' parameter
    private void emitError(Event event, String message, String code, Object[] args, int severity,
                           QName fieldName, QName offendingQName,
                           SchemaType expectedSchemaType, List<QName> expectedQNames,
                           int errorType, SchemaType badSchemaType) {
        _errorState++;

        if (_suspendErrors == 0) {
            if (severity == XmlError.SEVERITY_ERROR) {
                _invalid = true;
            }

            if (_errorListener != null) {
                assert event != null;
                XmlError error;
                XmlCursor curs = event.getLocationAsCursor();
                if (curs != null) {
                    // non-streaming validation uses XmlCursor
                    error = XmlValidationError.forCursorWithDetails(message, code, args, severity,
                        curs, fieldName, offendingQName, expectedSchemaType, expectedQNames,
                        errorType, badSchemaType);
                } else {
                    // streaming validation uses Location
                    error = XmlValidationError.forLocationWithDetails(message, code, args, severity,
                        event.getLocation(), fieldName, offendingQName, expectedSchemaType, expectedQNames,
                        errorType, badSchemaType);
                }

                _errorListener.add(error);
            }
        }
    }

    private void emitFieldError(Event event, String code, Object[] args, QName offendingQName,
                                SchemaType expectedSchemaType, List<QName> expectedQNames,
                                int errorType, SchemaType badSchemaType) {
        QName fieldName = null;
        if (_stateStack != null && _stateStack._field != null) {
            fieldName = _stateStack._field.getName();
        }

        Validator.this.emitError(event, null, code, args, XmlError.SEVERITY_ERROR, fieldName, offendingQName, expectedSchemaType,
            expectedQNames, errorType, badSchemaType);
    }

    public void nextEvent(int kind, Event event) {
        resetValues();

        if (_eatContent > 0) {
            switch (kind) {
                case END:
                    _eatContent--;
                    break;
                case BEGIN:
                    _eatContent++;
                    break;
            }
        } else {
            assert
                kind == BEGIN || kind == ATTR ||
                kind == END || kind == TEXT || kind == ENDATTRS;

            switch (kind) {
                case BEGIN:
                    beginEvent(event);
                    break;
                case ATTR:
                    attrEvent(event);
                    break;
                case ENDATTRS:
                    endAttrsEvent(event);
                    break;
                case TEXT:
                    textEvent(event);
                    break;
                case END:
                    endEvent(event);
                    break;
            }
        }
    }

    private void beginEvent(Event event) {
        _localElement = null;
        _wildcardElement = null;
        State state = topState();

        SchemaType elementType;
        SchemaField elementField;

        if (state == null) {
            elementType = _rootType;
            elementField = _rootField;
        } else {

            QName name = event.getName();

            assert name != null;

            state._isEmpty = false;

            if (state._isNil) {
                emitFieldError(event, XmlErrorCodes.ELEM_LOCALLY_VALID$NIL_WITH_CONTENT,
                    null, state._field.getName(), state._type, null,
                    XmlValidationError.NIL_ELEMENT, state._type);

                _eatContent = 1;
                return;
            }

            if (state._field != null && state._field.isFixed()) {
                emitFieldError(event, XmlErrorCodes.ELEM_LOCALLY_VALID$FIXED_WITH_CONTENT,
                    new Object[]{QNameHelper.pretty(state._field.getName())},
                    state._field.getName(), state._type, null,
                    XmlValidationError.ELEMENT_NOT_ALLOWED, state._type);
            }

            if (!state.visit(name)) {
                findDetailedErrorBegin(event, state, name);

                _eatContent = 1;

                return;
            }

            SchemaParticle currentParticle = state.currentParticle();
            _wildcardElement = currentParticle;

            if (currentParticle.getParticleType() == SchemaParticle.WILDCARD) {
                //_wildcardElement = currentParticle;
                QNameSet elemWildcardSet = currentParticle.getWildcardSet();

                if (!elemWildcardSet.contains(name)) {
                    // Additional processing may be needed to generate more
                    // descriptive messages
                    // KHK: cvc-complex-type.2.4? cvc-particle.1.3? cvc-wildcard-namespace ?
                    emitFieldError(event, XmlErrorCodes.PARTICLE_VALID$NOT_WILDCARD_VALID,
                        new Object[]{QNameHelper.pretty(name)},
                        name, null, null,
                        XmlValidationError.ELEMENT_NOT_ALLOWED, state._type);

                    _eatContent = 1;

                    return;
                }

                int wildcardProcess = currentParticle.getWildcardProcess();

                if (wildcardProcess == SchemaParticle.SKIP ||
                    wildcardProcess == SchemaParticle.LAX && _treatLaxAsSkip) {
                    _eatContent = 1;
                    return;
                }

                _localElement = _globalTypes.findElement(name);
                elementField = _localElement;

                if (elementField == null) {
                    if (wildcardProcess == SchemaParticle.STRICT) {
                        // KHK: cvc-complex-type.2.4c? cvc-assess-elt.1.1.1.3.2?
                        emitFieldError(event, XmlErrorCodes.ASSESS_ELEM_SCHEMA_VALID$NOT_RESOLVED,
                            new Object[]{QNameHelper.pretty(name)},
                            name, state._type, null,
                            XmlValidationError.ELEMENT_NOT_ALLOWED, state._type);
                    }

                    _eatContent = 1;

                    return;
                }
            } else {
                assert currentParticle.getParticleType() == SchemaParticle.ELEMENT;

                // If the current element particle name does not match the name
                // of the event, then the current element is a substitute for
                // the current particle. Replace the field with the global
                // element for the replacement

                if (!currentParticle.getName().equals(name)) {
                    if (((SchemaLocalElement) currentParticle).blockSubstitution()) {
                        emitFieldError(event, XmlErrorCodes.PARTICLE_VALID$BLOCK_SUBSTITUTION,
                            new Object[]{QNameHelper.pretty(name)},
                            name, state._type, null,
                            XmlValidationError.ELEMENT_NOT_ALLOWED, state._type);

                        _eatContent = 1;
                        return;
                    }

                    SchemaGlobalElement newField = _globalTypes.findElement(name);

                    assert newField != null;

                    elementField = newField;
                    _localElement = newField;
                } else {
                    elementField = (SchemaField) currentParticle;
                }
            }

            elementType = elementField.getType();
        }

        assert elementType != null;

        //
        // the no-type is always invalid (even if there is an xsi:type)
        //

        if (elementType.isNoType()) {
            emitFieldError(event, XmlErrorCodes.ELEM_LOCALLY_VALID$NO_TYPE,
                null, event.getName(), null, null,
                XmlValidationError.ELEMENT_TYPE_INVALID, null);

            _eatContent = 1;
        }

        //
        // See if the element has an xsi:type on it
        //

        SchemaType xsiType = null;

        String value = event.getXsiType();

        if (value != null) {
            // Turn off the listener so a public error message
            // does not get generated, but I can see if there was
            // an error through the error state

            int originalErrorState = _errorState;

            _suspendErrors++;

            try {
                _vc._event = null;

                xsiType = _globalTypes.findType(XmlQNameImpl.validateLexical(value, _vc, event));
            } catch (Throwable t) {
                _errorState++;
            } finally {
                _suspendErrors--;
            }

            if (originalErrorState != _errorState) {
                // not sure how to extract this one
                emitFieldError(event, XmlErrorCodes.ELEM_LOCALLY_VALID$XSI_TYPE_INVALID_QNAME,
                    new Object[]{value}, event.getName(), xsiType, null,
                    XmlValidationError.ELEMENT_TYPE_INVALID, (state == null ? null : state._type));

                _eatContent = 1;

                return;
            } else if (xsiType == null) {
                // NOT SURE errorAttributes._expectedSchemaType = xsiType;
                emitFieldError(event, XmlErrorCodes.ELEM_LOCALLY_VALID$XSI_TYPE_NOT_FOUND,
                    new Object[]{value}, event.getName(), null, null,
                    XmlValidationError.ELEMENT_TYPE_INVALID, null);

                _eatContent = 1;

                return;
            }
        }

        if (xsiType != null && !xsiType.equals(elementType)) {
            if (!elementType.isAssignableFrom(xsiType)) {
                emitFieldError(event, XmlErrorCodes.ELEM_LOCALLY_VALID$XSI_TYPE_NOT_DERIVED,
                    new Object[]{xsiType, elementType}, event.getName(), elementType, null,
                    XmlValidationError.ELEMENT_TYPE_INVALID, (state == null ? null : state._type));

                _eatContent = 1;

                return;
            }

            if (elementType.blockExtension()) {
                for (SchemaType t = xsiType; !t.equals(elementType);
                     t = t.getBaseType()) {
                    if (t.getDerivationType() == SchemaType.DT_EXTENSION) {
                        emitFieldError(event, XmlErrorCodes.ELEM_LOCALLY_VALID$XSI_TYPE_BLOCK_EXTENSION,
                            new Object[]{xsiType, elementType}, event.getName(), elementType, null,
                            XmlValidationError.ELEMENT_TYPE_INVALID, (state == null ? null : state._type));

                        _eatContent = 1;

                        return;
                    }
                }
            }

            if (elementType.blockRestriction()) {
                for (SchemaType t = xsiType; !t.equals(elementType);
                     t = t.getBaseType()) {
                    if (t.getDerivationType() == SchemaType.DT_RESTRICTION) {
                        emitFieldError(event, XmlErrorCodes.ELEM_LOCALLY_VALID$XSI_TYPE_BLOCK_RESTRICTION,
                            new Object[]{xsiType, elementType}, event.getName(), elementType, null,
                            XmlValidationError.ELEMENT_TYPE_INVALID, (state == null ? null : state._type));

                        _eatContent = 1;

                        return;
                    }
                }
            }

            if (elementField instanceof SchemaLocalElement) {
                SchemaLocalElement sle = (SchemaLocalElement) elementField;
                _localElement = sle;

                if (sle.blockExtension() || sle.blockRestriction()) {
                    for (SchemaType t = xsiType; !t.equals(elementType);
                         t = t.getBaseType()) {
                        if ((t.getDerivationType() == SchemaType.DT_RESTRICTION && sle.blockRestriction()) ||
                            (t.getDerivationType() == SchemaType.DT_EXTENSION && sle.blockExtension())) {
                            //need to find a way to get the right type
                            emitFieldError(event, XmlErrorCodes.ELEM_LOCALLY_VALID$XSI_TYPE_PROHIBITED_SUBST,
                                new Object[]{xsiType, QNameHelper.pretty(sle.getName())},
                                sle.getName(), null, null, XmlValidationError.ELEMENT_TYPE_INVALID, null);

                            _eatContent = 1;

                            return;
                        }
                    }
                }
            }

            elementType = xsiType;
        }

        if (elementField instanceof SchemaLocalElement) {
            SchemaLocalElement sle = (SchemaLocalElement) elementField;
            _localElement = sle;

            if (sle.isAbstract()) {
                //todo (dutta) need to find a way to get the right type
                emitError(event, XmlErrorCodes.ELEM_LOCALLY_VALID$ABSTRACT,
                    new Object[]{QNameHelper.pretty(sle.getName())},
                    sle.getName(), null, XmlValidationError.ELEMENT_TYPE_INVALID, null);

                _eatContent = 1;
                return;
            }
        }

        if (elementType.isAbstract()) {
            emitError(event, XmlErrorCodes.ELEM_LOCALLY_VALID$ABSTRACT,
                new Object[]{elementType},
                event.getName(), elementType, XmlValidationError.ELEMENT_TYPE_INVALID, (state == null ? null : state._type));

            _eatContent = 1;

            return;
        }

        boolean isNil = false;
        boolean hasNil = false;

        String nilValue = event.getXsiNil();

        if (nilValue != null) {
            _vc._event = event;
            isNil = JavaBooleanHolder.validateLexical(nilValue, _vc);
            hasNil = true;
        }

        // note in schema spec 3.3.4, you're not even allowed to say xsi:nil="false" if you're not nillable!
        if (hasNil && (elementField == null || !elementField.isNillable())) {
            emitFieldError(event, XmlErrorCodes.ELEM_LOCALLY_VALID$NOT_NILLABLE, null,
                elementField == null ? null : elementField.getName(), elementType, null,
                XmlValidationError.ELEMENT_TYPE_INVALID, (state == null ? null : state._type));

            _eatContent = 1;
            return;
        }

        if (isNil && elementField != null && elementField.isFixed()) {
            emitFieldError(event, XmlErrorCodes.ELEM_LOCALLY_VALID$NIL_WITH_FIXED, null,
                elementField.getName(), elementType, null,
                XmlValidationError.ELEMENT_TYPE_INVALID, (state == null ? null : state._type));
        }

        newState(elementType, elementField, isNil);

        // Dispatch this element event to any identity constraints
        // As well as adding any new identity constraints that exist

        _constraintEngine.element(
            event,
            elementType,
            elementField instanceof SchemaLocalElement
                ? ((SchemaLocalElement) elementField).getIdentityConstraints()
                : null);
    }

    private void attrEvent(Event event) {
        QName attrName = event.getName();

        State state = topState();

        if (state._attrs == null) {
            state._attrs = new HashSet<>();
        }

        if (state._attrs.contains(attrName)) {
            emitFieldError(event, XmlErrorCodes.XML_DUPLICATE_ATTRIBUTE,
                new Object[]{QNameHelper.pretty(attrName)},
                attrName, null, null, XmlValidationError.INCORRECT_ATTRIBUTE, state._type);

            return;
        }

        state._attrs.add(attrName);

        if (!state._canHaveAttrs) {
            emitFieldError(event, XmlErrorCodes.ELEM_COMPLEX_TYPE_LOCALLY_VALID$NO_WILDCARD,
                new Object[]{QNameHelper.pretty(attrName)}, attrName, null, null,
                XmlValidationError.INCORRECT_ATTRIBUTE, state._type);
            return;
        }

        SchemaLocalAttribute attrSchema =
            state._attrModel == null
                ? null
                : state._attrModel.getAttribute(attrName);

        if (attrSchema != null) {
            _localAttribute = attrSchema;

            if (attrSchema.getUse() == SchemaLocalAttribute.PROHIBITED) {
                emitFieldError(event, XmlErrorCodes.ELEM_COMPLEX_TYPE_LOCALLY_VALID$PROHIBITED_ATTRIBUTE,
                    new Object[]{QNameHelper.pretty(attrName)},
                    attrName, null, null, XmlValidationError.INCORRECT_ATTRIBUTE, state._type);

                return;
            }

            String value =
                validateSimpleType(
                    attrSchema.getType(), attrSchema, event, false, false);

            _constraintEngine.attr(event, attrName, attrSchema.getType(), value);

            return;
        }

        int wildcardProcess = state._attrModel.getWildcardProcess();

        _wildcardAttribute = state._attrModel;

        if (wildcardProcess == SchemaAttributeModel.NONE) {
            // todo (dutta) need additional logic to determine the expectedSchemaType
            emitFieldError(event, XmlErrorCodes.ELEM_COMPLEX_TYPE_LOCALLY_VALID$NO_WILDCARD,
                new Object[]{QNameHelper.pretty(attrName)},
                attrName, null, null, XmlValidationError.INCORRECT_ATTRIBUTE, state._type);

            return;
        }

        QNameSet attrWildcardSet = state._attrModel.getWildcardSet();

        if (!attrWildcardSet.contains(attrName)) {
            // todo (dutta) need additional logic to determine the expectedSchemaType
            emitFieldError(event, XmlErrorCodes.ELEM_COMPLEX_TYPE_LOCALLY_VALID$NOT_WILDCARD_VALID,
                new Object[]{QNameHelper.pretty(attrName)},
                attrName, null, null, XmlValidationError.INCORRECT_ATTRIBUTE, state._type);

            return;
        }

        if (wildcardProcess == SchemaAttributeModel.SKIP ||
            wildcardProcess == SchemaAttributeModel.LAX && _treatLaxAsSkip) {
            return;
        }

        attrSchema = _globalTypes.findAttribute(attrName);
        _localAttribute = attrSchema;

        if (attrSchema == null) {
            if (wildcardProcess == SchemaAttributeModel.LAX) {
                return;
            }

            assert wildcardProcess == SchemaAttributeModel.STRICT;

            // KHK: cvc-assess-attr.1.2 ?
            // todo (dutta) need additional logic to determine the expectedSchemaType
            emitFieldError(event, XmlErrorCodes.ASSESS_ATTR_SCHEMA_VALID$NOT_RESOLVED,
                new Object[]{QNameHelper.pretty(attrName)},
                attrName, null, null, XmlValidationError.INCORRECT_ATTRIBUTE, state._type);

            return;
        }

        String value =
            validateSimpleType(
                attrSchema.getType(), attrSchema, event, false, false);

        _constraintEngine.attr(event, attrName, attrSchema.getType(), value);
    }

    private void endAttrsEvent(Event event) {
        State state = topState();

        if (state._attrModel != null) {
            SchemaLocalAttribute[] attrs = state._attrModel.getAttributes();

            for (SchemaLocalAttribute sla : attrs) {
                if (state._attrs == null ||
                    !state._attrs.contains(sla.getName())) {
                    if (sla.getUse() == SchemaLocalAttribute.REQUIRED) {
                        // KHK: cvc-complex-type.4
                        emitFieldError(event, XmlErrorCodes.ELEM_COMPLEX_TYPE_LOCALLY_VALID$MISSING_REQUIRED_ATTRIBUTE,
                            new Object[]{QNameHelper.pretty(sla.getName())},
                            sla.getName(), null, null, XmlValidationError.INCORRECT_ATTRIBUTE, state._type);
                    } else if (sla.isDefault() || sla.isFixed()) {
                        _constraintEngine.attr(event, sla.getName(), sla.getType(), sla.getDefaultText());

                        // We don't need to validate attribute defaults because this is done at compiletime.
                        /*
                        String value = sla.getDefaultText();
                        SchemaType type = sla.getType();

                        if (XmlQName.type.isAssignableFrom(type))
                        {
                            emitFieldError(
                                event,
                                "Default QName values are unsupported for attribute: " +
                                    QNameHelper.pretty(sla.getName()),
                                XmlError.SEVERITY_INFO);
                        }

                        else
                        {
                            validateSimpleType(
                                type, sla.getDefaultText(), event );

                            _constraintEngine.attr( event, type, value );
                        }
                        */
                    }
                }
            }
        }
    }

    private void endEvent(Event event) {
        _localElement = null;
        _wildcardElement = null;
        State state = topState();

        if (!state._isNil) {
            if (!state.end()) {
                findDetailedErrorEnd(event, state);
            }

            // This end event has no text, use this fact to pass no text to
            // handleText

            if (state._isEmpty) {
                handleText(event, true, state._field);
            }
        }

        popState(event);

        _constraintEngine.endElement(event);
    }

    private void textEvent(Event event) {
        State state = topState();

        if (state._isNil) {
            emitFieldError(event, XmlErrorCodes.ELEM_LOCALLY_VALID$NIL_WITH_CONTENT, null,
                state._field.getName(), state._type, null,
                XmlValidationError.NIL_ELEMENT, state._type);
        } else {
            handleText(event, false, state._field);
        }

        state._isEmpty = false;
    }


    private void handleText(
        Event event, boolean emptyContent, SchemaField field) {
        State state = topState();

        if (!state._sawText) {
            if (state._hasSimpleContent) {
                String value =
                    validateSimpleType(
                        state._type, field, event, emptyContent, true);

                _constraintEngine.text(event, state._type, value, false);
            } else if (state._canHaveMixedContent) {
                // handles cvc-elt.5.2.2.2.1, checking mixed content against fixed.
                // if we see <mixedType>a</b>c</mixedType>, we validate against
                // the first 'a' text and we check the content of mixedType to
                // be empty in beginElem().  we don't care about checking against
                // the 'c' text since there will already be an error for <b/>
                String value =
                    validateSimpleType(
                        XmlString.type, field, event, emptyContent, true);

                _constraintEngine.text(event, XmlString.type, value, false);
            } else if (emptyContent) {
                _constraintEngine.text(event, state._type, null, true);
            } else {
                _constraintEngine.text(event, state._type, "", false);
            }
        }

        if (!emptyContent && !state._canHaveMixedContent &&
            !event.textIsWhitespace() && !state._hasSimpleContent) {
            if (field instanceof SchemaLocalElement) {
                SchemaLocalElement e = (SchemaLocalElement) field;

                assert state._type.getContentType() == SchemaType.EMPTY_CONTENT ||
                       state._type.getContentType() == SchemaType.ELEMENT_CONTENT;

                // KHK: cvc-complex-type.2.1 or .2.3
                String errorCode = (state._type.getContentType() == SchemaType.EMPTY_CONTENT ?
                    XmlErrorCodes.ELEM_COMPLEX_TYPE_LOCALLY_VALID$EMPTY_WITH_CONTENT :
                    XmlErrorCodes.ELEM_COMPLEX_TYPE_LOCALLY_VALID$ELEMENT_ONLY_WITH_TEXT);

                emitError(event, errorCode, new Object[]{QNameHelper.pretty(e.getName())},
                    e.getName(), field.getType(),
                    XmlValidationError.ELEMENT_TYPE_INVALID, null);
            } else {
                // KHK: cvc-complex-type.2.1 or .2.3
                // todo (dutta) offendingQName = not sure how to get this(event.getName()??);
                emitError(event, "Can't have mixed content", event.getName(),
                    state._type, XmlValidationError.ELEMENT_TYPE_INVALID);
            }
        }

        if (!emptyContent) {
            state._sawText = true;
        }
    }

    private void findDetailedErrorBegin(Event event, State state, QName qName) {
        ArrayList<QName> expectedNames = new ArrayList<>();
        ArrayList<QName> optionalNames = new ArrayList<>();

        SchemaProperty[] eltProperties = state._type.getElementProperties();
        for (SchemaProperty sProp : eltProperties) {
            //Get the element from the schema
            // test if the element is valid
            if (state.test(sProp.getName())) {
                if (0 == BigInteger.ZERO.compareTo(sProp.getMinOccurs())) {
                    optionalNames.add(sProp.getName());
                } else {
                    expectedNames.add(sProp.getName());
                }
            }
        }

        List<QName> names = (expectedNames.size() > 0 ? expectedNames : optionalNames);

        if (names.size() > 0) {
            String buf = names.stream().map(QNameHelper::pretty).collect(Collectors.joining(" "));
            emitFieldError(event, XmlErrorCodes.ELEM_COMPLEX_TYPE_LOCALLY_VALID$EXPECTED_DIFFERENT_ELEMENT,
                new Object[]{names.size(), buf, QNameHelper.pretty(qName)},
                qName, null, names, XmlValidationError.INCORRECT_ELEMENT, state._type);
        } else {
            emitFieldError(event, XmlErrorCodes.ELEM_COMPLEX_TYPE_LOCALLY_VALID$ELEMENT_NOT_ALLOWED,
                new Object[]{QNameHelper.pretty(qName)},
                qName, null, null, XmlValidationError.INCORRECT_ELEMENT, state._type);
        }
    }

    private void findDetailedErrorEnd(Event event, State state) {
        SchemaProperty[] eltProperties = state._type.getElementProperties();

        ArrayList<QName> expectedNames = new ArrayList<>();
        ArrayList<QName> optionalNames = new ArrayList<>();

        for (SchemaProperty sProp : eltProperties) {
            //Get the element from the schema
            // test if the element is valid
            if (state.test(sProp.getName())) {
                if (0 == BigInteger.ZERO.compareTo(sProp.getMinOccurs())) {
                    optionalNames.add(sProp.getName());
                } else {
                    expectedNames.add(sProp.getName());
                }
            }
        }

        List<QName> names = (expectedNames.size() > 0 ? expectedNames : optionalNames);

        if (names.size() > 0) {
            String buf = names.stream().map(QNameHelper::pretty).collect(Collectors.joining(" "));

            emitFieldError(event, XmlErrorCodes.ELEM_COMPLEX_TYPE_LOCALLY_VALID$MISSING_ELEMENT,
                new Object[]{names.size(), buf},
                null, null, names, XmlValidationError.INCORRECT_ELEMENT, state._type);
        } else {
            emitFieldError(event, XmlErrorCodes.ELEM_COMPLEX_TYPE_LOCALLY_VALID$EXPECTED_ELEMENT,
                null, null, null, null, XmlValidationError.ELEMENT_NOT_ALLOWED, state._type);
        }
    }


    private static final class State {
        boolean visit(QName name) {
            return _canHaveElements && _visitor.visit(name);
        }

        boolean test(QName name) {
            return _canHaveElements && _visitor.testValid(name);
        }

        boolean end() {
            return !_canHaveElements || _visitor.visit(null);
        }

        SchemaParticle currentParticle() {
            assert _visitor != null;
            return _visitor.currentParticle();
        }

        SchemaType _type;
        SchemaField _field;

        boolean _canHaveAttrs;
        boolean _canHaveMixedContent;
        boolean _hasSimpleContent;

        boolean _sawText;
        boolean _isEmpty;
        boolean _isNil;

        SchemaTypeVisitorImpl _visitor;
        boolean _canHaveElements;

        SchemaAttributeModel _attrModel;

        HashSet<QName> _attrs;

        State _next;
    }

    private boolean derivedFromInteger(SchemaType type) {
        int btc = type.getBuiltinTypeCode();

        while (btc == SchemaType.BTC_NOT_BUILTIN) {
            type = type.getBaseType();
            btc = type.getBuiltinTypeCode();
        }
        // This depends on the ordering of the constant values, which is not ideal but is easier
        return btc >= SchemaType.BTC_INTEGER && btc <= SchemaType.BTC_UNSIGNED_BYTE;
    }

    private void newState(SchemaType type, SchemaField field, boolean isNil) {
        State state = new State();

        state._type = type;
        state._field = field;
        state._isEmpty = true;
        state._isNil = isNil;

        if (type.isSimpleType()) {
            state._hasSimpleContent = true;
        } else {
            state._canHaveAttrs = true;
            state._attrModel = type.getAttributeModel();

            switch (type.getContentType()) {
                case SchemaType.EMPTY_CONTENT:
                    break;

                case SchemaType.SIMPLE_CONTENT:
                    state._hasSimpleContent = true;
                    break;

                case SchemaType.MIXED_CONTENT:
                    state._canHaveMixedContent = true;
                    // Fall through

                case SchemaType.ELEMENT_CONTENT:

                    SchemaParticle particle = type.getContentModel();

                    state._canHaveElements = particle != null;

                    if (state._canHaveElements) {
                        state._visitor = initVisitor(particle);
                    }

                    break;

                default:
                    throw new RuntimeException("Unexpected content type");
            }
        }

        pushState(state);
    }

    private void popState(Event e) {
        if (_stateStack._visitor != null) {
            poolVisitor(_stateStack._visitor);
            _stateStack._visitor = null;
        }

        _stateStack = _stateStack._next;
    }

    private void pushState(State state) {
        state._next = _stateStack;
        _stateStack = state;
    }

    private final LinkedList<TypeStoreVisitor> _visitorPool = new LinkedList<>();

    private void poolVisitor(SchemaTypeVisitorImpl visitor) {
        _visitorPool.add(visitor);
    }

    private SchemaTypeVisitorImpl initVisitor(SchemaParticle particle) {
        if (_visitorPool.isEmpty()) {
            return new SchemaTypeVisitorImpl(particle);
        }

        SchemaTypeVisitorImpl result =
            (SchemaTypeVisitorImpl) _visitorPool.removeLast();

        result.init(particle);

        return result;
    }

    private State topState() {
        return _stateStack;
    }

    //
    // Simple Type Validation
    //
    // emptyContent means that you can't use the event to get text: there is
    // no text, but you can use the event to do prefix resolution (in the case
    // where the default is a qname)
    //

    private String validateSimpleType(
        SchemaType type, SchemaField field, Event event,
        boolean emptyContent, boolean canApplyDefault) {
        if (!type.isSimpleType() &&
            type.getContentType() != SchemaType.SIMPLE_CONTENT) {
            assert false;
            // throw new RuntimeException( "Not a simple type" );
            return null; // should never happen
        }

        //
        // the no-type is always invalid
        //

        if (type.isNoType()) {
            emitError(event, (field.isAttribute() ? XmlErrorCodes.ATTR_LOCALLY_VALID$NO_TYPE : XmlErrorCodes.ELEM_LOCALLY_VALID$NO_TYPE),
                null, field.getName(), type, XmlValidationError.ELEMENT_TYPE_INVALID, null);

            return null;
        }

        // Get the value as a string (as normalized by the white space rule
        // TODO - will want to optimize this later

        String value = "";

        if (!emptyContent) {
            int wsr = type.getWhiteSpaceRule();
            value = wsr == SchemaType.WS_PRESERVE ? event.getText() : event.getText(wsr);
        }

        // See if I can apply a default/fixed value

        if (value.length() == 0 && canApplyDefault && field != null &&
            (field.isDefault() || field.isFixed())) {
            if (XmlQName.type.isAssignableFrom(type)) {
                // TODO: will be fixed in XmlSchema 1.1
                emitError(event, "Default QName values are unsupported for " +
                                 QNameHelper.readable(type) + " - ignoring.", null, null,
                    XmlError.SEVERITY_INFO, field.getName(), null, type, null,
                    XmlValidationError.ELEMENT_TYPE_INVALID, null);

                return null;
            }

            String defaultValue =
                XmlWhitespace.collapse(
                    field.getDefaultText(), type.getWhiteSpaceRule());

// BUGBUG - should validate defaultValue at compile time
            // KHK: cvc-elt.5.1.2 ?
            return
                validateSimpleType(type, defaultValue, event)
                    ? defaultValue
                    : null;
        }

        // KHK: cvc-elt.5.2.1 ?
        if (!validateSimpleType(type, value, event)) {
            return null;
        }

        if (field != null && field.isFixed()) {
// TODO - fixed value should have been cooked at compile time
            String fixedValue =
                XmlWhitespace.collapse(
                    field.getDefaultText(), type.getWhiteSpaceRule());

            if (!validateSimpleType(type, fixedValue, event)) {
                return null;
            }

            XmlObject val = type.newValue(value);
            XmlObject def = type.newValue(fixedValue);

            if (!val.valueEquals(def)) {
                // TODO (dutta) - make this more verbose
                if (field.isAttribute()) {
                    // KHK: check for is cvc-complex-type.3.1 or cvc-au
                    emitError(event, XmlErrorCodes.ATTR_LOCALLY_VALID$FIXED,
                        new Object[]{value, fixedValue, QNameHelper.pretty(event.getName())},
                        null, field.getType(), XmlValidationError.ELEMENT_TYPE_INVALID, null);
                } else {
                    String errorCode = null;

                    // see rule 5 of cvc-elt: Element Locally Valid (Element)
                    if (field.getType().getContentType() == SchemaType.MIXED_CONTENT) {
                        errorCode = XmlErrorCodes.ELEM_LOCALLY_VALID$FIXED_VALID_MIXED_CONTENT;
                    } else if (type.isSimpleType()) {
                        errorCode = XmlErrorCodes.ELEM_LOCALLY_VALID$FIXED_VALID_SIMPLE_TYPE;
                    } else {
                        assert false : "Element with fixed may not be EMPTY or ELEMENT_ONLY";
                    }

                    emitError(event, errorCode, new Object[]{value, fixedValue},
                        field.getName(), field.getType(), XmlValidationError.ELEMENT_TYPE_INVALID, null);
                }

                return null;
            }
        }

        return value;
    }

    private boolean validateSimpleType(
        SchemaType type, String value, Event event) {
        if (!type.isSimpleType() &&
            type.getContentType() != SchemaType.SIMPLE_CONTENT) {
            assert false;
            throw new RuntimeException("Not a simple type");
        }

        int retState = _errorState;

        switch (type.getSimpleVariety()) {
            case SchemaType.ATOMIC:
                validateAtomicType(type, value, event);
                break;
            case SchemaType.UNION:
                validateUnionType(type, value, event);
                break;
            case SchemaType.LIST:
                validateListType(type, value, event);
                break;

            default:
                throw new RuntimeException("Unexpected simple variety");
        }

        return retState == _errorState;
    }

    private void validateAtomicType(
        SchemaType type, String value, Event event) {
        // Now we should have only an atomic type to validate

        assert type.getSimpleVariety() == SchemaType.ATOMIC;

        // Record the current error state to see if any new errors are made
        int errorState = _errorState;
        _vc._event = event;

        switch (type.getPrimitiveType().getBuiltinTypeCode()) {
            case SchemaType.BTC_ANY_SIMPLE: {
                // Always valid!
                _stringValue = value;
                break;
            }
            case SchemaType.BTC_STRING: {
                JavaStringEnumerationHolderEx.validateLexical(value, type, _vc);
                _stringValue = value;
                break;
            }
            case SchemaType.BTC_DECIMAL: {
                JavaDecimalHolderEx.validateLexical(value, type, _vc);

                // An additional rule states that if the type is xs:integer or derived from it,
                // then the decimal dot is not allowed.
                // verify that values extending xsd:integer don't have a decimal point
                if (derivedFromInteger(type) && value.lastIndexOf('.') >= 0) {
                    _vc.invalid(XmlErrorCodes.INTEGER, new Object[]{value});
                }

                if (errorState == _errorState) {
                    _decimalValue = new BigDecimal(value);
                    JavaDecimalHolderEx.validateValue(_decimalValue, type, _vc);
                }

                break;
            }
            case SchemaType.BTC_BOOLEAN: {
                _booleanValue = JavaBooleanHolderEx.validateLexical(value, type, _vc);
                break;
            }
            case SchemaType.BTC_FLOAT: {
                float f =
                    JavaFloatHolderEx.validateLexical(value, type, _vc);

                if (errorState == _errorState) {
                    JavaFloatHolderEx.validateValue(f, type, _vc);
                }

                _floatValue = f;
                break;
            }
            case SchemaType.BTC_DOUBLE: {
                double d =
                    JavaDoubleHolderEx.validateLexical(value, type, _vc);

                if (errorState == _errorState) {
                    JavaDoubleHolderEx.validateValue(d, type, _vc);
                }

                _doubleValue = d;
                break;
            }
            case SchemaType.BTC_QNAME: {
                QName n =
                    JavaQNameHolderEx.validateLexical(
                        value, type, _vc, event);

                if (errorState == _errorState) {
                    JavaQNameHolderEx.validateValue(n, type, _vc);
                }

                _qnameValue = n;
                break;
            }
            case SchemaType.BTC_ANY_URI: {
                JavaUriHolderEx.validateLexical(value, type, _vc);
                // Do strict validation
                if (_strict) {
                    try {
                        XsTypeConverter.lexAnyURI(value);
                    } catch (InvalidLexicalValueException ilve) {
                        _vc.invalid(XmlErrorCodes.ANYURI, new Object[]{value});
                    }
                }
                _stringValue = value;
                break;
            }
            case SchemaType.BTC_G_MONTH: {
                // In the case of gMonth, there is some strict mode validation to do
                if (_strict && value.length() == 6 &&
                    value.charAt(4) == '-' && value.charAt(5) == '-') {
                    _vc.invalid(XmlErrorCodes.DATE, new Object[]{value});
                }
                // Fall through
            }
            case SchemaType.BTC_DATE_TIME:
            case SchemaType.BTC_TIME:
            case SchemaType.BTC_DATE:
            case SchemaType.BTC_G_YEAR_MONTH:
            case SchemaType.BTC_G_YEAR:
            case SchemaType.BTC_G_MONTH_DAY:
            case SchemaType.BTC_G_DAY: {
                GDate d = XmlDateImpl.validateLexical(value, type, _vc);

                if (d != null) {
                    XmlDateImpl.validateValue(d, type, _vc);
                }

                _gdateValue = d;
                break;
            }
            case SchemaType.BTC_DURATION: {
                GDuration d = XmlDurationImpl.validateLexical(value, type, _vc);

                if (d != null) {
                    XmlDurationImpl.validateValue(d, type, _vc);
                }

                _gdurationValue = d;
                break;
            }
            case SchemaType.BTC_BASE_64_BINARY: {
                byte[] v =
                    JavaBase64HolderEx.validateLexical(value, type, _vc);

                if (v != null) {
                    JavaBase64HolderEx.validateValue(v, type, _vc);
                }

                _byteArrayValue = v;
                break;
            }
            case SchemaType.BTC_HEX_BINARY: {
                byte[] v =
                    JavaHexBinaryHolderEx.validateLexical(value, type, _vc);

                if (v != null) {
                    JavaHexBinaryHolderEx.validateValue(v, type, _vc);
                }

                _byteArrayValue = v;
                break;
            }
            case SchemaType.BTC_NOTATION: {
                QName n =
                    JavaNotationHolderEx.validateLexical(
                        value, type, _vc, event);

                if (errorState == _errorState) {
                    JavaNotationHolderEx.validateValue(n, type, _vc);
                }

                _qnameValue = n;
                break;
            }

            default:
                throw new RuntimeException("Unexpected primitive type code");
        }
    }

    private void validateListType(
        SchemaType type, String value, Event event) {
        int errorState = _errorState;

        if (!type.matchPatternFacet(value)) {
            emitError(event, XmlErrorCodes.DATATYPE_VALID$PATTERN_VALID,
                new Object[]{"list", value, QNameHelper.readable(type)},
                null, type, XmlValidationError.LIST_INVALID, null);
        }

        String[] items = XmlListImpl.split_list(value);

        int i;
        XmlObject o;

        if ((o = type.getFacet(SchemaType.FACET_LENGTH)) != null) {
            if ((i = ((SimpleValue) o).getIntValue()) != items.length) {
                //offending Qname not valid
                emitError(event, XmlErrorCodes.DATATYPE_LENGTH_VALID$LIST_LENGTH,
                    new Object[]{value, items.length, i, QNameHelper.readable(type)},
                    null, type, XmlValidationError.LIST_INVALID, null);
            }
        }

        if ((o = type.getFacet(SchemaType.FACET_MIN_LENGTH)) != null) {
            if ((i = ((SimpleValue) o).getIntValue()) > items.length) {
                //offending Qname not valid
                emitError(event, XmlErrorCodes.DATATYPE_LENGTH_VALID$LIST_LENGTH,
                    new Object[]{value, items.length, i, QNameHelper.readable(type)},
                    null, type, XmlValidationError.LIST_INVALID, null);
            }
        }

        if ((o = type.getFacet(SchemaType.FACET_MAX_LENGTH)) != null) {
            if ((i = ((SimpleValue) o).getIntValue()) < items.length) {
                //offending Qname not valid
                emitError(event, XmlErrorCodes.DATATYPE_LENGTH_VALID$LIST_LENGTH,
                    new Object[]{value, items.length, i, QNameHelper.readable(type)},
                    null, type, XmlValidationError.LIST_INVALID, null);
            }
        }

        SchemaType itemType = type.getListItemType();
        _listValue = new ArrayList<>();
        _listTypes = new ArrayList<>();

        for (i = 0; i < items.length; i++) {
            validateSimpleType(
                itemType, items[i], event);
            addToList(itemType);
        }

        // If no errors up to this point, then I can create an
        // XmlList from this value and campare it again enums.

        if (errorState == _errorState) {
            if (type.getEnumerationValues() != null) {
                // Lists which contain QNames will need a resolver

                NamespaceContext.push(
                    new NamespaceContext(event));

                try {
                    XmlObject listValue = ((SchemaTypeImpl) type).newValidatingValue(value);
                } catch (XmlValueOutOfRangeException e) {
                    //offending Qname not valid ??
                    emitError(event, XmlErrorCodes.DATATYPE_ENUM_VALID,
                        new Object[]{"list", value, QNameHelper.readable(type)},
                        null, type, XmlValidationError.LIST_INVALID, null);
                } finally {
                    NamespaceContext.pop();
                }
            }
        }
    }

    private void validateUnionType(
        SchemaType type, String value, Event event) {
        // TODO - if xsi:type is specified on a union, it selects
        // that union member type

        if (!type.matchPatternFacet(value)) {
            //offending Qname not valid ??
            emitError(event, XmlErrorCodes.DATATYPE_VALID$PATTERN_VALID,
                new Object[]{"union", value, QNameHelper.readable(type)},
                null, type, XmlValidationError.UNION_INVALID, null);
        }

        int currentWsr = SchemaType.WS_PRESERVE;
        String currentValue = value;

        SchemaType[] types = type.getUnionMemberTypes();

        int originalState = _errorState;

        int i;
        for (i = 0; i < types.length; i++) {
            int memberWsr = types[i].getWhiteSpaceRule();

            if (memberWsr == SchemaType.WS_UNSPECIFIED) {
                memberWsr = SchemaType.WS_PRESERVE;
            }

            if (memberWsr != currentWsr) {
                currentWsr = memberWsr;
                currentValue = XmlWhitespace.collapse(value, currentWsr);
            }

            int originalErrorState = _errorState;

            _suspendErrors++;

            try {
                validateSimpleType(types[i], currentValue, event);
            } finally {
                _suspendErrors--;
            }

            if (originalErrorState == _errorState) {
                _unionType = types[i];
                break;
            }
        }

        _errorState = originalState;

        if (i >= types.length) {
            //offending Qname not valid ??
            emitError(event, XmlErrorCodes.DATATYPE_VALID$UNION,
                new Object[]{value, QNameHelper.readable(type)},
                null, type, XmlValidationError.UNION_INVALID, null);
        } else {
            XmlObject[] unionEnumvals = type.getEnumerationValues();

            if (unionEnumvals != null) {
                // Unions which contain QNames will need a resolver

                NamespaceContext.push(new NamespaceContext(event));

                try {
                    XmlObject unionValue = type.newValue(value);

                    for (i = 0; i < unionEnumvals.length; i++) {
                        if (unionValue.valueEquals(unionEnumvals[i])) {
                            break;
                        }
                    }

                    if (i >= unionEnumvals.length) {
                        //offending Qname not valid ??
                        emitError(event, XmlErrorCodes.DATATYPE_ENUM_VALID,
                            new Object[]{"union", value, QNameHelper.readable(type)},
                            null, type, XmlValidationError.UNION_INVALID, null);
                    }
                } catch (XmlValueOutOfRangeException e) {
                    // actually, the current union code always ends up here when invalid

                    //offending Qname not valid ??
                    emitError(event, XmlErrorCodes.DATATYPE_ENUM_VALID,
                        new Object[]{"union", value, QNameHelper.readable(type)},
                        null, type, XmlValidationError.UNION_INVALID, null);
                } finally {
                    NamespaceContext.pop();
                }
            }
        }
    }

    private void addToList(SchemaType type) {
        if (type.getSimpleVariety() != SchemaType.ATOMIC &&
            type.getSimpleVariety() != SchemaType.UNION) {
            return;
        }

        if (type.getUnionMemberTypes().length > 0 && getUnionType() != null) {
            type = getUnionType();
            _unionType = null;
        }

        _listTypes.add(type);

        if (type.getPrimitiveType() == null) {
            // instance has an error for this value so there is no primitive type.
            // an error should already have been produced.
            _listValue.add(null);
            return;
        }

        switch (type.getPrimitiveType().getBuiltinTypeCode()) {
            case SchemaType.BTC_ANY_SIMPLE:
            case SchemaType.BTC_ANY_URI: {
                _listValue.add(_stringValue);
                break;
            }
            case SchemaType.BTC_STRING: {
                _listValue.add(_stringValue);
                _stringValue = null;
                break;
            }
            case SchemaType.BTC_DECIMAL: {
                _listValue.add(_decimalValue);
                _decimalValue = null;
                break;
            }
            case SchemaType.BTC_BOOLEAN: {
                _listValue.add(_booleanValue ? Boolean.TRUE : Boolean.FALSE);
                _booleanValue = false;
                break;
            }
            case SchemaType.BTC_FLOAT: {
                _listValue.add(_floatValue);
                _floatValue = 0;
                break;
            }
            case SchemaType.BTC_DOUBLE: {
                _listValue.add(_doubleValue);
                _doubleValue = 0;
                break;
            }
            case SchemaType.BTC_QNAME:
            case SchemaType.BTC_NOTATION: {
                _listValue.add(_qnameValue);
                _qnameValue = null;
                break;
            }
            case SchemaType.BTC_DATE_TIME:
            case SchemaType.BTC_TIME:
            case SchemaType.BTC_DATE:
            case SchemaType.BTC_G_YEAR_MONTH:
            case SchemaType.BTC_G_YEAR:
            case SchemaType.BTC_G_MONTH_DAY:
            case SchemaType.BTC_G_DAY:
            case SchemaType.BTC_G_MONTH: {
                _listValue.add(_gdateValue);
                _gdateValue = null;
                break;
            }
            case SchemaType.BTC_DURATION: {
                _listValue.add(_gdurationValue);
                _gdurationValue = null;
                break;
            }
            case SchemaType.BTC_BASE_64_BINARY:
            case SchemaType.BTC_HEX_BINARY: {
                _listValue.add(_byteArrayValue);
                _byteArrayValue = null;
                break;
            }

            default:
                throw new RuntimeException("Unexpected primitive type code");
        }
    }

    //
    // Members of the validator class
    //

    private boolean _invalid;
    private final SchemaType _rootType;
    private final SchemaField _rootField;
    private final SchemaTypeLoader _globalTypes;
    private State _stateStack;
    private int _errorState;
    private Collection<XmlError> _errorListener;
    private final boolean _treatLaxAsSkip;
    private final boolean _strict;
    private final ValidatorVC _vc;
    private int _suspendErrors;
    private final IdentityConstraint _constraintEngine;
    private int _eatContent;

    private SchemaLocalElement _localElement;
    private SchemaParticle _wildcardElement;
    private SchemaLocalAttribute _localAttribute;
    private SchemaAttributeModel _wildcardAttribute;
    private SchemaType _unionType;

    // Strongly typed values
    private String _stringValue;
    private BigDecimal _decimalValue;
    private boolean _booleanValue;
    private float _floatValue;
    private double _doubleValue;
    private QName _qnameValue;
    private GDate _gdateValue;
    private GDuration _gdurationValue;
    private byte[] _byteArrayValue;
    private List<Object> _listValue;
    private List<SchemaType> _listTypes;

    private void resetValues() {
        _localAttribute = null;
        _wildcardAttribute = null;
        _stringValue = null;
        _decimalValue = null;
        _booleanValue = false;
        _floatValue = 0;
        _doubleValue = 0;
        _qnameValue = null;
        _gdateValue = null;
        _gdurationValue = null;
        _byteArrayValue = null;
        _listValue = null;
        _listTypes = null;
        _unionType = null;
    }

    /**
     * @return Returns the SchemaType of the current element.
     * This can be different than getCurrentElement().getType() if xsi:type attribute is used.
     * Null is returned if no schema type is available.
     * For attribute types use {@link #getCurrentAttribute()}.getType().
     * Warning: the returned SchemaType can be an {@link org.apache.xmlbeans.XmlBeans#NO_TYPE},
     * see {@link SchemaType#isNoType}. Or can be the parent type, for unrecognized elements
     * that are part of wildcards.
     */
    public SchemaType getCurrentElementSchemaType() {
        State state = topState();
        if (state != null) {
            return state._type;
        }

        return null;
    }

    /**
     * @return Returns the curent local element, null if one is not available, see {@link #getCurrentWildcardElement()}.
     */
    public SchemaLocalElement getCurrentElement() {
        if (_localElement != null) {
            return _localElement;
        }

        // it means the element is to be skiped and it doesn't have a known SchemaLocalElement

        if (_eatContent > 0) {
            return null;
        }

        //try getting it from the stack (this should happen after END)

        if (_stateStack != null && _stateStack._field instanceof SchemaLocalElement) {
            return (SchemaLocalElement) _stateStack._field;
        }

        return null;
    }

    /**
     * @return Returns the current particle, if this is a wildcard particle {@link SchemaParticle#WILDCARD}
     * method {@link #getCurrentElement()} might return null if wildcard's processContents is skip or lax.
     */
    public SchemaParticle getCurrentWildcardElement() {
        return _wildcardElement;
    }

    /**
     * @return Returns the curent local attribute, global attribute if the current attribute is part of an
     * attribute wildcard, or null if none is available.
     */
    public SchemaLocalAttribute getCurrentAttribute() {
        return _localAttribute;
    }

    /**
     * @return Returns the attribute model for attributes if available, else null is returned.
     */
    public SchemaAttributeModel getCurrentWildcardAttribute() {
        return _wildcardAttribute;
    }

    public String getStringValue() {
        return _stringValue;
    }

    public BigDecimal getDecimalValue() {
        return _decimalValue;
    }

    public boolean getBooleanValue() {
        return _booleanValue;
    }

    public float getFloatValue() {
        return _floatValue;
    }

    public double getDoubleValue() {
        return _doubleValue;
    }

    public QName getQNameValue() {
        return _qnameValue;
    }

    public GDate getGDateValue() {
        return _gdateValue;
    }

    public GDuration getGDurationValue() {
        return _gdurationValue;
    }

    public byte[] getByteArrayValue() {
        return _byteArrayValue;
    }

    public List<Object> getListValue() {
        return _listValue;
    }

    public List<SchemaType> getListTypes() {
        return _listTypes;
    }

    public SchemaType getUnionType() {
        return _unionType;
    }
}
