/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.sdo.helper;

import commonj.sdo.DataObject;
import commonj.sdo.Property;
import commonj.sdo.Type;
import commonj.sdo.helper.DataFactory;
import commonj.sdo.helper.HelperContext;
import java.lang.reflect.Modifier;
import java.util.Stack;
import javax.xml.namespace.QName;
import org.eclipse.persistence.sdo.SDOConstants;
import org.eclipse.persistence.sdo.SDODataObject;
import org.eclipse.persistence.sdo.SDOProperty;
import org.eclipse.persistence.sdo.SDOType;
import org.eclipse.persistence.sdo.SDOXMLDocument;
import org.eclipse.persistence.exceptions.DescriptorException;
import org.eclipse.persistence.exceptions.XMLMarshalException;
import org.eclipse.persistence.internal.oxm.StrBuffer;
import org.eclipse.persistence.internal.oxm.TreeObjectBuilder;
import org.eclipse.persistence.internal.oxm.record.XMLRecord;
import org.eclipse.persistence.internal.oxm.record.namespaces.UnmarshalNamespaceResolver;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.oxm.XMLConstants;
import org.eclipse.persistence.oxm.XMLDescriptor;
import org.eclipse.persistence.oxm.record.UnmarshalRecord;
import org.eclipse.persistence.oxm.unmapped.UnmappedContentHandler;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;

/**
 * <p><b>Purpose</b>: Called during XMLHelper load methods when there is unknown content in an XML document.  This
 * means when there is no corresponding SDO Type found.
 *
 * @see commonj.sdo.helper.XMLHelper
 */
public class SDOUnmappedContentHandler implements UnmappedContentHandler {
    private UnmarshalRecord parentRecord;
    private SDOXMLDocument xmlDocument;
    private QName currentSchemaType;
    private StrBuffer currentBuffer;
    private Stack currentDataObjects;
    private Stack currentProperties;
    private boolean rootProcessed;
    private boolean isInCharacterBlock;
    private HelperContext aHelperContext;
    private int lastEvent = -1;
    private static final int START_ELEMENT = 0;
    private static final int END_ELEMENT = 1;
    private UnmarshalNamespaceResolver unmarshalNamespaceResolver;
    private static final String NO_NAMESPACE = null;
    private int depth = 0;

    public SDOUnmappedContentHandler() {
        isInCharacterBlock = false;
        currentDataObjects = new Stack();
        currentProperties = new Stack();
        currentBuffer = new StrBuffer();
    }

    public void setDocumentLocator(Locator locator) {
    }

    public void startDocument() throws SAXException {
    }

    public void endDocument() throws SAXException {
    }

    public void startPrefixMapping(String prefix, String uri) throws SAXException {
        unmarshalNamespaceResolver.push(prefix, uri);
    }

    public void endPrefixMapping(String prefix) throws SAXException {
        unmarshalNamespaceResolver.pop(prefix);
    }

    public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
        if (isInCharacterBlock) {
            if (!currentBuffer.toString().trim().equals("")) {
                DataObject dObj = (DataObject) currentDataObjects.peek();
                dObj.getSequence().addText(currentBuffer.toString());
            }
            currentBuffer.reset();
        }
        if (!rootProcessed) {
            processRoot(namespaceURI, localName, qName, atts);
        } else {
            processNonRoot(namespaceURI, localName, qName, atts);
        }
        lastEvent = START_ELEMENT;
    }

    /**
     * Return a QName representing the value of the xsi:type attribute or null if one is not present
     */
    private QName getTypeAttributeQName(Attributes atts) {
        int attributeSize = atts.getLength();
        for (int i = 0; i < attributeSize; i++) {
            String stringValue = atts.getValue(i);
            String uri = atts.getURI(i);
            String attrName = atts.getLocalName(i);
            if (javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI.equals(uri) && XMLConstants.SCHEMA_TYPE_ATTRIBUTE.equals(attrName)) {
                int colonIndex = stringValue.indexOf(':');
                String localPrefix = stringValue.substring(0, colonIndex);
                String localURI = unmarshalNamespaceResolver.getNamespaceURI(localPrefix);
                if (localURI != null) {
                    String localName = stringValue.substring(colonIndex + 1, stringValue.length());
                    QName theQName = new QName(localURI, localName);
                    currentSchemaType = theQName;
                    return theQName;
                } else {
                    throw XMLMarshalException.namespaceNotFound(localPrefix);
                }
            }
        }

        return null;
    }

    private void processAttributes(Attributes atts, DataObject dataObject, boolean isRoot) {
        int attributeSize = atts.getLength();
        for (int i = 0; i < attributeSize; i++) {
            String stringValue = atts.getValue(i);
            String uri = atts.getURI(i);
            String attrName = atts.getLocalName(i);

            if ((atts.getQName(i) != null) && atts.getQName(i).startsWith(javax.xml.XMLConstants.XMLNS_ATTRIBUTE + ":")) {
                //namespace declaration - do nothing because namespaces were already handled
            } else if (isRoot && XMLConstants.SCHEMA_LOCATION.equals(attrName)) {
                getXmlDocument().setSchemaLocation(stringValue);
            } else if (isRoot && XMLConstants.NO_NS_SCHEMA_LOCATION.equals(attrName)) {
                getXmlDocument().setNoNamespaceSchemaLocation(stringValue);
            } else if (javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI.equals(uri) && XMLConstants.SCHEMA_TYPE_ATTRIBUTE.equals(attrName)) {
                //do nothing
            } else if (SDOConstants.CHANGESUMMARY_REF.equals(attrName) && SDOConstants.SDO_URL.equals(uri)) {
                ((SDODataObject)dataObject)._setSdoRef(stringValue);
            } else {
                HelperContext aHelperContext = ((SDOType)dataObject.getType()).getHelperContext();
                Property prop = aHelperContext.getXSDHelper().getGlobalProperty(uri, attrName, false);
                if (prop != null) {
                    Object convertedValue = ((SDODataHelper)aHelperContext.getDataHelper()).convertFromStringValue(stringValue, prop.getType());
                    dataObject.set(prop, convertedValue);
                } else {
                    Object convertedValue = ((SDODataHelper)aHelperContext.getDataHelper()).convertFromStringValue(stringValue, SDOConstants.SDO_STRING);

                    //can't use create on demand property via a set by string name operation because that would create an element
                    prop = defineNewSDOProperty(uri, attrName, false, SDOConstants.SDO_STRING);
                    dataObject.set(prop, convertedValue);
                }
            }
        }
    }

    public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
    }

    public void processingInstruction(String target, String data) throws SAXException {
    }

    public void skippedEntity(String name) throws SAXException {
    }

    public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
        if ((currentDataObjects.size() == 0) && (currentProperties.size() == 0)) {
            return;
        }

        if ((currentDataObjects.size() == 1) && (currentProperties.size() == 0)) {
            parentRecord.getUnmarshaller().getUnmarshalListener().afterUnmarshal(currentDataObjects.peek(), null);
            currentDataObjects.pop();
            depth--;
            return;
        } else {
            setElementPropertyValue();
            return;
        }
    }

    public void characters(char[] ch, int start, int length) throws SAXException {
        if (!isInCharacterBlock) {
            currentBuffer.reset();
            isInCharacterBlock = true;
        }
        currentBuffer.append(ch, start, length);
    }

    private void setElementPropertyValue() {
        Property currentProperty = (Property)currentProperties.pop();
        boolean simple = true;

        if (lastEvent == END_ELEMENT) {
            simple = false;
        } else {
            //last event was start element so is usually a simple thing
            //if depth is greater than the stack size it means dataType == true
            if (depth > currentDataObjects.size()) {
                simple = true;
            } else {
                //if depth and stack size are the same it means complex or simple.
                DataObject nextDO = (DataObject)currentDataObjects.peek();

                if (nextDO.getInstanceProperties().size() > 0) {
                    simple = false;
                    if (!currentBuffer.toString().trim().equals("")) {
                        DataObject dObj = (DataObject) currentDataObjects.peek();
                        dObj.getSequence().addText(currentBuffer.toString());
                    }
                } else {
                    currentDataObjects.pop();
                }
            }
            depth--;
        }

        lastEvent = END_ELEMENT;
        if (simple && (!isInCharacterBlock || (currentBuffer.length() == 0))) {
            return;
        }

        DataObject currentDataObject = (DataObject)currentDataObjects.peek();

        if (currentProperty != null) {
            Object value = null;
            if (simple) {
                value = currentBuffer.toString();
                ((SDOProperty)currentProperty).setType(SDOConstants.SDO_STRING);
                ((SDOProperty)currentProperty).setContainment(false);
            } else {
                value = currentDataObject;
                currentDataObjects.pop();
                depth--;
                if (currentDataObjects.isEmpty()) {
                    currentDataObject = null;
                } else {
                    currentDataObject = (DataObject)currentDataObjects.peek();
                }
            }
            HelperContext aHelperContext = ((SDOType)currentDataObject.getType()).getHelperContext();
            if (currentSchemaType != null) {
                Type sdoType = ((SDOTypeHelper)aHelperContext.getTypeHelper()).getSDOTypeFromXSDType(currentSchemaType);

                if (sdoType != null) {
                    ((SDOProperty)currentProperty).setType(sdoType);
                }

                if ((currentProperty.getType() != null) && simple) {
                    value = ((SDODataHelper)aHelperContext.getDataHelper()).convertFromStringValue((String)value, currentProperty.getType(), currentSchemaType);
                }
                currentSchemaType = null;
            } else if ((currentProperty.getType() != null) && ((SDOType)currentProperty.getType()).isDataType()) {
                value = ((SDODataHelper)aHelperContext.getDataHelper()).convertFromStringValue((String)value, currentProperty.getType());
            }

            if (currentDataObject != null) {
                if (!simple) {
                    parentRecord.getUnmarshaller().getUnmarshalListener().afterUnmarshal(value, currentDataObject);
                }

                //newly defined Properties will be many=true
                if (currentProperty.isMany()) {
                    currentDataObject.getList(currentProperty).add(value);
                } else {
                    currentDataObject.set(currentProperty, value);
                }
            }
            currentBuffer.reset();
        }
    }

    private void processNonRoot(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
        DataObject owner = (DataObject)currentDataObjects.peek();

        if ((owner != null) && !owner.getType().isOpen()) {
            return;
        }
        Property globalProperty = aHelperContext.getXSDHelper().getGlobalProperty(namespaceURI, localName, true);
        if (globalProperty != null) {
            currentProperties.push(globalProperty);
            SDOType theType = ((SDOType)globalProperty.getType());

            if (((SDOType)globalProperty.getType()).isDataType()) {
                depth++;
            } else {
                XMLDescriptor xmlDescriptor = theType.getXmlDescriptor();
                giveToOXToProcess(namespaceURI, localName, qName, atts, xmlDescriptor);
            }
        } else {
            String typeName = localName;
            String typeUri = namespaceURI;
            QName typeAttribute = getTypeAttributeQName(atts);
            Type newType = null;
            if (typeAttribute != null) {
                typeName = typeAttribute.getLocalPart();
                typeUri = typeAttribute.getNamespaceURI();
                newType = aHelperContext.getTypeHelper().getType(typeUri, typeName);
            }
            if (newType == null) {
                newType = aHelperContext.getTypeHelper().getType(SDOConstants.ORACLE_SDO_URL, "OpenSequencedType");
                Type dataObjectType = aHelperContext.getTypeHelper().getType(SDOConstants.SDO_URL, "DataObject");
                Property property = defineNewSDOProperty(namespaceURI, localName, true, dataObjectType);
                DataObject newDO = aHelperContext.getDataFactory().create(newType);
                processAttributes(atts, newDO, false);
                currentDataObjects.push(newDO);
                depth++;
                parentRecord.setCurrentObject(newDO);

                currentProperties.push(property);
            } else {
                //this means that type is a known type which will have a descriptor
                XMLDescriptor xmlDescriptor = ((SDOType)newType).getXmlDescriptor();
                giveToOXToProcess(namespaceURI, localName, qName, atts, xmlDescriptor);
                Property property = defineNewSDOProperty(namespaceURI, localName, true, newType);
                currentProperties.push(property);
                return;
            }
        }
    }

    private void processRoot(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
        DataFactory dataFactory = aHelperContext.getDataFactory();
        SDOTypeHelper typeHelper = (SDOTypeHelper)aHelperContext.getTypeHelper();

        getXmlDocument().setRootElementName(localName);
        getXmlDocument().setRootElementURI(namespaceURI);
        Property rootElementProperty = aHelperContext.getXSDHelper().getGlobalProperty(namespaceURI, localName, true);
        Type rootObjectType = null;

        if (rootElementProperty != null) {
            rootObjectType = rootElementProperty.getType();
        } else {
            QName typeQName = getTypeAttributeQName(atts);
            if (typeQName != null) {
                String typeName = typeQName.getLocalPart();
                String typeUri = null;
                String prefix = typeQName.getPrefix();
                if (prefix == null) {
                    typeUri = null;
                } else {
                    typeUri = unmarshalNamespaceResolver.getNamespaceURI(prefix);
                }
            rootObjectType = typeHelper.getType(typeUri, typeName);
            }
        }

        DataObject rootObject = null;
        if (rootObjectType != null) {
            giveToOXToProcess(namespaceURI, localName, qName, atts, ((SDOType)rootObjectType).getXmlDescriptor());
            return;
        } else {
            Type rootType = aHelperContext.getTypeHelper().getType(SDOConstants.ORACLE_SDO_URL, "OpenSequencedType");
            rootObject = dataFactory.create(rootType);
        }
        currentDataObjects.push(rootObject);
        depth++;
        processAttributes(atts, rootObject, true);

        getXmlDocument().setRootObject(rootObject);
        rootProcessed = true;
        parentRecord.setCurrentObject(getXmlDocument());
    }

    private SDOProperty defineNewSDOProperty(String uri, String localName, boolean isElement, Type type) {
        DataObject currentDataObject = (DataObject)currentDataObjects.peek();

        if ((uri != null) && uri.equals("")) {
            uri = NO_NAMESPACE;
        }

        //Check if the current DataObject has a property by this name and if so return it,
        //otherwise create a new property
        SDOProperty lookedUp = (SDOProperty)currentDataObject.getInstanceProperty(localName);
        if ((lookedUp != null) && equalStrings(lookedUp.getUri(), uri)) {
            if (isElement && aHelperContext.getXSDHelper().isElement(lookedUp)) {
                return lookedUp;
            }
            if (!isElement && aHelperContext.getXSDHelper().isAttribute(lookedUp)) {
                return lookedUp;
            }
        }

        //This Property will not be registered with XSDHelper or TypeHelper
        SDOProperty property = new SDOProperty(aHelperContext);
        property.setName(localName);
        property.setMany(isElement);
        property.setContainment(!((SDOType)type).isDataType());
        property.setType(type);
        property.setUri(uri);
        property.setInstanceProperty(SDOConstants.XMLELEMENT_PROPERTY, isElement);

        return property;
    }

    private void giveToOXToProcess(String namespaceURI, String localName, String qName, Attributes atts, XMLDescriptor xmlDescriptor) throws SAXException {
        AbstractSession session = ((SDOXMLHelper)aHelperContext.getXMLHelper()).getXmlContext().getReadSession(xmlDescriptor);

        //taken from SAXUnmarshallerHandler start Element
        UnmarshalRecord unmarshalRecord;
        if (xmlDescriptor.hasInheritance()) {
            unmarshalRecord = new UnmarshalRecord((TreeObjectBuilder)xmlDescriptor.getObjectBuilder());
            unmarshalRecord.setUnmarshalNamespaceResolver(unmarshalNamespaceResolver);
            unmarshalRecord.setAttributes(atts);
            if(parentRecord != null){
                unmarshalRecord.setXMLReader(parentRecord.getXMLReader());
            }
            Class classValue = xmlDescriptor.getInheritancePolicy().classFromRow(unmarshalRecord, session);
            if (classValue == null) {
                // no xsi:type attribute - look for type indicator on the default root element
                QName leafElementType = xmlDescriptor.getDefaultRootElementType();

                // if we have a user-set type, try to get the class from the inheritance policy
                if (leafElementType != null) {
                    Object indicator = xmlDescriptor.getInheritancePolicy().getClassIndicatorMapping().get(leafElementType);
                    if(indicator != null) {
                        classValue = (Class)indicator;
                    }
                }
            }
            if (classValue != null) {
                xmlDescriptor = (XMLDescriptor)session.getDescriptor(classValue);
            } else {
                // since there is no xsi:type attribute, we'll use the descriptor
                // that was retrieved based on the rootQName -  we need to make
                // sure it is non-abstract
                if (Modifier.isAbstract(xmlDescriptor.getJavaClass().getModifiers())) {
                    // need to throw an exception here
                    throw DescriptorException.missingClassIndicatorField((XMLRecord) unmarshalRecord, xmlDescriptor.getInheritancePolicy().getDescriptor());
                }
            }
        }

        unmarshalRecord = (UnmarshalRecord)xmlDescriptor.getObjectBuilder().createRecord(session);
        unmarshalRecord.setParentRecord(parentRecord);
        unmarshalRecord.setUnmarshaller(parentRecord.getUnmarshaller());
        unmarshalRecord.setXMLReader(parentRecord.getXMLReader());
        unmarshalRecord.startDocument();
        unmarshalRecord.setUnmarshalNamespaceResolver(unmarshalNamespaceResolver);
        unmarshalRecord.startElement(namespaceURI, localName, qName, atts);

        parentRecord.getXMLReader().setContentHandler(unmarshalRecord);

        try {
            unmarshalRecord.getXMLReader().setProperty("http://xml.org/sax/properties/lexical-handler", unmarshalRecord);
        } catch (SAXNotRecognizedException ex) {
        } catch (SAXNotSupportedException ex) {
            //if lexical handling is not supported by this parser, just ignore.
        }

        currentDataObjects.push(unmarshalRecord.getCurrentObject());
        depth++;
    }

    private SDOXMLDocument getXmlDocument() {
        if (xmlDocument == null) {
            xmlDocument = new SDOXMLDocument();
        }
        return xmlDocument;
    }

    public void setUnmarshalRecord(UnmarshalRecord unmarshalRecord) {
        parentRecord = unmarshalRecord;
        aHelperContext = (HelperContext)unmarshalRecord.getUnmarshaller().getProperty(SDOConstants.SDO_HELPER_CONTEXT);

        if (parentRecord.getParentRecord() == null) {
            rootProcessed = false;
        } else {
            rootProcessed = true;
            if (parentRecord.getParentRecord().getCurrentObject() instanceof DataObject) {
                currentDataObjects.push(parentRecord.getParentRecord().getCurrentObject());
                depth++;
            }
        }
        unmarshalNamespaceResolver = parentRecord.getUnmarshalNamespaceResolver();
    }

    private boolean equalStrings(String string1, String string2) {
        if (string1 == null) {
            if (string2 == null) {
                return true;
            } else {
                return false;
            }
        } else {
            if (string2 == null) {
                return false;
            } else {
                return string1.equals(string2);
            }
        }
    }
}
