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

import java.util.ArrayList;
import java.util.Iterator;

import javax.xml.namespace.QName;

import org.eclipse.persistence.exceptions.ConversionException;
import org.eclipse.persistence.internal.core.sessions.CoreAbstractSession;
import org.eclipse.persistence.internal.oxm.ConversionManager;
import org.eclipse.persistence.internal.oxm.XMLConversionManager;
import org.eclipse.persistence.internal.oxm.mappings.UnionField;
import org.eclipse.persistence.internal.oxm.record.AbstractUnmarshalRecord;

/**
 * <p>Subclass of XMLField for fields that are mapped to unions.
 * Maintains a list of schema types instead of just one single schema type.
 * Schema types can be added using the addSchemaType api.
 *
 * XMLConstants has a list of useful constants including a list of QNames for
 * built-in schema types that can be used when adding schema types.
 *
 * <p>When reading and writing an element that is mapped with an XMLUnionField, a conversion to
 * each of the schema types on the field (in the order they are specified ) is tried
 * until a conversion is successful. The java type to convert to is based on the list of
 * schema type to java conversion pairs specified on the field.  These conversion pairs
 * can be modified using the addXMLConversion api.
 *
 * <p><em>Code Sample</em><br>
 * <code>
 * In this example the age field could be a date or an int.<br>
 * XMLUnionField field = new XMLUnionField("age/text()");<br>
 * field.addSchemaType(XMLConstants.DATE_QNAME);<br>
 * field.addSchemaType(XMLConstants.INT_QNAME)<br>
 * </code>
 *
 * @see XMLField
 * @see XMLConstants
 */
public class XMLUnionField extends XMLField implements UnionField<XMLConversionManager, NamespaceResolver> {
    private ArrayList schemaTypes;

    /**
     * Constructs an XMLUnionField
     */
    public XMLUnionField() {
        super();
        schemaTypes = new ArrayList();
    }

    /**
    * Constructs an XMLUnionField with the xpath set to the specified xPath
    * @param xPath The xpath expression for the field
    */
    public XMLUnionField(String xPath) {
        super(xPath);
        schemaTypes = new ArrayList();
    }

    /**
    * Return the list of schema types
    * @return the list of types
    */
    @Override
    public ArrayList getSchemaTypes() {
        return schemaTypes;
    }

    /**
    * Sets the schema types that this attribute can be mapped to
    * Valid QName schema types can be found on org.eclipse.persistence.oxm.XMLConstants
    * @param value An ArrayList containing the schema types.
    * @see org.eclipse.persistence.oxm.XMLConstants
    */
    public void setSchemaTypes(ArrayList value) {
        schemaTypes = value;
    }

    /**
    * Adds the new type value to the list of types
    * @param value QName to be added to the list of schema types
    */
    public void addSchemaType(QName value) {
        if (value != null) {
            if (schemaTypes == null) {
                schemaTypes = new ArrayList();
            }

            // don't add things twice - for now the MW adds only some types...
            // once they add all types this can be removed
            if (!contains(schemaTypes, value)) {
                schemaTypes.add(value);
            }
        }
    }

    /**
      * Return the first schema type in the list of schema types
      * @return the first item in the collection of schema types
      */
    @Override
    public QName getSchemaType() {
        if (schemaTypes != null) {
            return (QName) getSchemaTypes().get(0);
        }
        return null;
    }

    /**
      * Adds the new type value to the list of types
      * @param value The value to be added to the list of schema types
      */
    @Override
    public void setSchemaType(QName value) {
        addSchemaType(value);
    }

    /**
    * Checks for existance of a schema type in the list.
    *
    * @param types List of types
    * @param value the QName to look for in the list
    * @return true if 'value' exists in the list, false otherwise
    */
    private boolean contains(ArrayList types, QName value) {
        QName type;
        Iterator it = types.iterator();

        while (it.hasNext()) {
            type = (QName) it.next();
            if (type.equals(value)) {
                return true;
            }
        }

        return false;
    }

    /**
    * INTERNAL:
    * returns true since this is a union field
    */
    @Override
    public boolean isUnionField() {
        return true;
    }

    @Override
    public QName getSchemaTypeForValue(Object value, CoreAbstractSession session) {
        if(leafElementType != null){
            return leafElementType;
        }else if (isTypedTextField()) {
            ConversionManager conversionManager = (ConversionManager) session.getDatasourcePlatform().getConversionManager();
            return getXMLType(value.getClass(), conversionManager);
        }
        return getSingleValueToWriteForUnion(value, session);
    }

    protected QName getSingleValueToWriteForUnion(Object value, CoreAbstractSession session) {
        ArrayList schemaTypes = getSchemaTypes();
        QName schemaType = null;
        for (int i = 0, schemaTypesSize = schemaTypes.size(); i < schemaTypesSize; i++) {
            QName nextQName = (QName)getSchemaTypes().get(i);
            try {
                if (nextQName != null) {
                    ConversionManager conversionManager = (ConversionManager) session.getDatasourcePlatform().getConversionManager();
                    Class javaClass = getJavaClass(nextQName, conversionManager);
                    conversionManager.convertObject(value, javaClass, nextQName);
                    schemaType = nextQName;
                    break;
                }
            } catch (ConversionException ce) {
                if (i == (schemaTypes.size() - 1)) {
                    schemaType = nextQName;
                }
            }
        }
        return schemaType;
    }


    /**
    * INTERNAL:
    */
    @Override
    public Object convertValueBasedOnSchemaType(Object value, XMLConversionManager xmlConversionManager, AbstractUnmarshalRecord record) {
        Object convertedValue = value;
        for (int i = 0; i < schemaTypes.size(); i++) {
            QName nextQName = (QName) schemaTypes.get(i);
            try {
                if (nextQName != null) {

                    if(XMLConstants.QNAME_QNAME.equals(nextQName)){
                        xmlConversionManager.buildQNameFromString((String)value, record);
                        break;
                    }else{
                        Class javaClass = getJavaClass(nextQName, xmlConversionManager);
                        convertedValue = xmlConversionManager.convertObject(value, javaClass, nextQName);
                        break;
                    }
                }
            } catch (ConversionException ce) {
                if (i == (schemaTypes.size() - 1)) {
                    throw ce;
                }
            }
        }
        return convertedValue;
    }

    /**
      * Return the class for a given qualified XML Schema type.
      * If the class is a primitive the corresponding wrapper class is returned
      * @param qname The qualified name of the XML Schema type to use as a key in the lookup
      * @return The class associated with the specified schema type, if no corresponding match found returns null
      */
    @Override
    public Class getJavaClass(QName qname) {
        return getJavaClass(qname, XMLConversionManager.getDefaultXMLManager());
    }

    /**
     * INTERNAL
     * @return the class for a given qualified XML Schema type.
     * @since EclipseLink 2.6.0
     */
    @Override
    public Class getJavaClass(QName qname, ConversionManager conversionManager) {
        if (userXMLTypes != null) {
            Class theClass = (Class) userXMLTypes.get(qname);
            if(theClass != null){
                return theClass;
            }
        }
        Class javaClass = conversionManager.javaType(qname);
        return XMLConversionManager.getObjectClass(javaClass);
    }
    /**
     * INTERNAL
     */
    @Override
    public boolean isSchemaType(QName schemaType){
        if(getSchemaTypes() == null || getSchemaTypes().size() ==0){
            return false;
        }
        return contains(getSchemaTypes(), schemaType);
    }
}
