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

import java.util.ArrayList;
import java.util.List;

import org.eclipse.persistence.internal.sessions.CollectionChangeRecord;
import org.eclipse.persistence.internal.sessions.ObjectChangeSet;
import org.eclipse.persistence.mappings.DatabaseMapping;

/**
 * INTERNAL:
 * Capture the changes for an ordered collection where
 * the entire collection is simply replaced if it has changed.
 */
public class EISOrderedCollectionChangeRecord extends CollectionChangeRecord implements org.eclipse.persistence.sessions.changesets.EISOrderedCollectionChangeRecord {

    /** The added stuff. */
    private List adds;

    /** The indexes into the new collection of the elements that were added. */
    private int[] addIndexes;

    /** The moved stuff. */
    private List moves;

    /** The index pairs of the elements that were moved (before and after indexes). */
    private int[][] moveIndexPairs;

    /** The removed stuff. */
    private List removes;

    /** The indexes into the old collection of the elements that were removed. */
    private int[] removeIndexes;

    /**
     * Construct a ChangeRecord that can be used to represent the changes to
     * an ordered collection.
     */
    public EISOrderedCollectionChangeRecord(ObjectChangeSet owner, String attributeName, DatabaseMapping mapping) {
        super();
        this.owner = owner;
        this.attribute = attributeName;
        this.mapping = mapping;
    }

    /**
     * Add an added change set.
     */
    public void addAddedChangeSet(Object changeSet, int index) {
        getAdds().add(changeSet);
        setAddIndexes(this.addTo(index, getAddIndexes()));
    }

    /**
     * Add an moved change set.
     */
    public void addMovedChangeSet(Object changeSet, int oldIndex, int newIndex) {
        getMoves().add(changeSet);
        int[] pair = new int[2];
        pair[0] = oldIndex;
        pair[1] = newIndex;
        setMoveIndexPairs(this.addTo(pair, getMoveIndexPairs()));
    }

    /**
     * Add an removed change set.
     */
    public void addRemovedChangeSet(Object changeSet, int index) {
        getRemoves().add(changeSet);
        setRemoveIndexes(this.addTo(index, getRemoveIndexes()));
    }

    /**
     * Add the int to the end of the array.
     * Return the new array.
     */
    private int[] addTo(int newInt, int[] oldArray) {
        int oldCount = oldArray.length;
        int[] newArray = new int[oldCount + 1];
        System.arraycopy(oldArray, 0, newArray, 0, oldCount);
        newArray[oldCount] = newInt;// zero-based index
        return newArray;
    }

    /**
     * Add the int[] to the end of the array.
     * Return the new array.
     */
    private int[][] addTo(int[] newInts, int[][] oldArray) {
        int oldCount = oldArray.length;
        int[][] newArray = new int[oldCount + 1][];
        System.arraycopy(oldArray, 0, newArray, 0, oldCount);
        newArray[oldCount] = newInts;// zero-based index
        return newArray;
    }

    /**
     * Return the specified add.
     */
    private Object getAdd(int index) {
        return this.getAdds().get(index);
    }

    /**
     * ADVANCED:
     * Return the indexes into the new collection of
     * the elements that were added.
     */
    public int[] getAddIndexes() {
        if (addIndexes == null) {
            addIndexes = new int[0];
        }
        return addIndexes;
    }

    /**
     * ADVANCED:
     * Return the entries for all the elements added to the new collection.
     * The contents of this collection is determined by the mapping that
     * populated it
     */
    public List getAdds() {
        if (adds == null) {
            adds = new ArrayList(2);// keep it as small as possible
        }
        return adds;
    }

    /**
     * Return the index in the adds of the specified change set,
     * without triggering the instantiation of the collection.
     */
    private int getAddsIndexOf(Object changeSet) {
        if (adds == null) {
            return -1;
        }
        return adds.indexOf(changeSet);
    }

    /**
     * Return the number of adds, without triggering
     * the instantiation of the collection.
     */
    private int getAddsSize() {
        if (adds == null) {
            return 0;
        }
        return adds.size();
    }

    /**
     * Return the specified move.
     */
    private Object getMove(int index) {
        return this.getMoves().get(index);
    }

    /**
     * Return the specified "before" move index.
     */
    private int getBeforeMoveIndex(int index) {
        int[][] pairs = getMoveIndexPairs();
        return pairs[index][0];
    }

    /**
     * ADVANCED:
     * Return the indexes of the elements that were simply moved
     * within the collection.
     * Each element in the outer array is another two-element
     * array where the first entry [0] is the index of the object in
     * the old collection and the second entry [1] is the index
     * of the object in the new collection. These two indexes
     * can be equal.
     */
    public int[][] getMoveIndexPairs() {
        if (moveIndexPairs == null) {
            moveIndexPairs = new int[0][0];
        }
        return moveIndexPairs;
    }

    /**
     * ADVANCED:
     * Return the entries for all the elements that were simply shuffled
     * within the collection.
     * The contents of this collection is determined by the mapping that
     * populated it
     */
    public List getMoves() {
        if (moves == null) {
            moves = new ArrayList(2);// keep it as small as possible
        }
        return moves;
    }

    /**
     * Return the index in the moves of the specified change set,
     * without triggering the instantiation of the collection.
     */
    private int getMovesIndexOf(Object changeSet) {
        if (moves == null) {
            return -1;
        }
        return moves.indexOf(changeSet);
    }

    /**
     * Return the number of moves, without triggering
     * the instantiation of the collection.
     */
    private int getMovesSize() {
        if (moves == null) {
            return 0;
        }
        return moves.size();
    }

    /**
     * ADVANCED:
     * Return the entries for all the elements in the new collection.
     * The contents of this collection is determined by the mapping that
     * populated it
     */
    public List getNewCollection() {
        int newSize = getNewCollectionSize();
        List newCollection = new ArrayList(newSize);

        int[] localAddIndexes = addIndexes;
        if (localAddIndexes == null) {
            localAddIndexes = new int[0];
        }

        int[][] localMoveIndexPairs = moveIndexPairs;
        if (localMoveIndexPairs == null) {
            localMoveIndexPairs = new int[0][0];
        }

        int addIndex = 0;
        int moveIndex = 0;
        for (int i = 0; i < newSize; i++) {
            if ((addIndex < localAddIndexes.length) && (localAddIndexes[addIndex] == i)) {
                newCollection.add(this.getAdd(addIndex));
                addIndex++;
                continue;
            }
            if ((moveIndex < localMoveIndexPairs.length) && (localMoveIndexPairs[moveIndex][1] == i)) {
                newCollection.add(this.getMove(moveIndex));
                moveIndex++;
                continue;
            }
            throw new IllegalStateException(String.valueOf(i));
        }
        return newCollection;
    }

    /**
     * Return the number of elements in the new collection,
     * without triggering the instantiation of the collections.
     */
    private int getNewCollectionSize() {
        return this.getAddsSize() + this.getMovesSize();
    }

    /**
     * Return the specified remove index.
     */
    private int getRemoveIndex(int index) {
        return this.getRemoveIndexes()[index];
    }

    /**
     * ADVANCED:
     * Return the indexes into the old collection of
     * the elements that were removed.
     */
    public int[] getRemoveIndexes() {
        if (removeIndexes == null) {
            removeIndexes = new int[0];
        }
        return removeIndexes;
    }

    /**
     * ADVANCED:
     * Return the entries for all the elements removed from the old collection.
     * The contents of this collection is determined by the mapping that
     * populated it
     */
    public List getRemoves() {
        if (removes == null) {
            removes = new ArrayList(2);// keep it as small as possible
        }
        return removes;
    }

    /**
     * Return the index in the removes of the specified change set,
     * without triggering the instantiation of the collection.
     */
    private int getRemovesIndexOf(Object changeSet) {
        if (removes == null) {
            return -1;
        }
        return removes.indexOf(changeSet);
    }

    /**
     * Return whether any adds have been recorded with the change record.
     * Directly reference the instance variable, so as to not trigger the lazy instantiation.
     */
    private boolean hasAdds() {
        return (addIndexes != null) && (addIndexes.length != 0);
    }

    /**
     * Return whether any changes have been recorded with the change record.
     */
    public boolean hasChanges() {
        if (this.hasAdds() || this.hasRemoves() || this.getOwner().isNew()) {
            return true;
        }

        // BUG#.... the moves always contain everything, must check if any indexes are different.
        if (hasMoves()) {
            for (int index = 0; index < moveIndexPairs.length; index++) {
                if (moveIndexPairs[index][0] != moveIndexPairs[index][1]) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Return whether any moves have been recorded with the change record.
     * Directly reference the instance variable, so as to not trigger the lazy instantiation.
     */
    private boolean hasMoves() {
        return (moveIndexPairs != null) && (moveIndexPairs.length != 0);
    }

    /**
     * Return whether any removes have been recorded with the change record.
     * Directly reference the instance variable, so as to not trigger the lazy instantiation.
     */
    private boolean hasRemoves() {
        return (removeIndexes != null) && (removeIndexes.length != 0);
    }

    /**
     * Remove the specified slot from the array.
     * Return the new array.
     */
    private int[] removeFrom(int removeIndex, int[] oldArray) {
        int oldCount = oldArray.length;
        int[] newArray = new int[oldCount - 1];
        System.arraycopy(oldArray, 0, newArray, 0, removeIndex);
        System.arraycopy(oldArray, removeIndex + 1, newArray, removeIndex, oldCount - removeIndex - 1);
        return newArray;
    }

    /**
     * Remove the specified slot from the array.
     * Return the new array.
     */
    private int[][] removeFrom(int removeIndex, int[][] oldArray) {
        int oldCount = oldArray.length;
        int[][] newArray = new int[oldCount - 1][];
        System.arraycopy(oldArray, 0, newArray, 0, removeIndex);
        System.arraycopy(oldArray, removeIndex + 1, newArray, removeIndex, oldCount - removeIndex - 1);
        return newArray;
    }

    /**
     * The specified change set was added earlier;
     * cancel it out.
     */
    private void cancelAddedChangeSet(Object changeSet) {
        int changeSetIndex = this.getAddsIndexOf(changeSet);
        if (changeSetIndex == -1) {
            throw new IllegalStateException(changeSet.toString());
        }
        this.getAdds().remove(changeSetIndex);
        this.setAddIndexes(this.removeFrom(changeSetIndex, this.getAddIndexes()));
    }

    /**
     * Attempt to remove the specified change set
     * from the collection of moved change sets.
     * Return true if the change set was moved earlier
     * and was successfully removed.
     */
    private boolean removeMovedChangeSet(Object changeSet) {
        int changeSetIndex = this.getMovesIndexOf(changeSet);
        if (changeSetIndex == -1) {
            return false;
        }
        this.getMoves().remove(changeSetIndex);
        int beforeMoveIndex = this.getBeforeMoveIndex(changeSetIndex);
        this.setMoveIndexPairs(this.removeFrom(changeSetIndex, this.getMoveIndexPairs()));
        // now move the change set over to the collection of removes
        this.addRemovedChangeSet(changeSet, beforeMoveIndex);
        return true;
    }

    /**
     * Attempt to restore the specified change set.
     * Return true if the change set was removed earlier
     * and was successfully restored to the end of
     * the collection.
     */
    private boolean restoreRemovedChangeSet(Object changeSet) {
        int changeSetIndex = this.getRemovesIndexOf(changeSet);
        if (changeSetIndex == -1) {
            return false;
        }
        this.getRemoves().remove(changeSetIndex);
        int removeIndex = this.getRemoveIndex(changeSetIndex);
        this.setRemoveIndexes(this.removeFrom(changeSetIndex, this.getRemoveIndexes()));
        // now move the change set over to the collection of moves
        this.addMovedChangeSet(changeSet, removeIndex, this.getNewCollectionSize());
        return true;
    }

    /**
     * Set the indexes into the new collection of
     * the elements that were added.
     */
    private void setAddIndexes(int[] addIndexes) {
        this.addIndexes = addIndexes;
    }

    /**
     * Set the indexes of the elements that were moved.
     */
    private void setMoveIndexPairs(int[][] moveIndexPairs) {
        this.moveIndexPairs = moveIndexPairs;
    }

    /**
     * Set the indexes into the old collection of
     * the elements that were removed.
     */
    private void setRemoveIndexes(int[] removeIndexes) {
        this.removeIndexes = removeIndexes;
    }

    /**
     * Add a change set after it has been applied.
     */
    public void simpleAddChangeSet(Object changeSet) {
        // check whether the change set was removed earlier
        if (!this.restoreRemovedChangeSet(changeSet)) {
            // the change set is tacked on the end of the new collection
            this.addAddedChangeSet(changeSet, this.getNewCollectionSize());
        }
    }

    /**
     * Remove a change set after it has been applied.
     */
    public void simpleRemoveChangeSet(Object changeSet) {
        // the change set must have been either moved or added earlier
        if (!this.removeMovedChangeSet(changeSet)) {
            this.cancelAddedChangeSet(changeSet);
        }
    }
}
