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

import java.util.List;

import org.eclipse.persistence.eis.EISCollectionChangeRecord;
import org.eclipse.persistence.eis.EISOrderedCollectionChangeRecord;
import org.eclipse.persistence.internal.queries.ContainerPolicy;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.ChangeRecord;
import org.eclipse.persistence.internal.sessions.MergeManager;
import org.eclipse.persistence.internal.sessions.ObjectChangeSet;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.mappings.DatabaseMapping;

/**
 * INTERNAL:
 * Helper class to consolidate all the heinous comparing
 * and merging code for the EIS one to many mappings.
 */
public class EISOneToManyMappingHelper {

    /** The mapping that needs help comparing and merging. */
    private EISOneToManyMapping mapping;
    private static Object XXX = new Object();// object used to marked cleared out slots when comparing

    /**
     * Constructor.
     */
    public EISOneToManyMappingHelper(EISOneToManyMapping mapping) {
        super();
        this.mapping = mapping;
    }

    /**
     * Convenience method.
     */
    private Object buildAddedElementFromChangeSet(Object changeSet, MergeManager mergeManager, AbstractSession targetSession) {
        return this.getMapping().buildAddedElementFromChangeSet(changeSet, mergeManager, targetSession);
    }

    /**
     * Convenience method.
     */
    private Object buildChangeSet(Object element, ObjectChangeSet owner, AbstractSession session) {
        return this.getMapping().buildChangeSet(element, owner, session);
    }

    /**
     * Convenience method.
     */
    private Object buildElementFromElement(Object element, MergeManager mergeManager, AbstractSession targetSession) {
        return this.getMapping().buildElementFromElement(element, mergeManager, targetSession);
    }

    /**
     * Convenience method.
     */
    private Object buildRemovedElementFromChangeSet(Object changeSet, MergeManager mergeManager, AbstractSession targetSession) {
        return this.getMapping().buildRemovedElementFromChangeSet(changeSet, mergeManager, targetSession);
    }

    /**
     * Compare the attributes. Return true if they are alike.
     * Assume the passed-in attributes are non-null.
     */
    private boolean compareAttributeValues(Object collection1, Object collection2, AbstractSession session) {
        ContainerPolicy cp = this.getContainerPolicy();

        if (cp.sizeFor(collection1) != cp.sizeFor(collection2)) {
            return false;
        }

        // if they are both empty, go no further...
        if (cp.sizeFor(collection1) == 0) {
            return true;
        }

        if (cp.hasOrder()) {
            return this.compareAttributeValuesWithOrder(collection1, collection2, session);
        } else {
            return this.compareAttributeValuesWithoutOrder(collection1, collection2, session);
        }
    }

    /**
     * Build and return the change record that results
     * from comparing the two collection attributes.
     * The order of the elements is significant.
     */
    private ChangeRecord compareAttributeValuesForChangeWithOrder(Object cloneCollection, Object backupCollection, ObjectChangeSet owner, AbstractSession session) {
        ContainerPolicy cp = this.getContainerPolicy();

        List cloneVector = cp.vectorFor(cloneCollection, session);// convert it to a Vector so we can preserve the order and use indexes
        List backupVector = cp.vectorFor(backupCollection, session);// "clone" it so we can clear out the slots

        EISOrderedCollectionChangeRecord changeRecord = new EISOrderedCollectionChangeRecord(owner, this.getAttributeName(), this.getDatabaseMapping());

        for (int i = 0; i < cloneVector.size(); i++) {
            Object cloneElement = cloneVector.get(i);
            boolean found = false;
            for (int j = 0; j < backupVector.size(); j++) {
                if (this.compareElementsForChange(cloneElement, backupVector.get(j), session)) {
                    // the clone element was found in the backup collection
                    found = true;
                    backupVector.set(j, XXX);// clear out the matching backup element

                    changeRecord.addMovedChangeSet(this.buildChangeSet(cloneElement, owner, session), j, i);
                    break;// matching backup element found - skip the rest of them
                }
            }
            if (!found) {
                // the clone element was not found, so it must have been added
                changeRecord.addAddedChangeSet(this.buildChangeSet(cloneElement, owner, session), i);
            }
        }

        for (int i = 0; i < backupVector.size(); i++) {
            Object backupElement = backupVector.get(i);
            if (backupElement != XXX) {
                // the backup element was not in the clone collection, so it must have been removed
                changeRecord.addRemovedChangeSet(this.buildChangeSet(backupElement, owner, session), i);
            }
        }

        if (changeRecord.hasChanges()) {
            return changeRecord;
        } else {
            return null;
        }
    }

    /**
     * Build and return the change record that results
     * from comparing the two collection attributes.
     * Ignore the order of the elements.
     */
    private ChangeRecord compareAttributeValuesForChangeWithoutOrder(Object cloneCollection, Object backupCollection, ObjectChangeSet owner, AbstractSession session) {
        ContainerPolicy cp = this.getContainerPolicy();

        List backupVector = cp.vectorFor(backupCollection, session);// "clone" it so we can clear out the slots

        EISCollectionChangeRecord changeRecord = new EISCollectionChangeRecord(owner, this.getAttributeName(), this.getDatabaseMapping());
        for (Object cloneIter = cp.iteratorFor(cloneCollection); cp.hasNext(cloneIter);) {
            Object cloneElement = cp.next(cloneIter, session);

            boolean found = false;
            for (int i = 0; i < backupVector.size(); i++) {
                if (this.compareElementsForChange(cloneElement, backupVector.get(i), session)) {
                    // the clone element was found in the backup collection
                    found = true;
                    backupVector.set(i, XXX);// clear out the matching backup element
                    if (this.mapKeyHasChanged(cloneElement, session)) {
                        changeRecord.addChangedMapKeyChangeSet(this.buildChangeSet(cloneElement, owner, session));
                    }
                    break;// matching backup element found - skip the rest of them
                }
            }
            if (!found) {
                // the clone element was not found, so it must have been added
                changeRecord.addAddedChangeSet(this.buildChangeSet(cloneElement, owner, session));
            }
        }

        for (int i = 0; i < backupVector.size(); i++) {
            Object backupElement = backupVector.get(i);
            if (backupElement != XXX) {
                // the backup element was not in the clone collection, so it must have been removed
                changeRecord.addRemovedChangeSet(this.buildChangeSet(backupElement, owner, session));
            }
        }

        if (changeRecord.hasChanges()) {
            return changeRecord;
        } else {
            return null;
        }
    }

    /**
     * Compare the attributes. Return true if they are alike.
     * The order of the elements is significant.
     */
    private boolean compareAttributeValuesWithOrder(Object collection1, Object collection2, AbstractSession session) {
        ContainerPolicy cp = this.getContainerPolicy();

        Object iter1 = cp.iteratorFor(collection1);
        Object iter2 = cp.iteratorFor(collection2);

        while (cp.hasNext(iter1)) {
            if (!this.compareElements(cp.next(iter1, session), cp.next(iter2, session), session)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Compare the attributes. Return true if they are alike.
     * Ignore the order of the elements.
     */
    private boolean compareAttributeValuesWithoutOrder(Object collection1, Object collection2, AbstractSession session) {
        ContainerPolicy cp = this.getContainerPolicy();

        List vector2 = cp.vectorFor(collection2, session);// "clone" it so we can clear out the slots

        for (Object iter1 = cp.iteratorFor(collection1); cp.hasNext(iter1);) {
            Object element1 = cp.next(iter1, session);

            boolean found = false;
            for (int i = 0; i < vector2.size(); i++) {
                if (this.compareElements(element1, vector2.get(i), session)) {
                    found = true;
                    vector2.set(i, XXX);// clear out the matching element
                    break;// matching element found - skip the rest of them
                }
            }
            if (!found) {
                return false;
            }
        }

        // look for elements that were not in collection1
        for (Object value : vector2) {
            if (value != XXX) {
                return false;
            }
        }
        return true;
    }

    /**
     * Convenience method.
     * Check for null values before delegating to the mapping.
     */
    private boolean compareElements(Object element1, Object element2, AbstractSession session) {
        if ((element1 == null) && (element2 == null)) {
            return true;
        }
        if ((element1 == null) || (element2 == null)) {
            return false;
        }
        if (element2 == XXX) {// if element2 was marked as cleared out, it is not a match
            return false;
        }
        return this.getMapping().compareElements(element1, element2, session);
    }

    /**
     * Convenience method.
     * Check for null values before delegating to the mapping.
     */
    private boolean compareElementsForChange(Object element1, Object element2, AbstractSession session) {
        if ((element1 == null) && (element2 == null)) {
            return true;
        }
        if ((element1 == null) || (element2 == null)) {
            return false;
        }
        if (element2 == XXX) {// if element2 was marked as cleared out, it is not a match
            return false;
        }
        return this.getMapping().compareElementsForChange(element1, element2, session);
    }

    /**
     * INTERNAL:
     * Build and return the change record that results
     * from comparing the two collection attributes.
     */
    public ChangeRecord compareForChange(Object clone, Object backup, ObjectChangeSet owner, AbstractSession session) {
        ContainerPolicy cp = this.getContainerPolicy();
        Object cloneCollection = this.getRealCollectionAttributeValueFromObject(clone, session);

        Object backupCollection = null;
        if (owner.isNew()) {
            backupCollection = cp.containerInstance(1);
        } else {
            backupCollection = this.getRealCollectionAttributeValueFromObject(backup, session);
        }

        if (cp.hasOrder()) {
            return this.compareAttributeValuesForChangeWithOrder(cloneCollection, backupCollection, owner, session);
        } else {
            return this.compareAttributeValuesForChangeWithoutOrder(cloneCollection, backupCollection, owner, session);
        }
    }

    /**
     * INTERNAL:
     * Compare the attributes belonging to this mapping for the objects.
     */
    public boolean compareObjects(Object object1, Object object2, AbstractSession session) {
        return this.compareAttributeValues(this.getRealCollectionAttributeValueFromObject(object1, session), this.getRealCollectionAttributeValueFromObject(object2, session), session);
    }

    /**
     * Convenience method.
     */
    private String getAttributeName() {
        return this.getMapping().getAttributeName();
    }

    /**
     * Convenience method.
     */
    private ContainerPolicy getContainerPolicy() {
        return this.getMapping().getContainerPolicy();
    }

    /**
     * INTERNAL:
     * Return the mapping, casted a bit more generally.
     */
    public DatabaseMapping getDatabaseMapping() {
        return this.getMapping();
    }

    /**
     * INTERNAL:
     * Return the mapping.
     */
    public EISOneToManyMapping getMapping() {
        return mapping;
    }

    /**
     * Convenience method.
     */
    private Object getRealCollectionAttributeValueFromObject(Object object, AbstractSession session) {
        return this.getMapping().getRealCollectionAttributeValueFromObject(object, session);
    }

    /**
     * Convenience method.
     */
    private boolean mapKeyHasChanged(Object element, AbstractSession session) {
        return this.getMapping().mapKeyHasChanged(element, session);
    }

    /**
     * INTERNAL:
     * Merge changes from the source to the target object.
     */
    public void mergeChangesIntoObject(Object target, ChangeRecord changeRecord, Object source, MergeManager mergeManager, AbstractSession targetSession) {
        if (this.getContainerPolicy().hasOrder()) {
            this.mergeChangesIntoObjectWithOrder(target, changeRecord, source, mergeManager, targetSession);
        } else {
            this.mergeChangesIntoObjectWithoutOrder(target, changeRecord, source, mergeManager, targetSession);
        }
    }

    /**
     * Merge changes from the source to the target object.
     * Simply replace the entire target collection.
     */
    private void mergeChangesIntoObjectWithOrder(Object target, ChangeRecord changeRecord, Object source, MergeManager mergeManager, AbstractSession targetSession) {
        ContainerPolicy cp = this.getContainerPolicy();
        AbstractSession session = mergeManager.getSession();

        List changes = ((EISOrderedCollectionChangeRecord)changeRecord).getNewCollection();
        Object targetCollection = cp.containerInstance(changes.size());

        for (Object changed : changes) {
            Object targetElement = buildAddedElementFromChangeSet(changed, mergeManager, targetSession);
            cp.addInto(targetElement, targetCollection, session);
        }

        // reset the attribute to allow for set method to re-morph changes if the collection is not being stored directly
        this.setRealAttributeValueInObject(target, targetCollection);
    }

    /**
     * Merge changes from the source to the target object.
     * Make the necessary removals and adds and map key modifications.
     */
    private void mergeChangesIntoObjectWithoutOrder(Object target, ChangeRecord changeRecord, Object source, MergeManager mergeManager, AbstractSession targetSession) {
        EISCollectionChangeRecord sdkChangeRecord = (EISCollectionChangeRecord)changeRecord;
        ContainerPolicy cp = this.getContainerPolicy();
        AbstractSession session = mergeManager.getSession();

        Object targetCollection = null;
        if (sdkChangeRecord.getOwner().isNew()) {
            targetCollection = cp.containerInstance(sdkChangeRecord.getAdds().size());
        } else {
            targetCollection = this.getRealCollectionAttributeValueFromObject(target, session);
        }

        List removes = sdkChangeRecord.getRemoves();
        List adds = sdkChangeRecord.getAdds();
        List changedMapKeys = sdkChangeRecord.getChangedMapKeys();

        synchronized (targetCollection) {
            for (Object removed : removes) {
                Object removeElement = buildRemovedElementFromChangeSet(removed, mergeManager, targetSession);

                Object targetElement = null;
                for (Object iter = cp.iteratorFor(targetCollection); cp.hasNext(iter);) {
                    targetElement = cp.next(iter, session);
                    if (this.compareElements(targetElement, removeElement, session)) {
                        break;// matching element found - skip the rest of them
                    }
                }
                if (targetElement != null) {
                    // a matching element was found, remove it
                    cp.removeFrom(targetElement, targetCollection, session);
                }
            }

            for (Object added : adds) {
                Object addElement = buildAddedElementFromChangeSet(added, mergeManager, targetSession);
                cp.addInto(addElement, targetCollection, session);
            }

            for (Object changed : changedMapKeys) {
                Object changedMapKeyElement = buildAddedElementFromChangeSet(changed, mergeManager, targetSession);
                Object originalElement = ((UnitOfWorkImpl)session).getOriginalVersionOfObject(changedMapKeyElement);
                cp.removeFrom(originalElement, targetCollection, session);
                cp.addInto(changedMapKeyElement, targetCollection, session);
            }
        }

        // reset the attribute to allow for set method to re-morph changes if the collection is not being stored directly
        setRealAttributeValueInObject(target, targetCollection);
    }

    /**
     * INTERNAL:
     * Merge changes from the source to the target object.
     * Simply replace the entire target collection.
     */
    public void mergeIntoObject(Object target, boolean isTargetUnInitialized, Object source, MergeManager mergeManager, AbstractSession targetSession) {
        ContainerPolicy cp = this.getContainerPolicy();
        AbstractSession session = mergeManager.getSession();

        Object sourceCollection = this.getRealCollectionAttributeValueFromObject(source, session);
        Object targetCollection = cp.containerInstance(cp.sizeFor(sourceCollection));

        for (Object iter = cp.iteratorFor(sourceCollection); cp.hasNext(iter);) {
            Object targetElement = this.buildElementFromElement(cp.next(iter, session), mergeManager, targetSession);
            cp.addInto(targetElement, targetCollection, session);
        }

        // reset the attribute to allow for set method to re-morph changes if the collection is not being stored directly
        setRealAttributeValueInObject(target, targetCollection);
    }

    /**
     * Convenience method.
     */
    private void setRealAttributeValueInObject(Object object, Object attributeValue) {
        getMapping().setRealAttributeValueInObject(object, attributeValue);
    }

    /**
     * ADVANCED:
     * This method is used to add an object to a collection once the changeSet is applied.
     * The referenceKey parameter should only be used for direct Maps.
     */
    public void simpleAddToCollectionChangeRecord(Object referenceKey, Object changeSetToAdd, ObjectChangeSet changeSet, AbstractSession session) {
        if (this.getContainerPolicy().hasOrder()) {
            this.simpleAddToCollectionChangeRecordWithOrder(referenceKey, changeSetToAdd, changeSet, session);
        } else {
            this.simpleAddToCollectionChangeRecordWithoutOrder(referenceKey, changeSetToAdd, changeSet, session);
        }
    }

    /**
     * Add stuff to an ordered collection.
     */
    private void simpleAddToCollectionChangeRecordWithOrder(Object referenceKey, Object changeSetToAdd, ObjectChangeSet changeSet, AbstractSession session) {
        EISOrderedCollectionChangeRecord changeRecord = (EISOrderedCollectionChangeRecord)changeSet.getChangesForAttributeNamed(this.getAttributeName());
        if (changeRecord == null) {
            changeRecord = new EISOrderedCollectionChangeRecord(changeSet, this.getAttributeName(), this.getDatabaseMapping());
            changeSet.addChange(changeRecord);
        }
        changeRecord.simpleAddChangeSet(changeSetToAdd);
    }

    /**
     * Add stuff to an unordered collection.
     */
    private void simpleAddToCollectionChangeRecordWithoutOrder(Object referenceKey, Object changeSetToAdd, ObjectChangeSet changeSet, AbstractSession session) {
        EISCollectionChangeRecord changeRecord = (EISCollectionChangeRecord)changeSet.getChangesForAttributeNamed(this.getAttributeName());
        if (changeRecord == null) {
            changeRecord = new EISCollectionChangeRecord(changeSet, this.getAttributeName(), this.getDatabaseMapping());
            changeSet.addChange(changeRecord);
        }
        changeRecord.simpleAddChangeSet(changeSetToAdd);
    }

    /**
     * ADVANCED:
     * This method is used to remove an object from a collection once the changeSet is applied.
     * The referenceKey parameter should only be used for direct Maps.
     */
    public void simpleRemoveFromCollectionChangeRecord(Object referenceKey, Object changeSetToRemove, ObjectChangeSet changeSet, AbstractSession session) {
        if (this.getContainerPolicy().hasOrder()) {
            this.simpleRemoveFromCollectionChangeRecordWithOrder(referenceKey, changeSetToRemove, changeSet, session);
        } else {
            this.simpleRemoveFromCollectionChangeRecordWithoutOrder(referenceKey, changeSetToRemove, changeSet, session);
        }
    }

    /**
     * Remove stuff from an ordered collection.
     */
    private void simpleRemoveFromCollectionChangeRecordWithOrder(Object referenceKey, Object changeSetToRemove, ObjectChangeSet changeSet, AbstractSession session) {
        EISOrderedCollectionChangeRecord changeRecord = (EISOrderedCollectionChangeRecord)changeSet.getChangesForAttributeNamed(this.getAttributeName());
        if (changeRecord == null) {
            changeRecord = new EISOrderedCollectionChangeRecord(changeSet, this.getAttributeName(), this.getDatabaseMapping());
            changeSet.addChange(changeRecord);
        }
        changeRecord.simpleRemoveChangeSet(changeSetToRemove);
    }

    /**
     * Remove stuff from an unordered collection.
     */
    private void simpleRemoveFromCollectionChangeRecordWithoutOrder(Object referenceKey, Object changeSetToRemove, ObjectChangeSet changeSet, AbstractSession session) {
        EISCollectionChangeRecord changeRecord = (EISCollectionChangeRecord)changeSet.getChangesForAttributeNamed(this.getAttributeName());
        if (changeRecord == null) {
            changeRecord = new EISCollectionChangeRecord(changeSet, this.getAttributeName(), this.getDatabaseMapping());
            changeSet.addChange(changeRecord);
        }
        changeRecord.simpleRemoveChangeSet(changeSetToRemove);
    }
}
