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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.changetracking.CollectionChangeEvent;
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.internal.queries.ContainerPolicy;

/**
 * <p>
 * <b>Purpose</b>: This class holds the record of the changes made to a collection attribute of
 * an object.
 * <p>
 * <b>Description</b>: Collections must be compared to each other and added and removed objects must
 * be recorded separately.
 */
public class CollectionChangeRecord extends DeferrableChangeRecord implements org.eclipse.persistence.sessions.changesets.CollectionChangeRecord {

    /**
     * Contains the added values to the collection and their corresponding ChangeSets.
     */
    protected Map<ObjectChangeSet, ObjectChangeSet> addObjectList;

    /**
     * Contains the added values to the collection and their corresponding ChangeSets in order.
     */
    protected List<ObjectChangeSet> orderedAddObjects;

    /**
     * Contains the added values index to the collection.
     */
    protected Map<ObjectChangeSet, Integer> orderedAddObjectIndices;

    /**
     * Contains OrderedChangeObjects representing each change made to the collection.
     */
    protected List<OrderedChangeObject> orderedChangeObjectList;

    /**
     * Contains the removed values to the collection and their corresponding ChangeSets.
     */
    protected Map<Integer, ObjectChangeSet> orderedRemoveObjects;

    /**
     * Contains the removed values index to the collection.
     */
    protected transient List<Integer> orderedRemoveObjectIndices;

    /**
     * Contains a list of extra adds.  These extra adds are used by attribute change tracking
     * to replicate behavior when someone adds the same object to a list and removes it once.
     * In this case the object should still appear once in the change set.
     */
     protected transient List<ObjectChangeSet> addOverFlow;

    /**
     * Contains the removed values from the collection and their corresponding ChangeSets.
     */
    protected Map<ObjectChangeSet, ObjectChangeSet> removeObjectList;

    /**
     * Indicates whether IndirectList's order has been repaired.
     */
    protected boolean orderHasBeenRepaired;

    /**
     * This default constructor.
     */
    public CollectionChangeRecord() {
        super();
    }

    /**
     * Constructor for the ChangeRecord representing a collection mapping
     * @param owner the changeSet that uses this record
     */
    public CollectionChangeRecord(ObjectChangeSet owner) {
        this.owner = owner;
    }

    /**
     * This method takes a Map of objects, converts these into ObjectChangeSets.
     */
    public void addAdditionChange(Map objectChanges, ContainerPolicy cp, UnitOfWorkChangeSet changeSet, AbstractSession session) {
        Iterator enumtr = objectChanges.values().iterator();
        while (enumtr.hasNext()) {
            Object object = cp.unwrapElement(enumtr.next());
            ObjectChangeSet change = session.getDescriptor(object.getClass()).getObjectBuilder().createObjectChangeSet(object, changeSet, session);
            if (change.hasKeys()){
                // if change set has keys this is a map comparison.  Maps are
                // not supported in change tracking so do not need to prevent duplicates
                // when map support is added this will have to be refactored
                getAddObjectList().put(change, change);
            } else {
                if (getRemoveObjectList().containsKey(change)) {
                    getRemoveObjectList().remove(change);
                } else {
                    getAddObjectList().put(change, change);
                }
            }
        }
    }

    /**
     * This method takes a list of objects and converts them into
     * ObjectChangeSets. This method should only be called from a
     * ListContainerPolicy. Additions to the list are made by index, hence,
     * the second Map of objectChangesIndices.
     */
    public void addOrderedAdditionChange(List<Object> orderedObjectsToAdd, Map<Object, Integer> objectChangesIndices, UnitOfWorkChangeSet changeSet, AbstractSession session) {
        for (Object object : orderedObjectsToAdd) {
            ObjectChangeSet change = session.getDescriptor(object.getClass()).getObjectBuilder().createObjectChangeSet(object, changeSet, session);
            getOrderedAddObjects().add(change);
            getOrderedAddObjectIndices().put(change, objectChangesIndices.get(object));
        }
    }

    /**
     * This method takes a map of objects and converts them into
     * ObjectChangeSets. This method should only be called from a
     * ListContainerPolicy. Deletions from the list is made by index, hence,
     * the second Vector of indicesToRemove.
     */
    public void addOrderedRemoveChange(List<Integer> indicesToRemove, Map objectChanges, UnitOfWorkChangeSet changeSet, AbstractSession session) {
        this.orderedRemoveObjectIndices = indicesToRemove;

        for (Integer index : indicesToRemove) {
            Object object = objectChanges.get(index);
            ObjectChangeSet change = session.getDescriptor(object.getClass()).getObjectBuilder().createObjectChangeSet(object, changeSet, session);
            getOrderedRemoveObjects().put(index, change);
        }
    }

    /**
     * This method takes a Map of objects, converts these into ObjectChangeSets.
     */
    public void addRemoveChange(Map objectChanges, ContainerPolicy cp, UnitOfWorkChangeSet changeSet, AbstractSession session) {
        // There is no need to keep track of removed new objects because it will not be in the backup,
        // It will not be in the backup because it is new.
        if(objectChanges.isEmpty()) {
            return;
        }
        ClassDescriptor descriptor = this.mapping.getReferenceDescriptor();
        boolean hasChildren = (descriptor.hasInheritance() && descriptor.getInheritancePolicy().hasChildren())
                || descriptor.hasTablePerClassPolicy();
        Iterator enumtr = cp.getChangeValuesFrom(objectChanges);
        while (enumtr.hasNext()) {
            Object object = cp.unwrapElement(enumtr.next());
            if (hasChildren) {
                descriptor = getReferenceDescriptor(object, session);
            }
            ObjectChangeSet change = descriptor.getObjectBuilder().createObjectChangeSet(object, changeSet, session);
            if (change.hasKeys()) {
                // if change set has keys this is a map comparison.  Maps are
                // not support in change tracking so do not need to prevent duplicates
                // when map support is added this will have to be refactored
                getRemoveObjectList().put(change, change);
            } else {
                if (getAddObjectList().containsKey(change)) {
                    getAddObjectList().remove(change);
                } else {
                    getRemoveObjectList().put(change, change);
                }
            }
        }
    }

    /**
     * ADVANCED:
     * This method returns the collection of ChangeSets that were added to the collection.
     */
    @Override
    public Map<ObjectChangeSet, ObjectChangeSet> getAddObjectList() {
        if (addObjectList == null) {
            addObjectList = new IdentityHashMap(10);
        }
        return addObjectList;
    }

    /**
     * Returns a list of extra adds.
     * These extra adds are used by attribute change tracking
     * to replicate behavior when someone adds the same object to a list and removes it once.
     * In this case the object should still appear once in the change set.
     */
    public List<ObjectChangeSet> getAddOverFlow() {
        if (addOverFlow == null) {
            addOverFlow = new ArrayList();
        }
        return addOverFlow;
    }

    /**
     * Returns descriptor corresponding to the object.
     */
    ClassDescriptor getReferenceDescriptor(Object object, AbstractSession session) {
        return session.getClassDescriptor(object);
    }

    /**
     * PUBLIC:
     * This method returns the Map that contains the removed values from the collection
     * and their corresponding ChangeSets.
     */
    @Override
    public Map<ObjectChangeSet, ObjectChangeSet> getRemoveObjectList() {
        if (removeObjectList == null) {
            removeObjectList = new IdentityHashMap();
        }
        return removeObjectList;
    }

    /**
     * PUBLIC:
     * Returns true if the change set has changes.
     */
    @Override
    public boolean hasChanges() {
        return (!(  (this.addObjectList == null || this.addObjectList.isEmpty()) &&
                    (this.removeObjectList == null || this.removeObjectList.isEmpty()) &&
                    (this.orderedAddObjects == null || this.orderedAddObjects.isEmpty()) &&
                    (this.orderedRemoveObjects == null || this.orderedRemoveObjects.isEmpty()) &&
                    (this.orderedChangeObjectList == null || this.orderedChangeObjectList.isEmpty())))
                || getOwner().isNew();
    }

    /**
     * This method will be used to merge one record into another.
     */
    @Override
    public void mergeRecord(ChangeRecord mergeFromRecord, UnitOfWorkChangeSet mergeToChangeSet, UnitOfWorkChangeSet mergeFromChangeSet) {
        if (((DeferrableChangeRecord)mergeFromRecord).isDeferred()){
            if (this.hasChanges()){
                //merging into existing change record need to combine changes
                ((DeferrableChangeRecord)mergeFromRecord).getMapping().calculateDeferredChanges(mergeFromRecord, mergeToChangeSet.getSession());
            }else{
                if (! this.isDeferred){
                    this.originalCollection = ((DeferrableChangeRecord)mergeFromRecord).originalCollection;
                }
                this.isDeferred = true;
                this.latestCollection = ((DeferrableChangeRecord)mergeFromRecord).latestCollection;
                return;
            }
        }
        Map<ObjectChangeSet, ObjectChangeSet> changeSets = new HashMap<>();
        Iterator addEnum = ((CollectionChangeRecord)mergeFromRecord).getAddObjectList().keySet().iterator();
        while (addEnum.hasNext()) {
            ObjectChangeSet mergingObject = (ObjectChangeSet)addEnum.next();
            ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet(mergingObject, mergeFromChangeSet);
            if (getRemoveObjectList().containsKey(localChangeSet)) {
                getRemoveObjectList().remove(localChangeSet);
            } else {
                changeSets.put(localChangeSet, localChangeSet);
            }
        }
        getAddObjectList().putAll(changeSets);
        changeSets = new HashMap<>();
        Iterator removeEnum = ((CollectionChangeRecord)mergeFromRecord).getRemoveObjectList().keySet().iterator();
        while (removeEnum.hasNext()) {
            ObjectChangeSet mergingObject = (ObjectChangeSet)removeEnum.next();
            ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet(mergingObject, mergeFromChangeSet);
            if (getAddObjectList().containsKey(localChangeSet)) {
                getAddObjectList().remove(localChangeSet);
            } else {
                changeSets.put(localChangeSet, localChangeSet);
            }
        }
        getRemoveObjectList().putAll(changeSets);
        //237545: merge the changes for ordered list's attribute change tracking. (still need to check if deferred changes need to be merged)
        List<OrderedChangeObject> orderedChangeSets = new ArrayList<>();
        Iterator orderedChangeObjectEnum = ((CollectionChangeRecord)mergeFromRecord).getOrderedChangeObjectList().iterator();
        while (orderedChangeObjectEnum.hasNext()) {
            OrderedChangeObject changeObject = (OrderedChangeObject)orderedChangeObjectEnum.next();
            ObjectChangeSet mergingObject = changeObject.getChangeSet();
            ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet(mergingObject, mergeFromChangeSet);

            OrderedChangeObject orderedChangeObject = new OrderedChangeObject(changeObject.getChangeType(), changeObject.getIndex(), localChangeSet);;
            orderedChangeSets.add(orderedChangeObject);
        }
        getOrderedChangeObjectList().addAll(orderedChangeSets);
    }

    /**
     * Sets the Added objects list.
     */
    public void setAddObjectList(Map<ObjectChangeSet, ObjectChangeSet> objectChangesList) {
        this.addObjectList = objectChangesList;
    }

    /**
     * Sets the removed objects list.
     */
    public void setRemoveObjectList(Map<ObjectChangeSet, ObjectChangeSet> objectChangesList) {
        this.removeObjectList = objectChangesList;
    }

    /**
     * This method will be used to update the objectsChangeSets references.
     */
    @Override
    public void updateReferences(UnitOfWorkChangeSet mergeToChangeSet, UnitOfWorkChangeSet mergeFromChangeSet) {
        Map addList = new IdentityHashMap(this.getAddObjectList().size() + 1);
        Map removeList = new IdentityHashMap(this.getRemoveObjectList().size() + 1);
        // If we have ordered lists we need to iterate through those.
        if (getOrderedAddObjects().size() > 0 || getOrderedRemoveObjectIndices().size() > 0) {
            // Do the ordered adds first ...
            List<ObjectChangeSet> orderedAddList = new ArrayList(getOrderedAddObjects().size());
            Map orderedAddListIndices = new IdentityHashMap(getOrderedAddObjectIndices().size());

            for (int i = 0; i < getOrderedAddObjects().size(); i++) {
                ObjectChangeSet changeSet = getOrderedAddObjects().get(i);
                ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet(changeSet, mergeFromChangeSet);

                orderedAddList.add(localChangeSet);
                orderedAddListIndices.put(localChangeSet, getOrderedAddObjectIndices().get(changeSet));

                // Object was actually added and not moved.
                if (getAddObjectList().containsKey(changeSet)) {
                    addList.put(localChangeSet, localChangeSet);
                }
            }

            setOrderedAddObjects(orderedAddList);
            setOrderedAddObjectIndices(orderedAddListIndices);

            // Do the ordered removes now ...
            Map orderedRemoveList = new HashMap(getOrderedRemoveObjects().size());
            for (Object index : getOrderedRemoveObjects().keySet()) {
                ObjectChangeSet changeSet = getOrderedRemoveObjects().get(index);
                ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet(changeSet, mergeFromChangeSet);

                orderedRemoveList.put(index, localChangeSet);

                // Object was actually removed and not moved.
                if (getRemoveObjectList().containsKey(changeSet)) {
                    removeList.put(localChangeSet, localChangeSet);
                }
            }

            setOrderedRemoveObjects(orderedRemoveList);
            // Don't need to worry about the vector of indices (Integer's), just leave them as is.
        } else {
            Iterator changes = getAddObjectList().values().iterator();
            while (changes.hasNext()) {
                ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet((ObjectChangeSet)changes.next(), mergeFromChangeSet);
                addList.put(localChangeSet, localChangeSet);
            }

            changes = getRemoveObjectList().values().iterator();
            while (changes.hasNext()) {
                ObjectChangeSet localChangeSet = mergeToChangeSet.findOrIntegrateObjectChangeSet((ObjectChangeSet)changes.next(), mergeFromChangeSet);
                removeList.put(localChangeSet, localChangeSet);
            }
        }

        setAddObjectList(addList);
        setRemoveObjectList(removeList);
    }

    /**
     * This method returns the collection of ChangeSets in the order they were
     * added to the collection. This list includes those objects that were
     * moved within the collection.
     */
    public List<ObjectChangeSet> getOrderedAddObjects() {
        if (orderedAddObjects == null) {
            orderedAddObjects = new ArrayList();
        }

        return orderedAddObjects;
    }

    /**
     * This method returns the index of an object added to the collection.
     */
    public Integer getOrderedAddObjectIndex(ObjectChangeSet changes) {
        return getOrderedAddObjectIndices().get(changes);
    }

    /**
     * This method returns the collection of ChangeSets that they were
     * added to the collection.
     */
    public Map<ObjectChangeSet, Integer> getOrderedAddObjectIndices() {
        if (orderedAddObjectIndices == null) {
            orderedAddObjectIndices = new IdentityHashMap();
        }

        return orderedAddObjectIndices;
    }

    /**
     * This method returns the Vector of OrderedChangeObjects. These objects represent
     * all changes made to the collection, and their order in the vector represents the order
     * they were performed.
     */
    public List<OrderedChangeObject> getOrderedChangeObjectList() {
        if (orderedChangeObjectList == null) {
            orderedChangeObjectList = new ArrayList();
        }

        return orderedChangeObjectList;
    }

    /**
     * This method returns the ordered list of indices to remove from the collection.
     */
    public List<Integer> getOrderedRemoveObjectIndices() {
        if (this.orderedRemoveObjectIndices == null) {
            this.orderedRemoveObjectIndices = new ArrayList();
        }

        return this.orderedRemoveObjectIndices;
    }

    /**
     * This method returns the index of an object removed from the collection.
     */
    public Object getOrderedRemoveObject(Integer index) {
        return getOrderedRemoveObjects().get(index);
    }

    /**
     * This method returns the collection of ChangeSets of objects removed from
     * the collection.
     */
    public Map<Integer, ObjectChangeSet> getOrderedRemoveObjects() {
        if (this.orderedRemoveObjects == null) {
            this.orderedRemoveObjects = new HashMap();
        }

        return this.orderedRemoveObjects;
    }

    /**
     * Sets collection of ChangeSets (and their respective index) that they
     * were added to the collection.
     */
    public void setOrderedAddObjectIndices(Map<ObjectChangeSet, Integer> orderedAddObjectIndices) {
        this.orderedAddObjectIndices = orderedAddObjectIndices;
    }

    /**
     * Sets collection of ChangeSets that they were added to the collection.
     */
    public void setOrderedAddObjects(List<ObjectChangeSet> orderedAddObjects) {
        this.orderedAddObjects = orderedAddObjects;
    }

    public void setOrderedChangeObjectList(List<OrderedChangeObject> orderedChangeObjectList) {
        this.orderedChangeObjectList = orderedChangeObjectList;
    }

    /**
     * Sets collection of ChangeSets that they were removed from the collection.
     */
    public void setOrderedRemoveObjects(Map<Integer, ObjectChangeSet> orderedRemoveObjects) {
        this.orderedRemoveObjects = orderedRemoveObjects;
    }

    /**
     * The same size as original list,
     * at the i-th position holds the index of the i-th original object in the current list (-1 if the object was removed):
     * for example: {0, -1, 1, -1, 3} means that:
     *   previous(0) == current(0);
     *   previous(1) was removed;
     *   previous(2) == current(1);
     *   previous(3) was removed;
     *   previous(4) == current(3);
     */
    public List<Integer> getCurrentIndexesOfOriginalObjects(List newList) {
        int newSize = newList.size();
        List<Integer> currentIndexes = new ArrayList(newSize);
        for(int i=0; i < newSize; i++) {
            currentIndexes.add(i);
        }
        if(orderedChangeObjectList != null) {
            for (int i = this.orderedChangeObjectList.size() - 1; i>=0; i--) {
                OrderedChangeObject  orderedChange = orderedChangeObjectList.get(i);
                Object obj = orderedChange.getAddedOrRemovedObject();
                Integer index = orderedChange.getIndex();
                int changeType = orderedChange.getChangeType();
                if(changeType == CollectionChangeEvent.ADD) {
                    // the object was added - remove the corresponding index
                    if(index == null) {
                        currentIndexes.remove(currentIndexes.size()-1);
                    } else {
                        currentIndexes.remove(index.intValue());
                    }
                } else if(changeType == CollectionChangeEvent.REMOVE) {
                    // the object was removed - add its index in the new list
                    if(index == null) {
                        throw ValidationException.collectionRemoveEventWithNoIndex(getMapping());
                    } else {
                        currentIndexes.add(index.intValue(), newList.indexOf(obj));
                    }
                }
            }
        }
        return currentIndexes;
    }

   /**
    * Recreates the original state of currentCollection.
    */
    @Override
   public void internalRecreateOriginalCollection(Object currentCollection, AbstractSession session) {
       ContainerPolicy cp = this.mapping.getContainerPolicy();
       if(orderedChangeObjectList == null || orderedChangeObjectList.isEmpty()) {
           if(this.removeObjectList != null) {
               Iterator it = this.removeObjectList.keySet().iterator();
               while(it.hasNext()) {
                   ObjectChangeSet changeSet = (ObjectChangeSet)it.next();
                   cp.addInto(changeSet.getUnitOfWorkClone(), currentCollection, session);
               }
           }
           if(this.addObjectList != null) {
               Iterator it = this.addObjectList.keySet().iterator();
               while(it.hasNext()) {
                   ObjectChangeSet changeSet = (ObjectChangeSet)it.next();
                   cp.removeFrom(changeSet.getUnitOfWorkClone(), currentCollection, session);
               }
           }
       } else {
           List originalList = (List)currentCollection;
           for (int i = this.orderedChangeObjectList.size() - 1; i>=0; i--) {
               OrderedChangeObject  orderedChange = this.orderedChangeObjectList.get(i);
               Object obj = orderedChange.getAddedOrRemovedObject();
               Integer index = orderedChange.getIndex();
               int changeType = orderedChange.getChangeType();
               if(changeType == CollectionChangeEvent.ADD) {
                   // the object was added - remove the corresponding index
                   if(index == null) {
                       originalList.remove(originalList.size()-1);
                   } else {
                       originalList.remove(index.intValue());
                   }
               } else if(changeType == CollectionChangeEvent.REMOVE) {
                   // the object was removed - add its index in the new list
                   if(index == null) {
                       throw ValidationException.collectionRemoveEventWithNoIndex(getMapping());
                   } else {
                       originalList.add(index.intValue(), obj);
                   }
               }
           }
       }
   }

   public void setOrderHasBeenRepaired(boolean hasBeenRepaired) {
       this.orderHasBeenRepaired = hasBeenRepaired;
   }
   public boolean orderHasBeenRepaired() {
       return this.orderHasBeenRepaired;
   }

   /**
    * Clears info about added / removed objects set by change tracker.
    */
    @Override
   public void clearChanges() {
       if(orderedChangeObjectList != null) {
           this.orderedChangeObjectList.clear();
       }
       if(this.removeObjectList != null) {
           this.removeObjectList.clear();
       }
       if(this.addObjectList != null) {
           this.addObjectList.clear();
       }
   }
}
