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

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import commonj.sdo.Property;
import commonj.sdo.Sequence;
import javax.xml.namespace.QName;
import org.eclipse.persistence.sdo.SDODataObject;
import org.eclipse.persistence.sdo.helper.ListWrapper;
import org.eclipse.persistence.sdo.helper.SDOTypeHelper;
import org.eclipse.persistence.core.mappings.CoreMapping;
import org.eclipse.persistence.exceptions.SDOException;
import org.eclipse.persistence.internal.oxm.XPathFragment;
import org.eclipse.persistence.oxm.NamespaceResolver;
import org.eclipse.persistence.oxm.XMLConstants;
import org.eclipse.persistence.oxm.XMLDescriptor;
import org.eclipse.persistence.oxm.XMLField;
import org.eclipse.persistence.oxm.XMLRoot;
import org.eclipse.persistence.oxm.mappings.XMLCollectionReferenceMapping;
import org.eclipse.persistence.oxm.mappings.XMLObjectReferenceMapping;
import org.eclipse.persistence.oxm.sequenced.Setting;
import org.eclipse.persistence.mappings.DatabaseMapping;

public class SDOSequence implements Sequence {
    private static final String TEXT_XPATH = "text()";
    private SDODataObject dataObject;
    private List<Setting> settings;
    private Map<Key, Setting> valuesToSettings;

    public SDOSequence(SDODataObject dataObject) {
        // catch a null dataObject early before we get NPE on any update
        // operations during add/remove
        if (null == dataObject) {
            throw SDOException.sequenceDataObjectInstanceFieldIsNull();
        }
        this.dataObject = dataObject;
        this.settings = new ArrayList<Setting>();
        this.valuesToSettings = new HashMap<Key, Setting>();
    }

    public SDODataObject getDataObject() {
        return dataObject;
    }

    public List<Setting> getSettings() {
        return settings;
    }

    protected Map<Key, Setting> getValuesToSettings() {
        return valuesToSettings;
    }

    public void add(int index, int propertyIndex, Object value) {
        Property property = dataObject.getInstanceProperty(propertyIndex);
        add(index, property, value);
    }

    public boolean add(int propertyIndex, Object value) {
        Property property = dataObject.getInstanceProperty(propertyIndex);
        return add(property, value);
    }

    public void add(int index, Property property, Object value) {
        if (!isAllowedInSequence(property)) {
            return;
        }

        if (property != null && property.isOpenContent() && dataObject.getType().isOpen()) {
            dataObject.addOpenContentProperty(property);
        }
        // Update the data object
        if (property.isMany()) {
            ListWrapper listWrapper = (ListWrapper) dataObject.getList(property);
            if (value instanceof List) {
                // iterate list
                for (Iterator i = ((List) value).iterator(); i.hasNext();) {
                    // add a setting to the end of the sequence
                    Object aValue = i.next();
                    Setting setting = convertToSetting(property, aValue);
                    valuesToSettings.put(new Key(property, aValue), setting);
                    settings.add(index++, setting);
                    // no need to check updateContainment flag -
                    // ListWrapper.add() will not pass an entire List here
                    listWrapper.add(aValue, false);
                }
            } else {
                // set individual list item
                // add a setting to the end of the sequence
                Setting setting = convertToSetting(property, value);
                valuesToSettings.put(new Key(property, value), setting);
                settings.add(index, setting);
                int listIdx = getIndexInList(property, value);
                if (listIdx != -1) {
                    listWrapper.add(listIdx, value, false);
                } else {
                    listWrapper.add(value, false);
                }
            }
        } else {
            dataObject.setPropertyInternal((SDOProperty) property, value, false);
            // Update the settings
            Setting setting = convertToSetting(property, value);
            valuesToSettings.put(new Key(property, value), setting);
            settings.add(index, setting);
        }
    }

    private boolean isAllowedInSequence(Property property) {
        // Disallow the addition of a null Property
        if (null == property) {
            return false;
        }
        // Disallow the addition of a read only Property
        if (((SDOProperty)property).isReadOnly()) {
            return false;
        }
        // Disallow the addition of a Properties representing an XML attribute
        if (dataObject.getType().getHelperContext().getXSDHelper().isAttribute(property)) {
            throw SDOException.sequenceAttributePropertyNotSupported(property.getName());
        }
        // Disallow an open Property on a closed Type
        if (property.isOpenContent() && !dataObject.getType().isOpen()) {
            return false;
        }
        // Disallow the addition of an isMany==false Property that is already
        // set
        if (property.isMany()) {
            return true;
        }
        if (dataObject.isSet(property)) {
            throw SDOException.sequenceDuplicateSettingNotSupportedForComplexSingleObject(getIndexForProperty(property), property.getName());
        }
        return true;
    }

    public void add(int index, String propertyName, Object value) {
        Property property = dataObject.getInstanceProperty(propertyName);
        if (property == null) {
            // Property with given name does not exist - create an open content
            // property
            property = dataObject.defineOpenContentProperty(propertyName, value);
            ((SDOProperty) property).setMany(true);
        }
        add(index, property, value);
    }

    public void add(int index, String text) {
        addText(index, text);
    }

    public boolean add(Property property, Object value) {
        if (addSettingWithoutModifyingDataObject(property, value)) {
            if (property != null && property.isOpenContent() && dataObject.getType().isOpen()) {
                dataObject.addOpenContentProperty(property);
            }
            if (value instanceof XMLRoot) {
                value = ((XMLRoot) value).getObject();
            }
            if (property.isMany()) {
                ListWrapper listWrapper = (ListWrapper) dataObject.getList(property);
                listWrapper.add(value, false);
            } else {
                dataObject.setPropertyInternal((SDOProperty) property, value, false);
            }
            return true;
        }
        return false;
    }

    public boolean add(String propertyName, Object value) {
        Property property = dataObject.getInstanceProperty(propertyName);
        if (property == null) {
            // Property with given name does not exist - create an open content
            // property
            property = dataObject.defineOpenContentProperty(propertyName, value);
            ((SDOProperty) property).setMany(true);
        }
        return add(property, value);
    }

    public void add(String text) {
        addText(text);
    }

    public void addText(int index, String text) {
        // Trigger store of original sequence
        dataObject._setModified(true);
        Setting textSetting = new Setting(null, TEXT_XPATH);
        textSetting.setObject(dataObject);
        textSetting.setValue(text, false);
        settings.add(index, textSetting);
    }

    public void addText(String text) {
        // Trigger store of original sequence
        dataObject._setModified(true);
        Setting textSetting = new Setting(null, TEXT_XPATH);
        textSetting.setObject(dataObject);
        textSetting.setValue(text, false);
        settings.add(textSetting);
    }

    public SDOProperty getProperty(int index) {
        try {
            return getProperty(settings.get(index));
        } catch (IndexOutOfBoundsException iobex) {
            throw SDOException.invalidIndex(iobex, index);
        }
    }

    public SDOProperty getProperty(Setting setting) {
        CoreMapping mapping = setting.getMapping();
        if (null == mapping) {
            List<Setting> children = setting.getChildren();
            if (null != children && children.size() > 0) {
                return getProperty(children.get(0));
            }
        } else {
            SDOProperty property = null;
            if (null == setting.getName()) {
                Object value = setting.getValue();
                if (value instanceof SDODataObject) {
                    SDOProperty containmentProp = ((SDODataObject) value).getContainmentProperty();
                    if (containmentProp != null) {
                        property = dataObject.getInstanceProperty(containmentProp.getName());
                    }
                    if (property == null) {
                        XMLDescriptor desc = ((SDODataObject) value).getType().getXmlDescriptor();

                        String qualifiedName = desc.getDefaultRootElement();
                        int index = qualifiedName.indexOf(':');
                        String localName = null;
                        if (index > -1) {
                            localName = qualifiedName.substring(index + 1, qualifiedName.length());
                        } else {
                            localName = qualifiedName;
                        }
                        property = dataObject.getInstanceProperty(localName);
                    }
                } else {
                    XMLRoot xmlRoot = (XMLRoot) value;
                    if (null != xmlRoot) {
                        property = dataObject.getInstanceProperty(xmlRoot.getLocalName());
                    }
                }
            } else {
                property = dataObject.getInstanceProperty(mapping.getAttributeName());
            }
            return property;
        }
        return null;
    }

    public Object getValue(int index) {
        try {
            return getValue(settings.get(index));
        } catch (IndexOutOfBoundsException iobex) {
            throw SDOException.invalidIndex(iobex, index);
        }
    }

    private Object getValue(Setting setting) {
        if (null != setting.getMapping() || (setting.getName() != null && setting.getName().equals(TEXT_XPATH))) {
            Object value = setting.getValue();
            if (value instanceof XMLRoot) {
                value = ((XMLRoot) value).getObject();
            }
            return value;
        }

        if (null == setting.getChildren() || setting.getChildren().size() == 0) {
            return null;
        }
        return getValue(setting.getChildren().get(0));
    }

    public void move(int toIndex, int fromIndex) {
        if (toIndex == fromIndex) {
            return;
        }
        // verify indexes are in range
        int size = settings.size();
        if (fromIndex < 0 || fromIndex >= size) {
            throw SDOException.invalidIndex(null, fromIndex);
        }
        if (toIndex < 0 || toIndex >= size) {
            throw SDOException.invalidIndex(null, toIndex);
        }
        // trigger deep copy of sequence (if applicable)
        dataObject._setModified(true);

        Setting setting = settings.remove(fromIndex);
        settings.add(toIndex, setting);

        SDOProperty prop = getProperty(setting);
        if (prop != null && prop.isMany()) {
            ListWrapper lw = (ListWrapper) dataObject.getList(prop);
            Object value = getValue(setting);
            int currentIndexInLw = lw.indexOf(value);
            lw.remove(currentIndexInLw, false);
            int newIndexInLw = getIndexInList(prop, value);
            lw.add(newIndexInLw, value, false);
        }
    }

    public void remove(int index) {
        Setting setting = settings.get(index);
        remove(setting);
        settings.remove(setting);
    }

    private void remove(Setting setting) {
        CoreMapping mapping = setting.getMapping();
        if (null != mapping) {
            Property property = null;
            if (null == setting.getName()) {
                XMLRoot xmlRoot = (XMLRoot) setting.getValue();
                if (null != xmlRoot) {
                    property = dataObject.getInstanceProperty(xmlRoot.getLocalName());
                    valuesToSettings.remove(new Key(property, setting.getValue()));
                }
            } else {
                property = dataObject.getInstanceProperty(mapping.getAttributeName());
                valuesToSettings.remove(new Key(property, setting.getValue()));
            }
            if (property.isMany()) {
                ListWrapper listWrapper = (ListWrapper) dataObject.getList(property);
                listWrapper.remove(setting.getValue(), false, false);
            } else {
                dataObject.unset(property, false, false);
            }
        } else if (setting.getName() != null && setting.getName().equals(TEXT_XPATH)) {
            // Handle "text()"
            dataObject._setModified(true);
        }
        List<Setting> children = setting.getChildren();
        if (null != children) {
            int childrenSize = children.size();
            for (int x = 0; x < childrenSize; x++) {
                remove(children.get(x));
            }
        }
    }

    /**
     * INTERNAL:
     *
     * @param setting
     */
    public void addValueToSettings(Setting setting) {
        valuesToSettings.put(new Key(getProperty(setting), getValue(setting)), setting);
    }

    /**
     * INTERNAL:
     *
     * @param setting
     */
    public void removeValueToSettings(Setting setting) {
        valuesToSettings.remove(new Key(getProperty(setting), getValue(setting)));
    }

    public Object setValue(int index, Object value) {
        return setValue(settings.get(index), value);
    }

    private Object setValue(Setting setting, Object value) {
        if (null == setting.getMapping()) {
            if (setting.getName() != null && setting.getName().equals(TEXT_XPATH)) {
                dataObject._setModified(true);
                Object oldValue = setting.getValue();
                setting.setValue(value, false);
                return oldValue;
            }
            List<Setting> children = setting.getChildren();
            if (null != children && children.size() > 0) {
                return setValue(children.get(0), value);
            }
            return null;
        }

        SDOProperty property = getProperty(setting);
        Object oldValue = setting.getValue();

        if (property.isMany()) {
            ListWrapper listValue = (ListWrapper) dataObject.getList(property);
            int valueIndex = listValue.indexOf(oldValue);
            listValue.remove(oldValue, property.isContainment(), false);
            listValue.add(valueIndex, value, false);
            setting.setValue(value, false);
        } else {
            if (dataObject.isSet(property)) {
                updateSettingWithoutModifyingDataObject(property, dataObject.get(property), value);
                setting.setValue(value);
            } else {
                addSettingWithoutModifyingDataObject(property, value);
            }
            dataObject.setPropertyInternal(property, value, false);
        }
        return oldValue;
    }

    public int size() {
        return settings.size();
    }

    private Setting convertToSetting(Property property, Object value) {
        SDOProperty sdoProperty = (SDOProperty) property;
        DatabaseMapping mapping = sdoProperty.getXmlMapping();
        Setting setting = new Setting();
        SDOType sdoType = dataObject.getType();
        XMLDescriptor xmlDescriptor = sdoType.getXmlDescriptor();
        if (null == mapping) {
            setting.setObject(dataObject);

            mapping = xmlDescriptor.getMappingForAttributeName("openContentProperties");
            setting.setMapping(mapping);
            XMLRoot xmlRoot = new XMLRoot();
            if (value instanceof XMLRoot) {
                xmlRoot.setLocalName(((XMLRoot) value).getLocalName());
                xmlRoot.setNamespaceURI(((XMLRoot) value).getNamespaceURI());
                xmlRoot.setObject(((XMLRoot) value).getObject());
            } else {
                xmlRoot.setLocalName(sdoProperty.getName());
                xmlRoot.setNamespaceURI(sdoProperty.getUri());
                xmlRoot.setObject(value);
                // do not set schema type for global properties
                SDOTypeHelper hlpr = (SDOTypeHelper) dataObject.getType().getHelperContext().getTypeHelper();
                if (hlpr.getOpenContentProperty(sdoProperty.getUri(), sdoProperty.getName()) == null) {
                    QName schemaTypeQName = hlpr.getXSDTypeFromSDOType(property.getType());
                    if (schemaTypeQName != null && schemaTypeQName != XMLConstants.STRING_QNAME) {
                        xmlRoot.setSchemaType(schemaTypeQName);
                    }
                }
            }
            setting.setValue(xmlRoot, false);
        } else {
            setting = convertToSetting(mapping, value);
        }
        return setting;
    }

    private Setting convertToSetting(DatabaseMapping mapping, Object value) {
        XMLDescriptor xmlDescriptor = (XMLDescriptor) mapping.getDescriptor();
        NamespaceResolver nsResolver = xmlDescriptor.getNamespaceResolver();
        Setting rootSetting = new Setting();
        XMLField xmlField = (XMLField) mapping.getField();
        if (xmlField == null) {
            if (mapping instanceof XMLObjectReferenceMapping) {
                xmlField = (XMLField) ((XMLObjectReferenceMapping) mapping).getFields().get(0);
            } else if (mapping instanceof XMLCollectionReferenceMapping) {
                xmlField = (XMLField) ((XMLCollectionReferenceMapping) mapping).getFields().get(0);
            }
        }
        Setting setting = rootSetting;
        if (xmlField != null) {
            XPathFragment xPathFragment = xmlField.getXPathFragment();
            rootSetting = convertToSetting(xPathFragment, nsResolver);
            setting = rootSetting;

            while (xPathFragment.getNextFragment() != null) {
                xPathFragment = xPathFragment.getNextFragment();
                Setting childSetting = convertToSetting(xPathFragment, nsResolver);
                setting.addChild(childSetting);
                setting = childSetting;
            }
        }
        setting.setObject(dataObject);
        setting.setMapping(mapping);
        setting.setValue(value, false);
        return rootSetting;
    }

    private Setting convertToSetting(XPathFragment xPathFragment, NamespaceResolver nsResolver) {
        Setting setting = new Setting();
        String name = xPathFragment.getLocalName();
        if (null == name) {
            name = xPathFragment.getShortName();
        }
        setting.setName(name);
        if (xPathFragment.hasNamespace()) {
            setting.setNamespaceURI(nsResolver.resolveNamespacePrefix(xPathFragment.getPrefix()));
        }
        return setting;
    }

    public SDOSequence copy() {
        SDOSequence copy = new SDOSequence(dataObject);
        for (int index = 0, size = settings.size(); index < size; index++) {
            Setting settingCopy = settings.get(index).copy();
            copy.getSettings().add(settingCopy);
            copy.getValuesToSettings().put(new Key(getProperty(settingCopy), getValue(settingCopy)), settingCopy);
        }
        return copy;
    }

    /**
     * INTERNAL: Add a setting to the list at the specified index. The owning
     * DataObject will not be made aware of this addition.
     *
     * @param index
     *            the index at which to add the new Setting in the Settings list
     * @param property
     * @param value
     * @return true if the a Setting was successfully added to the list,
     *         otherwise false
     */
    public boolean addSettingWithoutModifyingDataObject(int index, Property property, Object value) {
        Setting setting = convertToSetting(property, value);
        valuesToSettings.put(new Key(property, value), setting);
        if (index >= 0) {
            settings.add(index, setting);
        } else {
            settings.add(setting);
        }
        return true;
    }

    /**
     * INTERNAL:
     */
    public boolean addSettingWithoutModifyingDataObject(Property property, Object value) {
        return addSettingWithoutModifyingDataObject(property, value, true);
    }

    /**
     * INTERNAL:
     */
    public boolean addSettingWithoutModifyingDataObject(Property property, Object value, boolean checkAllowed) {
        if (checkAllowed && !isAllowedInSequence(property)) {
            return false;
        }
        Setting setting = convertToSetting(property, value);
        valuesToSettings.put(new Key(property, value), setting);
        settings.add(setting);
        return true;
    }

    /**
     * INTERNAL:
     */
    public void updateSettingWithoutModifyingDataObject(Property property, Object oldValue, Object newValue) {
        Key key = new Key(property, oldValue);
        Setting setting = valuesToSettings.get(key);
        valuesToSettings.remove(key);
        valuesToSettings.put(new Key(property, newValue), setting);
        // set the new value on the appropriate setting
        while (setting.getMapping() == null) {
            List<Setting> children = setting.getChildren();
            if (children != null && children.size() > 0) {
                setting = children.get(0);
            }
        }
        setting.setValue(newValue, false);
    }

    /**
     * INTERNAL:
     */
    public void removeSettingWithoutModifyingDataObject(Property property, Object value) {
        settings.remove(valuesToSettings.remove(new Key(property, value)));
    }

    /**
     * INTERNAL:
     */
    public void removeSettingWithoutModifyingDataObject(Property property) {
        List<Key> keys = new ArrayList<Key>(valuesToSettings.keySet());
        int size = valuesToSettings.keySet().size();
        for (int i = size - 1; i >= 0; i--) {
            Key nextKey = keys.get(i);
            if (nextKey.getProperty() == property) {
                settings.remove(valuesToSettings.remove(nextKey));
            }
        }
    }

    /**
     * INTERNAL: Convenience method that returns the index of the Setting
     * associated with a given property in the Settings list
     *
     * @param property
     * @return index of the Setting associated with a given property in the
     *         Settings list or -1 if not found
     */
    public int getIndexForProperty(Property property) {
        List<Key> keys = new ArrayList<Key>(valuesToSettings.keySet());
        for (int i = 0; i < keys.size(); i++) {
            Key nextKey = keys.get(i);
            if (nextKey.getProperty() == property) {
                return settings.indexOf(valuesToSettings.get(nextKey));
            }
        }
        return -1;
    }

    /**
     * INTERNAL: Convenience method that, given a many property and a value,
     * returns the associated Setting's index in the Settings list. For example,
     * if a sequence contains many properties "letters" and "numbers", such as
     * [A, 1, C, 2, B, D], and we are looking for the letter B, this method will
     * return 2. Although B is at index 4 of the Settings list, it is at index 2
     * of the list of "letters" - [A, C, B, D].
     *
     * @param property
     * @return index of the value's Setting in the list relative to a given
     *         property or -1 if not found.
     */
    private int getIndexInList(Property manyProp, Object value) {
        int returnIndex = -1;
        for (int i = 0; i < settings.size(); i++) {
            Setting nextSetting = settings.get(i);
            SDOProperty prop = getProperty(nextSetting);
            if (prop.equals(manyProp)) {
                returnIndex++;
                if (value.equals(getValue(nextSetting))) {
                    return returnIndex;
                }
            }
        }
        return returnIndex;
    }

    /**
     * INTERNAL: Get the root Setting for a given Setting.
     *
     * @param setting
     * @return the root Setting or this Setting if it is a root
     */
    public static Setting getRootSetting(Setting setting) {
        Setting rootSetting = setting;
        while (rootSetting.getParent() != null) {
            rootSetting = rootSetting.getParent();
        }
        return rootSetting;
    }

    /**
     * INTERNAL: Ensure that each Setting in the settings list is also present
     * in the valuesToSettings map
     */
    public void afterUnmarshal() {
        for (Iterator<Setting> setIt = getSettings().iterator(); setIt.hasNext();) {
            addValueToSettings(setIt.next());
        }
    }

    private static class Key {
        private Property property;
        private Object value;

        public Key(Property property, Object value) {
            this.property = property;
            this.value = value;
        }

        protected Property getProperty() {
            return this.property;
        }

        protected Object getValue() {
            return this.value;
        }

        public boolean equals(Object object) {
            try {
                Key key = (Key) object;
                return property == key.getProperty() && value == key.getValue();
            } catch (ClassCastException e) {
                return false;
            }

        }

        public int hashCode() {
            if (value == null) {
                return 0;
            }
            return value.hashCode();
        }
    }
}
