/*
 * 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 org.eclipse.persistence.indirection.IndirectCollection;
import org.eclipse.persistence.internal.queries.ContainerPolicy;

/**
 * Abstract change record for collection type records that allow deferrable change detection.
 * Used for change tracking when user sets entire collection.
 */
public abstract class DeferrableChangeRecord extends ChangeRecord {

    /**
     * Used for change tracking when user sets entire collection.
     */
    protected transient Object originalCollection;

    /**
     * Used for change tracking when user sets entire collection.
     */
    protected transient Object latestCollection;

    /**
     * Defines if this change should be calculated at commit time using the two collections.
     * This is used to handle collection replacement.
     */
    protected boolean isDeferred = false;

    public DeferrableChangeRecord() {
        super();
    }

    public DeferrableChangeRecord(ObjectChangeSet owner) {
        this.owner = owner;
    }

    /**
     * Returns if this change should be calculated at commit time using the two collections.
     * This is used to handle collection replacement.
     */
    public boolean isDeferred() {
        return isDeferred;
    }

    /**
     * Sets if this change should be calculated at commit time using the two collections.
     * This is used to handle collection replacement.
     */
    public void setIsDeferred(boolean isDeferred) {
        this.isDeferred = isDeferred;
    }

    /**
     * Used for change tracking when user sets entire collection.
     * This is the last collection that was set on the object.
     */
    public Object getLatestCollection() {
        return latestCollection;
    }

    /**
     * Used for change tracking when user sets entire collection.
     * This is the original collection that was set on the object when it was cloned.
     */
    public Object getOriginalCollection() {
        return originalCollection;
    }

    /**
     * Used for change tracking when user sets entire collection.
     * This is the last collection that was set on the object.
     */
    public void setLatestCollection(Object latestCollection) {
        this.latestCollection = latestCollection;
    }

    /**
     * Used for change tracking when user sets entire collection.
     * This is the original collection that was set on the object when it was cloned.
     */
    public void setOriginalCollection(Object originalCollection) {
        this.originalCollection = originalCollection;
    }

    /**
     * Recreates the original state of currentCollection.
     */
    abstract public void internalRecreateOriginalCollection(Object currentCollection, AbstractSession session);

    /**
     * Clears info about added / removed objects set by change tracker.
     * Called after the change info has been already used for creation of originalCollection.
     * Also called to make sure there is no change info before comparison for change is performed
     * (change info is still in the record after comparison for change is performed
     *  and may cause wrong results when the second comparison for change performed on the same change record).
     */
    abstract public void clearChanges();

    /**
     * Recreates the original state of the collection.
     */
    public void recreateOriginalCollection(Object currentCollection, AbstractSession session) {
       if(currentCollection == null) {
           this.setOriginalCollection(null);
           return;
       }
       if(currentCollection instanceof IndirectCollection) {
           // to avoid raising event when we add/remove elements from this collection later in this method.
           setOriginalCollection(((IndirectCollection)currentCollection).getDelegateObject());
       } else {
           setOriginalCollection(currentCollection);
       }
       internalRecreateOriginalCollection(this.originalCollection, session);
       clearChanges();
    }

    /**
     * ADVANCED:
     * If the owning UnitOfWork has shouldChangeRecordKeepOldValue set to true,
     * then return the old value of the attribute represented by this ChangeRecord.
     */
    public Object getOldValue() {
        if(this.originalCollection != null) {
            return this.originalCollection;
        } else {
            if(getOwner() != null) {
                Object obj = ((org.eclipse.persistence.internal.sessions.ObjectChangeSet)getOwner()).getUnitOfWorkClone();
                AbstractSession session = ((org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet)getOwner().getUOWChangeSet()).getSession();
                if(obj != null && session != null) {
                    Object currentCollection = this.mapping.getAttributeValueFromObject(obj);
                    ContainerPolicy cp = this.mapping.getContainerPolicy();
                    Object cloneCurrentCollection = cp.containerInstance(cp.sizeFor(currentCollection));
                    for (Object valuesIterator = cp.iteratorFor(currentCollection); cp.hasNext(valuesIterator);) {
                        Object member = cp.next(valuesIterator, session);
                        cp.addInto(cp.keyFromIterator(valuesIterator), member, cloneCurrentCollection , session);
                    }
                    return getOldValue(cloneCurrentCollection, session);
                }
            }
            return null;
        }
    }

    public Object getOldValue(Object currentCollection, AbstractSession session) {
        if(currentCollection != null) {
            if(currentCollection  instanceof IndirectCollection) {
                currentCollection = ((IndirectCollection)currentCollection).getDelegateObject();
            }
            internalRecreateOriginalCollection(currentCollection, session);
        }
        return currentCollection;
    }
}
