/*
 * Copyright (c) 2012, 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:
//     Blaise Doughan - 2.5 - initial implementation
package org.eclipse.persistence.internal.oxm.record;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.namespace.QName;

import org.eclipse.persistence.core.descriptors.CoreInheritancePolicy;
import org.eclipse.persistence.internal.core.helper.CoreField;
import org.eclipse.persistence.internal.core.sessions.CoreAbstractRecord;
import org.eclipse.persistence.internal.core.sessions.CoreAbstractSession;
import org.eclipse.persistence.internal.oxm.Constants;
import org.eclipse.persistence.internal.oxm.ConversionManager;
import org.eclipse.persistence.internal.oxm.Marshaller;
import org.eclipse.persistence.internal.oxm.Namespace;
import org.eclipse.persistence.internal.oxm.NamespaceResolver;
import org.eclipse.persistence.internal.oxm.ObjectBuilder;
import org.eclipse.persistence.internal.oxm.Root;
import org.eclipse.persistence.internal.oxm.XPathQName;
import org.eclipse.persistence.internal.oxm.mappings.Descriptor;
import org.eclipse.persistence.internal.oxm.mappings.Field;
import org.eclipse.persistence.oxm.XMLConstants;
import org.eclipse.persistence.oxm.XMLField;
import org.eclipse.persistence.oxm.schema.XMLSchemaReference;
import org.w3c.dom.Node;

public class AbstractMarshalRecordImpl<
    ABSTRACT_SESSION extends CoreAbstractSession,
    FIELD extends CoreField,
    MARSHALLER extends Marshaller,
    NAMESPACE_RESOLVER extends NamespaceResolver> extends CoreAbstractRecord implements AbstractMarshalRecord<ABSTRACT_SESSION, FIELD, MARSHALLER, NAMESPACE_RESOLVER> {

    private ConversionManager conversionManager;
    protected boolean equalNamespaceResolvers;
    protected boolean hasCustomNamespaceMapper;
    private boolean isXOPPackage;
    private XPathQName leafElementType;
    protected MARSHALLER marshaller;
    protected boolean namespaceAware = true;
    protected NAMESPACE_RESOLVER namespaceResolver;
    private Object owningObject;
    private AbstractMarshalRecord<ABSTRACT_SESSION, FIELD, MARSHALLER, NAMESPACE_RESOLVER> realRecord;
    protected ABSTRACT_SESSION session;

    public AbstractMarshalRecordImpl(AbstractMarshalRecord realRecord) {
        this.realRecord = realRecord;
    }

    @Override
    public List addExtraNamespacesToNamespaceResolver(Descriptor descriptor,
            CoreAbstractSession session, boolean allowOverride,
            boolean ignoreEqualResolvers) {
        if (equalNamespaceResolvers && !ignoreEqualResolvers) {
            return null;
        }

        org.eclipse.persistence.internal.oxm.NamespaceResolver descriptorNamespaceResolver = descriptor.getNamespaceResolver();
        if(null == descriptorNamespaceResolver || !descriptorNamespaceResolver.hasPrefixesToNamespaces()) {
            return null;
        }
        Map<String, String> prefixesToNamespaces = descriptorNamespaceResolver.getPrefixesToNamespaces();
        if(prefixesToNamespaces.size() == 0) {
            return null;
        }
        List returnList = new ArrayList(prefixesToNamespaces.size());
        org.eclipse.persistence.internal.oxm.NamespaceResolver marshalRecordNamespaceResolver = namespaceResolver;
        for(Entry<String, String> entry: prefixesToNamespaces.entrySet()) {

            //if isn't already on a parentadd namespace to this element
            String prefix = marshalRecordNamespaceResolver.resolveNamespaceURI(entry.getValue());

            if (prefix == null || prefix.length() == 0) {
                //if there is no prefix already declared for this uri in the nr add this one
                //unless that prefix is already bound to another namespace uri
                prefix = entry.getKey();
                if(hasCustomNamespaceMapper) {
                    String newPrefix = getMarshaller().getNamespacePrefixMapper().getPreferredPrefix(entry.getValue(), prefix, true);
                    if(newPrefix != null && !(newPrefix.length() == 0)) {
                        prefix = newPrefix;
                    }
                }
                String uri = marshalRecordNamespaceResolver.resolveNamespacePrefix(prefix);
                if(hasCustomNamespaceMapper || allowOverride || uri == null || uri.length() == 0) {
                    //if this uri is unknown, the cutom mapper will return the preferred prefix for this uri
                    marshalRecordNamespaceResolver.put(entry.getKey(), entry.getValue());
                    returnList.add(new Namespace(prefix, entry.getValue()));
                }
            } else if(allowOverride) {
                //if overrides are allowed, add the prefix if the URI is different
                if (!prefix.equals(entry.getKey()) && !hasCustomNamespaceMapper) {
                    //if prefix exists for uri but is different then add this
                    //unless using a custom namespace prefix mapper. Then prefix is expected to be different
                    marshalRecordNamespaceResolver.put(entry.getKey(), entry.getValue());
                    returnList.add(new Namespace(entry.getKey(), entry.getValue()));
                }
            }
        }
        return returnList;
    }

    public boolean addXsiTypeAndClassIndicatorIfRequired(Descriptor descriptor, Descriptor referenceDescriptor, Field xmlField, boolean isRootElement) {
        ObjectBuilder objectBuilder = (ObjectBuilder) descriptor.getObjectBuilder();
        boolean xsiTypeIndicatorField = objectBuilder.isXsiTypeIndicatorField();

        if(objectBuilder.addClassIndicatorFieldToRow(this)) {
            return true;
        }



        QName leafType = null;
        if (xmlField != null) {
            leafType = xmlField.getLeafElementType();

            XMLSchemaReference xmlRef = descriptor.getSchemaReference();
            if (xmlRef != null) {
                if (leafType == null) {
                    if (xmlRef.getType() == XMLSchemaReference.ELEMENT) {
                        return false;
                    }
                    if (referenceDescriptor == null) {
                        writeXsiTypeAttribute(descriptor, xmlRef, isRootElement);
                        return true;
                    }
                } else if (((xmlRef.getType() == XMLSchemaReference.COMPLEX_TYPE) || (xmlRef.getType() == XMLSchemaReference.SIMPLE_TYPE)) && xmlRef.getSchemaContext() != null && xmlRef.isGlobalDefinition()) {
                    QName ctxQName = xmlRef.getSchemaContextAsQName(descriptor.getNamespaceResolver());
                    if (!ctxQName.equals(leafType)) {
                        writeXsiTypeAttribute(descriptor, xmlRef, isRootElement);
                        return true;
                    }
                }
            }
        }

        if (referenceDescriptor != null && referenceDescriptor == descriptor) {
            return false;
        }
        if (descriptor.hasInheritance() && !descriptor.getInheritancePolicy().isRootParentDescriptor()) {
            CoreInheritancePolicy inheritancePolicy = descriptor.getInheritancePolicy();
            Field indicatorField = (Field) inheritancePolicy.getClassIndicatorField();
            if (indicatorField != null && xsiTypeIndicatorField) {
                Object classIndicatorValueObject = inheritancePolicy.getClassIndicatorMapping().get(descriptor.getJavaClass());
                String classIndicatorUri = null;
                String classIndicatorLocal= null;
                String classIndicatorPrefix= null;
                if (classIndicatorValueObject instanceof QName) {
                    QName classIndicatorQName = (QName) classIndicatorValueObject;
                    classIndicatorUri = classIndicatorQName.getNamespaceURI();
                    classIndicatorLocal = classIndicatorQName.getLocalPart();
                    classIndicatorPrefix = classIndicatorQName.getPrefix();
                } else {
                    String classIndicatorValue = (String) inheritancePolicy.getClassIndicatorMapping().get(descriptor.getJavaClass());
                    int nsindex = classIndicatorValue.indexOf(Constants.COLON);
                    String prefix = null;
                    if (nsindex != -1) {
                        classIndicatorLocal = classIndicatorValue.substring(nsindex + 1);
                        prefix = classIndicatorValue.substring(0, nsindex);
                    } else {
                        classIndicatorLocal = classIndicatorValue;
                    }
                    classIndicatorUri = descriptor.getNonNullNamespaceResolver().resolveNamespacePrefix(prefix);
                }
                if(leafType == null
                        || isRootElement && marshaller.isApplicationJSON() && !marshaller.isIncludeRoot()
                        || !(leafType.getLocalPart().equals(classIndicatorLocal))
                        || (classIndicatorUri == null && (leafType.getNamespaceURI() != null && leafType.getNamespaceURI().length() >0))
                        || (classIndicatorUri != null && !classIndicatorUri.equals(leafType.getNamespaceURI()))
                       ){
                    if (inheritancePolicy.hasClassExtractor()) {
                        objectBuilder.addClassIndicatorFieldToRow(this);
                    } else {
                        writeXsiTypeAttribute(descriptor, classIndicatorUri, classIndicatorLocal,classIndicatorPrefix, isRootElement);
                    }
                    return true;
                }
                return false;
            }

        }
        return false;
    }

    /**
     * INTERNAL
     * @since EclipseLink 2.5.0
     */
   public boolean addXsiTypeAndClassIndicatorIfRequired(Descriptor descriptor, Descriptor referenceDescriptor, Field xmlField,
           Object originalObject, Object obj, boolean wasXMLRoot, boolean isRootElement) {
        if (wasXMLRoot) {
            XMLSchemaReference xmlRef = descriptor.getSchemaReference();

            if (descriptor != null) {
                Root xr = (Root) originalObject;

                if (xmlRef == null) {
                    return false;
                }

        if (xr.getDeclaredType() != null && xr.getDeclaredType() == xr.getObject().getClass()) {
                    return false;
                }

                String xmlRootLocalName = xr.getLocalName();
                String xmlRootUri = xr.getNamespaceURI();

                XPathQName qName = new XPathQName(xmlRootUri, xmlRootLocalName, namespaceAware);
                Descriptor xdesc = marshaller.getContext().getDescriptor(qName);
                if (xdesc != null) {
                    boolean writeTypeAttribute = xdesc.getJavaClass() != descriptor.getJavaClass();
                    if (writeTypeAttribute) {
                        writeXsiTypeAttribute(descriptor, xmlRef, isRootElement);
                        return true;
                    }
                    return false;

                }

                boolean writeTypeAttribute = true;
                int tableSize = descriptor.getTableNames().size();
                for (int i = 0; i < tableSize; i++) {
                    if (!writeTypeAttribute) {
                        return false;
                    }
                    String defaultRootQualifiedName = (String) descriptor.getTableNames().get(i);
                    if (defaultRootQualifiedName != null) {
                        String defaultRootLocalName = null;
                        String defaultRootUri = null;
                        int colonIndex = defaultRootQualifiedName.indexOf(Constants.COLON);
                        if (colonIndex > 0) {
                            String defaultRootPrefix = defaultRootQualifiedName.substring(0, colonIndex);
                            defaultRootLocalName = defaultRootQualifiedName.substring(colonIndex + 1);
                            if (descriptor.getNamespaceResolver() != null) {
                                defaultRootUri = descriptor.getNamespaceResolver().resolveNamespacePrefix(defaultRootPrefix);
                            }
                        } else {
                            defaultRootLocalName = defaultRootQualifiedName;
                        }

                        if (xmlRootLocalName != null) {
                            if ((((defaultRootLocalName == null) && (xmlRootLocalName == null)) || (defaultRootLocalName.equals(xmlRootLocalName)))
                                && (((defaultRootUri == null) && (xmlRootUri == null)) || ((xmlRootUri != null) && (defaultRootUri != null) && (defaultRootUri.equals(xmlRootUri))))) {
                                // if both local name and uris are equal then don't need to write type attribute
                                return false;
                            }
                        }
                    } else {
                        // no default rootElement was set
                        // if xmlRootName = null then writeTypeAttribute = false
                        if (xmlRootLocalName == null) {
                            return false;
                        }
                    }
                }
                if (writeTypeAttribute && xmlRef != null) {
                    writeXsiTypeAttribute(descriptor, xmlRef, isRootElement);
                    return true;
                }
            }
            return false;
        } else {
            return addXsiTypeAndClassIndicatorIfRequired(descriptor, referenceDescriptor, xmlField, isRootElement);
        }
    }

    public void attribute(String namespaceURI, String localName,
            String qualifiedName, String value) {
        if(null != realRecord) {
            realRecord.attribute(namespaceURI, localName, qualifiedName, value);
        }
    }

    @Override
    public void attributeWithoutQName(String namespaceURI, String localName,
            String prefix, String value) {
        String qualifiedName = localName;
        if(prefix != null && prefix.length() >0){
            qualifiedName = prefix + getNamespaceSeparator() + qualifiedName;
        }
        attribute(namespaceURI, localName, qualifiedName, value);
    }

    /**
     * @since EclipseLink 2.6.0
     */
    @Override
    public ConversionManager getConversionManager() {
        if(null == conversionManager) {
            conversionManager = (ConversionManager) session.getDatasourcePlatform().getConversionManager();
        }
        return conversionManager;
    }

    @Override
    public Node getDOM() {
        // TODO Auto-generated method stub
        throw new UnsupportedOperationException();
    }

    public XPathQName getLeafElementType() {
        return leafElementType;
    }

    @Override
    public MARSHALLER getMarshaller() {
        return marshaller;
    }

    @Override
    public NAMESPACE_RESOLVER getNamespaceResolver() {
        return namespaceResolver;
    }

    @Override
    public char getNamespaceSeparator() {
        return Constants.COLON;
    }

    @Override
    public Object getOwningObject() {
        return owningObject;
    }

    @Override
    public ABSTRACT_SESSION getSession() {
        return session;
    }

    @Override
    public boolean hasCustomNamespaceMapper() {
        return hasCustomNamespaceMapper;
    }

    @Override
    public boolean hasEqualNamespaceResolvers() {
        return equalNamespaceResolvers;
    }

    /**
     * Determine if namespaces will be considered during marshal/unmarshal operations.
     */
    @Override
    public boolean isNamespaceAware() {
        return namespaceAware;
    }

    @Override
    public boolean isXOPPackage() {
        return isXOPPackage;
    }

    @Override
    public void namespaceDeclaration(String prefix, String typeUri) {
        if(realRecord != null) {
            realRecord.namespaceDeclaration(prefix, typeUri);
            return;
        }
        throw new UnsupportedOperationException();
    }

    @Override
    public Object put(FIELD field, Object object) {
        if(null != realRecord) {
            return realRecord.put(field, object);
        }
        throw new UnsupportedOperationException();
    }

    @Override
    public void removeExtraNamespacesFromNamespaceResolver(List<Namespace> extraNamespaces, CoreAbstractSession session) {
        if (extraNamespaces == null){
          return;
        }

         for (int i = 0; i < extraNamespaces.size(); i++) {
             Namespace nextExtraNamespace = (Namespace)extraNamespaces.get(i);
             String uri = namespaceResolver.resolveNamespacePrefix(nextExtraNamespace.getPrefix());
             if ((uri != null) && uri.equals(nextExtraNamespace.getNamespaceURI())) {
                namespaceResolver.removeNamespace(nextExtraNamespace.getPrefix());
             }
         }
     }

    @Override
    public String resolveNamespacePrefix(String prefix) {
        return null;
    }

    @Override
    public void setCustomNamespaceMapper(boolean customNamespaceMapper) {
        this.hasCustomNamespaceMapper = customNamespaceMapper;
    }

    @Override
    public void setEqualNamespaceResolvers(boolean equalNRs) {
        this.equalNamespaceResolvers = equalNRs;
    }

    @Override
    public void setLeafElementType(QName type) {
        if(type != null){
            setLeafElementType(new XPathQName(type, isNamespaceAware()));
        }
    }

    @Override
    public void setLeafElementType(XPathQName type) {
        leafElementType = type;
    }

    @Override
    public void setMarshaller(MARSHALLER marshaller) {
        this.marshaller = marshaller;
        if(marshaller != null){
            if(marshaller.getNamespacePrefixMapper() != null){
                namespaceAware = true;
            }else{
                namespaceAware = marshaller.isApplicationXML();
            }
        }
    }

    @Override
    public void setNamespaceResolver(NAMESPACE_RESOLVER namespaceResolver) {
        this.namespaceResolver = namespaceResolver;
    }

    @Override
    public void setOwningObject(Object owningObject) {
        this.owningObject = owningObject;
    }

    @Override
    public void setSession(ABSTRACT_SESSION session) {
        this.session = session;
    }

    @Override
    public void setXOPPackage(boolean isXOPPackage) {
        this.isXOPPackage = isXOPPackage;
    }

    @Override
    public void writeXsiTypeAttribute(Descriptor descriptor, String typeUri,
            String typeLocal, String typePrefix, boolean addToNamespaceResolver) {
        if (typeLocal == null){
            return;
        }
        String typeValue = typeLocal;
        if(isNamespaceAware() && typeUri != null && !typeUri.equals(Constants.EMPTY_STRING) && !typeUri.equals(namespaceResolver.getDefaultNamespaceURI())){
            String prefix = namespaceResolver.resolveNamespaceURI(typeUri);
            if(prefix != null && !prefix.equals(Constants.EMPTY_STRING)){
                typeValue = prefix + getNamespaceSeparator() + typeValue;
            } else if (typeUri.equals(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI)) {
                prefix = namespaceResolver.generatePrefix(Constants.SCHEMA_PREFIX);
                typeValue = prefix + getNamespaceSeparator() + typeValue;
                namespaceDeclaration(prefix, typeUri);

            } else if (typePrefix != null && !typePrefix.equals(Constants.EMPTY_STRING)){
                String existingUri = namespaceResolver.resolveNamespacePrefix(typePrefix);
                if(existingUri != null){
                    prefix = namespaceResolver.generatePrefix();
                }else{
                    prefix = typePrefix;
                }
                typeValue = prefix + getNamespaceSeparator() + typeValue;
                namespaceDeclaration(prefix, typeUri);
            }else{
                prefix = namespaceResolver.generatePrefix();
                typeValue = prefix + getNamespaceSeparator() + typeValue;
                namespaceDeclaration(prefix, typeUri);

            }
        }

        String xsiPrefix = null;
        if(isNamespaceAware()){
            xsiPrefix = namespaceResolver.resolveNamespaceURI(javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
            if (xsiPrefix == null) {
                xsiPrefix = namespaceResolver.generatePrefix(Constants.SCHEMA_INSTANCE_PREFIX);
                namespaceDeclaration(xsiPrefix, javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
                if(addToNamespaceResolver){
                    namespaceResolver.put(xsiPrefix, javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
                }
            }
        }
        if (marshaller.isApplicationJSON() && descriptor != null && descriptor.getInheritancePolicyOrNull() != null &&
                descriptor.getInheritancePolicyOrNull().getClassIndicatorFieldName() != null) {
            //true - override default type attribute name by JSON_TYPE_ATTRIBUTE_NAME marshaller property
            if(XMLConstants.DEFAULT_XML_TYPE_ATTRIBUTE == descriptor.getInheritancePolicyOrNull().getClassIndicatorField() &&
                    marshaller.getJsonTypeConfiguration().getJsonTypeAttributeName() != null) {
                attributeWithoutQName(javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, marshaller.getJsonTypeConfiguration().getJsonTypeAttributeName(), xsiPrefix, typeValue);
            } else {
                attributeWithoutQName(javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI,
                        ((XMLField) (descriptor.getInheritancePolicyOrNull().getClassIndicatorField())).getXPathFragment().getLocalName(), xsiPrefix, typeValue);
            }
        } else {
            attributeWithoutQName(javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, Constants.SCHEMA_TYPE_ATTRIBUTE, xsiPrefix, typeValue);
        }
    }

    @Override
    public void writeXsiTypeAttribute(Descriptor xmlDescriptor, XMLSchemaReference xmlRef, boolean addToNamespaceResolver) {
        QName contextAsQName = xmlRef.getSchemaContextAsQName();

        if(contextAsQName == null){
            contextAsQName = xmlRef.getSchemaContextAsQName(namespaceResolver);
        }
        if (contextAsQName != null) {
            writeXsiTypeAttribute(xmlDescriptor, contextAsQName.getNamespaceURI(), contextAsQName.getLocalPart(), null, addToNamespaceResolver);
        }
    }

}
