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

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 DirectCollectionChangeRecord extends DeferrableChangeRecord implements org.eclipse.persistence.sessions.changesets.DirectCollectionChangeRecord {
    protected HashMap addObjectMap;
    protected HashMap removeObjectMap;
    /**
     * Contains the number of objects that must be inserted to once the value is removed
     * in the database as a delete where value = "value" will remove all instances
     * of that value in the database not just one.
     */
    protected HashMap commitAddMap;

    /**
     * Used only in case listOrderField != null in the mapping.
     * Maps each object which has been added or removed or which order in the list has changed
     * to an array of two (non-intersecting) sets of indexes - old and new.
     */
    protected Map changedIndexes;
    protected int oldSize;
    protected int newSize;

    protected boolean isFirstToAddAlreadyInCollection;
    protected boolean isFirstToRemoveAlreadyOutCollection;

    protected boolean isFirstToAdd = true;
    protected boolean isFirstToRemove = true;

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

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

    /**
     * This constructor returns a changeRecord representing the DirectCollection mapping.
     */
    public DirectCollectionChangeRecord(ObjectChangeSet owner) {
        this.owner = owner;
    }

    /**
     * This method takes a map of primitive objects and adds them to the add list.
     * The map stores the number of times the object is in the list.
     */
    public void addAdditionChange(HashMap additions, HashMap databaseCount) {
        Iterator enumtr = additions.keySet().iterator();
        while (enumtr.hasNext()) {
            Object object = enumtr.next();
            if (databaseCount.containsKey(object)){
                getCommitAddMap().put(object, databaseCount.get(object));
            }
            addAdditionChange(object, (Integer)additions.get(object));
        }
    }

    /**
     * This method takes a single addition value and records it.
     */
    public void addAdditionChange(Object key, Integer count){
        if (getRemoveObjectMap().containsKey(key)) {
            int removeValue = ((Integer)getRemoveObjectMap().get(key)).intValue();
            int addition = count.intValue();
            int result = removeValue - addition;
            if (result > 0 ) { // more removes still
                getRemoveObjectMap().put(key, Integer.valueOf(result));
            } else if (result < 0) { // more adds now
                getRemoveObjectMap().remove(key);
                getAddObjectMap().put(key, Integer.valueOf(Math.abs(result)));
            } else { // equal
                getRemoveObjectMap().remove(key);
            }
        } else {
            if (this.getAddObjectMap().containsKey(key)) {
                int addValue = ((Integer)this.getAddObjectMap().get(key)).intValue();
                addValue += count.intValue();
                this.getAddObjectMap().put(key, Integer.valueOf(addValue));
            } else {
                this.getAddObjectMap().put(key, count);
            }
        }
        if(this.isFirstToAdd) {
            this.isFirstToAdd = false;
            if(this.isFirstToAddAlreadyInCollection) {
                return;
            }
        }
        // this is an attribute change track add keep count
        int addValue = count.intValue();
        int commitValue = 0;
        if (getCommitAddMap().containsKey(key)) {
            commitValue = ((Integer)getCommitAddMap().get(key)).intValue();
        }
        getCommitAddMap().put(key, Integer.valueOf(addValue + commitValue));
    }

    /**
     * This method takes a hashtable of primitive objects and adds them to the remove list.
     * Each reference in the hashtable lists the number of this object that needs to be removed from the
     * collection.
     */
    public void addRemoveChange(HashMap additions, HashMap databaseCount) {
        Iterator enumtr = additions.keySet().iterator();
        while (enumtr.hasNext()) {
            Object object = enumtr.next();
            if (databaseCount.containsKey(object)){
                getCommitAddMap().put(object, databaseCount.get(object));
            }
            addRemoveChange(object, (Integer)additions.get(object));
        }
    }

    /**
     * This method takes a single remove change and integrates it with this changeset.
     */
    public void addRemoveChange(Object key, Integer count){
        if (getAddObjectMap().containsKey(key)) {
            int removeValue = ((Integer)getAddObjectMap().get(key)).intValue();
            int addition = count.intValue();
            int result = removeValue - addition;
            if (result > 0 ) { // more removes still
                getAddObjectMap().put(key, Integer.valueOf(result));
            } else if (result < 0) { // more adds now
                getAddObjectMap().remove(key);
                getRemoveObjectMap().put(key, Integer.valueOf(Math.abs(result)));
            } else { // equal
                getAddObjectMap().remove(key);
            }
        } else {
            if (this.getRemoveObjectMap().containsKey(key)){
                int addValue = ((Integer)this.getRemoveObjectMap().get(key)).intValue();
                addValue += count.intValue();
                this.getRemoveObjectMap().put(key, Integer.valueOf(addValue));
            } else {
                this.getRemoveObjectMap().put(key, count);
            }
        }
        if(this.isFirstToRemove) {
            this.isFirstToRemove = false;
            if(this.isFirstToRemoveAlreadyOutCollection) {
                return;
            }
        }
        int removeValue = count.intValue();
        int commitValue = 0;
        if (getCommitAddMap().containsKey(key)){
            commitValue = ((Integer)getCommitAddMap().get(key)).intValue();
        }
        getCommitAddMap().put(key, Integer.valueOf(commitValue - removeValue));
    }

    /**
     * This method takes a hashtable of primitives and adds them to the commit list.
     * This count value provided is the number of instances that will need to be
     * inserted into the database once a remove has occurred.  This is only set
     * once for each object type.
     */
    public void setCommitAddition(Hashtable additions){
        Enumeration enumtr = additions.keys();
        while (enumtr.hasMoreElements()) {
            Object object = enumtr.nextElement();
            getCommitAddMap().put(object, additions.get(object));
        }
    }

    /**
     * This method will iterate over the collection and store the database counts for
     * the objects within the collection, this is used for minimal updates.
     */
    public void storeDatabaseCounts(Object collection, ContainerPolicy containerPolicy, AbstractSession session){
        Object iterator = containerPolicy.iteratorFor(collection);
        while (containerPolicy.hasNext(iterator)) {
            Object object = containerPolicy.next(iterator, session);
            incrementDatabaseCount(object);
        }
    }

    /**
     * Increment the count for object
     */
    public void incrementDatabaseCount(Object object){
        if (getCommitAddMap().containsKey(object)) {
            int count = ((Integer)getCommitAddMap().get(object)).intValue();
            getCommitAddMap().put(object, Integer.valueOf(++count));
        } else {
            getCommitAddMap().put(object, Integer.valueOf(1));
        }
    }

    /**
     * Decrement the count for object
     */
    public void decrementDatabaseCount(Object object){
        if (getCommitAddMap().containsKey(object)) {
            int count = ((Integer)getCommitAddMap().get(object)).intValue();
            if(count > 1) {
                getCommitAddMap().put(object, Integer.valueOf(--count));
            } else {
                getCommitAddMap().remove(object);
            }
        }
    }

    /**
     * ADVANCED:
     * This method returns the list of added objects.
     */
    public Vector getAddObjectList(){
        Vector vector = new Vector();
        for (Iterator iterator = getAddObjectMap().keySet().iterator(); iterator.hasNext();){
            Object object = iterator.next();
            int count = ((Integer)getAddObjectMap().get(object)).intValue();
            while (count > 0){
                vector.add(object);
                --count;
            }
        }
        return vector;
    }

    /**
     * This method returns the collection of objects that were added to the collection.
     */
    public HashMap getAddObjectMap() {
        if (this.addObjectMap == null) {
            this.addObjectMap = new HashMap(1);
        }
        return addObjectMap;
    }

    /**
     * This method returns the collection of objects that were added to the collection.
     */
    public HashMap getCommitAddMap() {
        if (this.commitAddMap == null) {
            this.commitAddMap = new HashMap(1);
        }
        return commitAddMap;
    }

    /**
     * ADVANCED:
     * This method returns the list of removed objects.
     */
    public Vector getRemoveObjectList(){
        Vector vector = new Vector();
        for (Iterator iterator = getRemoveObjectMap().keySet().iterator(); iterator.hasNext();){
            Object object = iterator.next();
            int count = ((Integer)getRemoveObjectMap().get(object)).intValue();
            while (count > 0){
                vector.add(object);
                --count;
            }
        }
        return vector;
    }

    /**
     * This method returns the collection of objects that were removed from the collection.
     */
    public HashMap getRemoveObjectMap() {
        if (this.removeObjectMap == null) {
            removeObjectMap = new HashMap(1);
        }
        return removeObjectMap;
    }

    /**
     * Returns true if the change set has changes.
     */
    public boolean hasChanges() {
        return (!((this.addObjectMap == null || this.addObjectMap.isEmpty())
            && (this.removeObjectMap == null || this.removeObjectMap.isEmpty())
            && (this.changedIndexes == null || this.changedIndexes.isEmpty()))) || getOwner().isNew();
    }

    /**
     * This method will be used to merge one record into another.
     */
    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{
                this.isDeferred = true;
                this.originalCollection = ((DeferrableChangeRecord)mergeFromRecord).originalCollection;
                this.latestCollection = ((DeferrableChangeRecord)mergeFromRecord).latestCollection;
                return;
            }
        }
        HashMap addMapToMerge = ((DirectCollectionChangeRecord)mergeFromRecord).getAddObjectMap();
        HashMap removeMapToMerge = ((DirectCollectionChangeRecord)mergeFromRecord).getRemoveObjectMap();
        //merge additions
        for (Iterator iterator = addMapToMerge.keySet().iterator(); iterator.hasNext();){
            Object added = iterator.next();
            if (!((DirectCollectionChangeRecord)mergeFromRecord).getCommitAddMap().containsKey(added)){
                // we have not recorded a change of this type in this class before so  add it
                this.getCommitAddMap().put(added, ((DirectCollectionChangeRecord)mergeFromRecord).getCommitAddMap().get(added));
            }
            this.addAdditionChange(added, (Integer)addMapToMerge.get(added));
        }
        //merge removals
        for (Iterator iterator = removeMapToMerge.keySet().iterator(); iterator.hasNext();){
            Object removed = iterator.next();
            if (!((DirectCollectionChangeRecord)mergeFromRecord).getCommitAddMap().containsKey(removed)){
                // we have not recorded a change of this type in this class before so  add it
                this.getCommitAddMap().put(removed, Integer.valueOf(1));
            }
            this.addRemoveChange(removed, (Integer)removeMapToMerge.get(removed));
        }

        if(this.changedIndexes != null) {
            if(((DirectCollectionChangeRecord)mergeFromRecord).getChangedIndexes() != null) {
                Iterator<Map.Entry<Object, Set[]>> itEntries = ((DirectCollectionChangeRecord)mergeFromRecord).getChangedIndexes().entrySet().iterator();
                while(itEntries.hasNext()) {
                    Map.Entry<Object, Set[]> entry = itEntries.next();
                    Object obj = entry.getValue();
                    Set[] indexes = entry.getValue();
                    if(this.changedIndexes.containsKey(obj)) {
                        // we assuming that these are two consecutive change records:
                        // oldIndexes[1] should be equal to newIndexes[0]
                        ((Set[])(this.changedIndexes.get(obj)))[1] = indexes[1];
                    } else {
                        this.changedIndexes.put(obj, indexes);
                    }
                }
                this.newSize = ((DirectCollectionChangeRecord)mergeFromRecord).getNewSize();
            }
        } else {
            if(((DirectCollectionChangeRecord)mergeFromRecord).getChangedIndexes() != null) {
                this.changedIndexes = new HashMap(((DirectCollectionChangeRecord)mergeFromRecord).getChangedIndexes());
                this.oldSize = ((DirectCollectionChangeRecord)mergeFromRecord).getOldSize();
                this.newSize = ((DirectCollectionChangeRecord)mergeFromRecord).getNewSize();
            }
        }
   }

    /**
     * This method will be used to update the objectsChangeSets references
     */
    public void updateReferences(UnitOfWorkChangeSet mergeToChangeSet, UnitOfWorkChangeSet mergeFromChangeSet) {
        //nothing for this record type to do as it does not reference any changesets
    }

    public static class NULL {
        // This is a placeholder for null instances.
        public NULL(){
        }
        public boolean equals(Object object){
            return object instanceof NULL;
        }
    }

    public void setFirstToAddAlreadyInCollection(boolean flag) {
        this.isFirstToAddAlreadyInCollection = flag;
    }
    public boolean isFirstToAddAlreadyInCollection() {
        return this.isFirstToAddAlreadyInCollection;
    }

    public void setFirstToRemoveAlreadyOutCollection(boolean flag) {
        this.isFirstToRemoveAlreadyOutCollection = flag;
    }
    public boolean isFirstToRemoveAlreadyOutCollection() {
        return this.isFirstToRemoveAlreadyOutCollection;
    }

    public void setChangedIndexes(Map changedIndexes) {
        this.changedIndexes = changedIndexes;
    }
    public Map getChangedIndexes() {
        return this.changedIndexes;
    }
    public void setOldSize(int size) {
        this.oldSize = size;
    }
    public int getOldSize() {
        return this.oldSize;
    }
    public void setNewSize(int size) {
        this.newSize = size;
    }
    public int getNewSize() {
        return this.newSize;
    }

   /**
    * Recreates the original state of the collection.
    */
  public void internalRecreateOriginalCollection(Object currentCollection, AbstractSession session) {
      ContainerPolicy cp = this.mapping.getContainerPolicy();
       if(this.removeObjectMap != null) {
           Iterator it = this.removeObjectMap.entrySet().iterator();
           while(it.hasNext()) {
               Map.Entry entry = (Map.Entry)it.next();
               Object obj = entry.getKey();
               int n = (Integer)entry.getValue();
               for(int i=0; i < n; i++) {
                   cp.addInto(obj, currentCollection, session);
               }
           }
       }
       if(this.addObjectMap != null) {
           Iterator it = this.addObjectMap.entrySet().iterator();
           while(it.hasNext()) {
               Map.Entry entry = (Map.Entry)it.next();
               Object obj = entry.getKey();
               int n = (Integer)entry.getValue();
               for(int i=0; i < n; i++) {
                   cp.removeFrom(obj, currentCollection, session);
               }
           }
       }
   }

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

   /**
    * Clears info about added / removed objects set by change tracker.
    */
   public void clearChanges() {
       if(this.removeObjectMap != null) {
           this.removeObjectMap.clear();
       }
       if(this.addObjectMap != null) {
           this.addObjectMap.clear();
       }
       if(this.addObjectMap != null) {
           this.addObjectMap.clear();
       }
       if(this.removeObjectMap != null) {
           this.removeObjectMap.clear();
       }
       if(this.commitAddMap != null) {
           this.commitAddMap.clear();
       }
   }
}
