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

import java.util.*;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.indirection.*;
import org.eclipse.persistence.internal.queries.AttributeItem;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.mappings.*;
import org.eclipse.persistence.queries.AttributeGroup;
import org.eclipse.persistence.queries.FetchGroup;

/**
 * This class provides a generic way of using the descriptor information
 * to traverse an object graph.
 * Define a subclass, or an inner class, that implements at least
 * #iterate(Object) to implement a new traversal
 * feature without having to change the mapping classes or the object builder.
 * It provides functionality such as a cascading depth, a stack of visited object,
 * and a collection of the visited objects.
 *
 * NOTE:
 * If this works nicely the merge manager, remote traversals, and maybe
 * even aspects of the commit manager could be converted to use this class.
 */
public abstract class DescriptorIterator {
    public static final int NoCascading = 1;
    public static final int CascadePrivateParts = 2;
    public static final int CascadeAllParts = 3;
    protected Map visitedObjects;
    protected Stack visitedStack;
    protected AbstractSession session;
    protected DatabaseMapping currentMapping;
    protected ClassDescriptor currentDescriptor;
    protected AttributeItem currentItem;
    protected AttributeGroup currentGroup;
    protected boolean usesGroup;
    /* Ignored if usesGroup is false.
     * If set to true allows visiting the same object several times -
     * as long as it hasn't been visited with the currentGroup.
     */
    protected boolean shouldTrackCurrentGroup;
    protected Object result;// this is a work area, typically used as a Collecting Parm
    protected boolean shouldIterateOverIndirectionObjects;
    protected boolean shouldIterateOverUninstantiatedIndirectionObjects;
    protected boolean shouldIterateOverWrappedObjects;
    protected boolean shouldIterateOnIndirectionObjects;
    protected boolean shouldIterateOnAggregates;
    protected boolean shouldIterateOnPrimitives;
    // false by default; true means if object has FetchGroup then don't iterate outside it.
    protected boolean shouldIterateOnFetchGroupAttributesOnly;
    protected boolean shouldBreak;
    protected int cascadeDepth;// see static constants below
    protected CascadeCondition cascadeCondition;

    /**
     * Construct a typical iterator:
     *    iterate over all the objects
     *    process the objects contained by "value holders"...
     *    ...but only if they have already been instantiated...
     *    ...and don't process the "value holders" themselves
     *    process "wrapped" objects
     *    skip aggregate objects
     *    skip primitives (Strings, Dates, Integers, etc.)
     */
    public DescriptorIterator() {
        // 2612538 - the default size of Map (32) is appropriate
        this.visitedObjects = new IdentityHashMap();
        this.visitedStack = new Stack();
        this.cascadeDepth = CascadeAllParts;
        this.shouldIterateOverIndirectionObjects = true;// process the objects contained by ValueHolders...
        this.shouldIterateOverUninstantiatedIndirectionObjects = false;// ...but only if they have already been instantiated...
        this.shouldIterateOnIndirectionObjects = false;// ...and don't process the ValueHolders themselves
        this.shouldIterateOverWrappedObjects = true;// process "wrapped" objects
        this.shouldIterateOnAggregates = false;
        this.shouldIterateOnPrimitives = false;
        this.shouldIterateOnFetchGroupAttributesOnly = false;
        this.shouldBreak = false;
        this.cascadeCondition = new CascadeCondition();
    }

    public int getCascadeDepth() {
        return cascadeDepth;
    }

    public ClassDescriptor getCurrentDescriptor() {
        return currentDescriptor;
    }

    public DatabaseMapping getCurrentMapping() {
        return currentMapping;
    }

    public AttributeItem getCurrentItem() {
        return this.currentItem;
    }

    public AttributeGroup getCurrentGroup() {
        return this.currentGroup;
    }

    /**
     * Fetch and return the descriptor for the specified object.
     */
    protected ClassDescriptor getDescriptorFor(Object object) {
        ClassDescriptor result = getSession().getDescriptor(object);
        if (result == null) {
            throw DescriptorException.missingDescriptor(object.getClass().getName());
        }
        return result;
    }

    public Object getResult() {
        return result;
    }

    public AbstractSession getSession() {
        return session;
    }

    /**
     * Return the second-to-last object visited.
     */
    public Object getVisitedGrandparent() {
        Object parent = getVisitedStack().pop();
        Object result = getVisitedStack().peek();
        getVisitedStack().push(parent);
        return result;
    }

    public Map getVisitedObjects() {
        return visitedObjects;
    }

    /**
     * Return the last object visited.
     */
    public Object getVisitedParent() {
        return getVisitedStack().peek();
    }

    public Stack getVisitedStack() {
        return visitedStack;
    }

    /**
     * Iterate an aggregate object
     * (i.e. an object that is the target of an AggregateMapping).
     * Override this method if appropriate.
     */
    protected void internalIterateAggregateObject(Object aggregateObject) {
        iterate(aggregateObject);
    }

    /**
     * Iterate an indirect container (IndirectList or IndirectMap).
     * Override this method if appropriate.
     */
    protected void internalIterateIndirectContainer(IndirectContainer container) {
        iterate(container);
    }

    /**
     * Iterate a primitive object (String, Date, Integer, etc.).
     * Override this method if appropriate.
     */
    protected void internalIteratePrimitive(Object primitiveValue) {
        iterate(primitiveValue);
    }

    /**
     * Iterate a (a non-Aggregate) reference object.
     * Override this method if appropriate.
     */
    protected void internalIterateReferenceObject(Object referenceObject) {
        iterate(referenceObject);
    }

    /**
     * Iterate a value holder.
     * Override this method if appropriate.
     */
    protected void internalIterateValueHolder(ValueHolderInterface valueHolder) {
        iterate(valueHolder);
    }

    /**
     * To define a new iterator create a subclass and define at least this method.
     * Given an object or set of the objects, this method will be called on those
     * objects and any object connected to them by using the descriptors to
     * traverse the object graph.
     * Override the assorted #internalIterate*() methods if appropriate.
     */
    protected abstract void iterate(Object object);

    /**
     * Iterate on the mapping's reference object and
     * recursively iterate on the reference object's
     * reference objects.
     * This is used for aggregate and aggregate collection mappings, which are not iterated on by default.
     */
    public void iterateForAggregateMapping(Object aggregateObject, DatabaseMapping mapping, ClassDescriptor descriptor) {
        if (aggregateObject == null) {
            return;
        }
        setCurrentMapping(mapping);
        // aggregate descriptors are passed in because they could be part of an inheritance tree
        setCurrentDescriptor(descriptor);

        AttributeGroup currentGroupOriginal = null;
        AttributeItem currentItemOriginal = null;
        if(this.usesGroup) {
            currentGroupOriginal = this.currentGroup;
            currentItemOriginal = this.currentItem;
            this.currentGroup = this.currentItem.getGroup();
        }

        if (shouldIterateOnAggregates()) {// false by default
            internalIterateAggregateObject(aggregateObject);
            if (shouldBreak()) {
                setShouldBreak(false);
                if(this.usesGroup) {
                    this.currentGroup = currentGroupOriginal;
                    this.currentItem = currentItemOriginal;
                }
                return;
            }
        }

        iterateReferenceObjects(aggregateObject);
        if(this.usesGroup) {
            this.currentGroup = currentGroupOriginal;
            this.currentItem = currentItemOriginal;
        }
    }

    /**
     * Iterate on the indirection object for its mapping.
     */
    public void iterateIndirectContainerForMapping(IndirectContainer container, DatabaseMapping mapping) {
        setCurrentMapping(mapping);
        setCurrentDescriptor(null);

        if (shouldIterateOnIndirectionObjects()) {// false by default
            internalIterateIndirectContainer(container);
        }

        if (shouldIterateOverUninstantiatedIndirectionObjects() || (shouldIterateOverIndirectionObjects() && container.isInstantiated())) {
            // force instantiation only if specified
            mapping.iterateOnRealAttributeValue(this, container);
        } else if (shouldIterateOverIndirectionObjects()) {
            // PERF: Allow the indirect container to iterate any cached elements.
            if (container instanceof IndirectCollection)  {
                mapping.iterateOnRealAttributeValue(this, ((IndirectCollection)container).getAddedElements());
            }
        }
    }

    /**
     * Iterate on the primitive value for its mapping.
     */
    public void iteratePrimitiveForMapping(Object primitiveValue, DatabaseMapping mapping) {
        if (primitiveValue == null) {
            return;
        }
        setCurrentMapping(mapping);
        setCurrentDescriptor(null);

        if (shouldIterateOnPrimitives()) {// false by default
            AttributeGroup currentGroupOriginal = null;
            AttributeItem currentItemOriginal = null;
            if(this.usesGroup) {
                currentGroupOriginal = this.currentGroup;
                currentItemOriginal = this.currentItem;
                this.currentGroup = this.currentItem.getGroup();
            }

            internalIteratePrimitive(primitiveValue);

            if(this.usesGroup) {
                this.currentGroup = currentGroupOriginal;
                this.currentItem = currentItemOriginal;
            }
        }
    }

    /**
     * Iterate on the mapping's reference object and
     * recursively iterate on the reference object's
     * reference objects.
     */
    public void iterateReferenceObjectForMapping(Object referenceObject, DatabaseMapping mapping) {
        if (this.cascadeCondition.shouldNotCascade(mapping)) {
            return;
        }

        // When using wrapper policy in EJB the iteration can stop in certain cases,
        // this is because EJB forces beans to be registered anyway and clone identity can be violated
        // and the violated clones references to session objects should not be traversed.
        ClassDescriptor rd = mapping.getReferenceDescriptor();
        if ((!shouldIterateOverWrappedObjects()) && (rd != null) && (rd.hasWrapperPolicy())) {
            return;
        }
        if (referenceObject == null) {
            return;
        }

        if(this.usesGroup && this.shouldTrackCurrentGroup) {
            Set visited = (Set)getVisitedObjects().get(referenceObject);
            if(visited == null) {
                visited = new HashSet(1);
                visited.add(this.currentItem.getGroup());
                getVisitedObjects().put(referenceObject, visited);
            } else {
                if(visited.contains(this.currentItem.getGroup())) {
                    // source object has been already visited with an equal group
                    return;
                } else {
                    visited.add(this.currentItem.getGroup());
                }
            }
        } else {
            // Check if already processed.
            if (getVisitedObjects().containsKey(referenceObject)) {
                return;
            }

            getVisitedObjects().put(referenceObject, referenceObject);
        }
        setCurrentMapping(mapping);
        setCurrentDescriptor(getDescriptorFor(referenceObject));

        AttributeGroup currentGroupOriginal = null;
        AttributeItem currentItemOriginal = null;
        if(this.usesGroup) {
            currentGroupOriginal = this.currentGroup;
            currentItemOriginal = this.currentItem;
            this.currentGroup = this.currentItem.getGroup();
        }

        internalIterateReferenceObject(referenceObject);
        if (shouldBreak()) {
            setShouldBreak(false);
            if(this.usesGroup) {
                this.currentGroup = currentGroupOriginal;
                this.currentItem = currentItemOriginal;
            }
            return;
        }

        iterateReferenceObjects(referenceObject);
        if(this.usesGroup) {
            this.currentGroup = currentGroupOriginal;
            this.currentItem = currentItemOriginal;
        }
    }

    /**
     * Iterate over the sourceObject's reference objects,
     * updating the visited stack appropriately.
     */
    protected void iterateReferenceObjects(Object sourceObject) {
        if(this.usesGroup) {
            // object is outside of the group - don't iterate over its references
            if(this.currentGroup == null || !this.currentGroup.hasItems()) {
                return;
            }
        }

        getVisitedStack().push(sourceObject);
        internalIterateReferenceObjects(sourceObject);
        getVisitedStack().pop();
    }

    protected void internalIterateReferenceObjects(Object sourceObject) {
        List<DatabaseMapping> mappings;
        // Only iterate on relationships if required.
        if (shouldIterateOnPrimitives()) {
            mappings = getCurrentDescriptor().getObjectBuilder().getDescriptor().getMappings();
        } else {
            ObjectBuilder builder = getCurrentDescriptor().getObjectBuilder().getDescriptor().getObjectBuilder();
            // PERF: Only process relationships.
            if (builder.isSimple()) {
                return;
            }
            mappings = builder.getRelationshipMappings();
        }

        if (shouldIterateOnFetchGroupAttributesOnly()) {
            if(getCurrentDescriptor().hasFetchGroupManager()) {
                FetchGroup fetchGroup = getCurrentDescriptor().getFetchGroupManager().getObjectFetchGroup(sourceObject);
                if (fetchGroup != null) {
                    List<DatabaseMapping> fetchGroupMappings = new ArrayList();
                    for (DatabaseMapping mapping : mappings) {
                        if (fetchGroup.containsAttributeInternal(mapping.getAttributeName())) {
                            fetchGroupMappings.add(mapping);
                        }
                    }
                    mappings = fetchGroupMappings;
                }
            }
        }

        if (this.usesGroup) {
            AttributeGroup currentGroupOriginal = this.currentGroup;
            AttributeItem currentItemOriginal = this.currentItem;
            for (DatabaseMapping mapping : mappings) {
                this.currentItem = this.currentGroup.getAllItems().get(mapping.getAttributeName());
                // iterate only over the mappings found in the group
                if (currentItem != null) {
                    mapping.iterate(this);
                    this.currentGroup = currentGroupOriginal;
                }
            }
            this.currentItem = currentItemOriginal;
        } else {
            for (DatabaseMapping mapping : mappings) {
                mapping.iterate(this);
            }
        }
    }

    /**
     * Iterate on the value holder for its mapping.
     */
    public void iterateValueHolderForMapping(ValueHolderInterface valueHolder, DatabaseMapping mapping) {
        setCurrentMapping(mapping);
        setCurrentDescriptor(null);

        if (shouldIterateOnIndirectionObjects()) {// false by default
            internalIterateValueHolder(valueHolder);
        }

        if (shouldIterateOverUninstantiatedIndirectionObjects() || (shouldIterateOverIndirectionObjects() && valueHolder.isInstantiated())) {
            // force instantiation only if specified
            mapping.iterateOnRealAttributeValue(this, valueHolder.getValue());
        }
    }

    public void setCascadeDepth(int cascadeDepth) {
        this.cascadeDepth = cascadeDepth;
    }

    public void setCascadeCondition(CascadeCondition cascadeCondition){
        this.cascadeCondition = cascadeCondition;
    }

    public void setCurrentDescriptor(ClassDescriptor currentDescriptor) {
        this.currentDescriptor = currentDescriptor;
    }

    public void setCurrentMapping(DatabaseMapping currentMapping) {
        this.currentMapping = currentMapping;
    }

    public void setCurrentItem(AttributeItem item) {
        this.currentItem = item;
    }

    public void setCurrentGroup(AttributeGroup group) {
        this.currentGroup = group;
    }

    public void setResult(Object result) {
        this.result = result;
    }

    public void setSession(AbstractSession session) {
        this.session = session;
    }

    public void setShouldBreak(boolean shouldBreak) {
        this.shouldBreak = shouldBreak;
    }

    /**
     * Set whether the aggregate reference objects themselves
     * should be processed. (The objects referenced by the aggregate
     * objects will be processed either way.)
     */
    public void setShouldIterateOnAggregates(boolean shouldIterateOnAggregates) {
        this.shouldIterateOnAggregates = shouldIterateOnAggregates;
    }

    /**
     * Set whether the attributes outside fetch group should be processed.
     */
    public void setShouldIterateOnFetchGroupAttributesOnly(boolean shouldIterateOnFetchGroupAttributesOnly) {
        this.shouldIterateOnFetchGroupAttributesOnly = shouldIterateOnFetchGroupAttributesOnly;
    }

    /**
     * Set whether the indirection objects themselves (e.g. the ValueHolders)
     * should be processed.
     */
    public void setShouldIterateOnIndirectionObjects(boolean shouldIterateOnIndirectionObjects) {
        this.shouldIterateOnIndirectionObjects = shouldIterateOnIndirectionObjects;
    }

    /**
     * Set whether to process primitive reference objects
     * (e.g. Strings, Dates, ints).
     */
    public void setShouldIterateOnPrimitives(boolean shouldIterateOnPrimitives) {
        this.shouldIterateOnPrimitives = shouldIterateOnPrimitives;
    }

    /**
     * Set whether to process the objects contained by indirection objects
     * (e.g. a ValueHolder's value) - but *without* instantiating them.
     * @see #setShouldIterateOverUninstantiatedIndirectionObjects()
     */
    public void setShouldIterateOverIndirectionObjects(boolean shouldIterateOverIndirectionObjects) {
        this.shouldIterateOverIndirectionObjects = shouldIterateOverIndirectionObjects;
    }

    /**
     * Set whether to *instantiate* and process the objects
     * contained by indirection objects (e.g. a ValueHolder's value).
     */
    public void setShouldIterateOverUninstantiatedIndirectionObjects(boolean shouldIterateOverUninstantiatedIndirectionObjects) {
        this.shouldIterateOverUninstantiatedIndirectionObjects = shouldIterateOverUninstantiatedIndirectionObjects;
    }

    public void setShouldIterateOverWrappedObjects(boolean shouldIterateOverWrappedObjects) {
        this.shouldIterateOverWrappedObjects = shouldIterateOverWrappedObjects;
    }

    public void setShouldTrackCurrentGroup(boolean shouldTrackCurrentGroup) {
        this.shouldTrackCurrentGroup = shouldTrackCurrentGroup;
    }

    public void setVisitedObjects(Map visitedObjects) {
        this.visitedObjects = visitedObjects;
    }

    protected void setVisitedStack(Stack visitedStack) {
        this.visitedStack = visitedStack;
    }

    public boolean shouldBreak() {
        return shouldBreak;
    }

    public boolean shouldCascadeAllParts() {
        return getCascadeDepth() == CascadeAllParts;
    }

    public boolean shouldCascadeNoParts() {
        return (getCascadeDepth() == NoCascading);
    }

    public boolean shouldCascadePrivateParts() {
        return (getCascadeDepth() == CascadeAllParts) || (getCascadeDepth() == CascadePrivateParts);
    }

    /**
     * Return whether the aggregate reference objects themselves
     * should be processed. (The objects referenced by the aggregate
     * objects will be processed either way.)
     */
    public boolean shouldIterateOnAggregates() {
        return shouldIterateOnAggregates;
    }

    /**
     * If true then if object has a FetchGroup then iterations
     * not performed on mappings that are outside of the FetchGroup.
     */
    public boolean shouldIterateOnFetchGroupAttributesOnly() {
        return this.shouldIterateOnFetchGroupAttributesOnly;
    }

    /**
     * Return whether the indirection objects themselves (e.g. the ValueHolders)
     * should be processed.
     */
    public boolean shouldIterateOnIndirectionObjects() {
        return shouldIterateOnIndirectionObjects;
    }

    /**
     * Return whether to process primitive reference objects
     * (e.g. Strings, Dates, ints).
     */
    public boolean shouldIterateOnPrimitives() {
        return shouldIterateOnPrimitives;
    }

    /**
     * Return whether to process the objects contained by indirection objects
     * (e.g. a ValueHolder's value) - but *without* instantiating them.
     * @see #shouldIterateOverUninstantiatedIndirectionObjects()
     */
    public boolean shouldIterateOverIndirectionObjects() {
        return shouldIterateOverIndirectionObjects;
    }

    /**
     * Return whether to *instantiate* and process the objects
     * contained by indirection objects (e.g. a ValueHolder's value).
     */
    public boolean shouldIterateOverUninstantiatedIndirectionObjects() {
        return shouldIterateOverUninstantiatedIndirectionObjects;
    }

    public boolean shouldIterateOverWrappedObjects() {
        return shouldIterateOverWrappedObjects;
    }

    public boolean shouldTrackCurrentGroup() {
        return this.shouldTrackCurrentGroup;
    }

    public boolean usesGroup() {
        return this.usesGroup;
    }

    /**
     * This is the root method called to start the iteration.
     */
    public void startIterationOn(Object sourceObject) {
        startIterationOn(sourceObject, null);
    }

    public void startIterationOn(Object sourceObject, AttributeGroup group) {
        this.usesGroup = group != null;
        if(this.usesGroup && this.shouldTrackCurrentGroup) {
            Set visited = (Set)getVisitedObjects().get(sourceObject);
            if(visited == null) {
                visited = new HashSet(1);
                visited.add(group);
                getVisitedObjects().put(sourceObject, visited);
            } else {
                if(visited.contains(group)) {
                    // source object has been already visited with an equal group
                    return;
                } else {
                    visited.add(group);
                }
            }
        } else {
            if (getVisitedObjects().containsKey(sourceObject)) {
                return;
            }
            getVisitedObjects().put(sourceObject, sourceObject);
        }
        setCurrentMapping(null);
        setCurrentDescriptor(getSession().getDescriptor(sourceObject));
        setCurrentItem(null);
        setCurrentGroup(group);

        iterate(sourceObject);

        // start the recursion
        if ((getCurrentDescriptor() != null) && (!shouldCascadeNoParts())  && !this.shouldBreak()) {
            iterateReferenceObjects(sourceObject);
        }
    }

    public class CascadeCondition{
        public boolean shouldNotCascade(DatabaseMapping mapping){
            return !(shouldCascadeAllParts() || (shouldCascadePrivateParts() && mapping.isPrivateOwned()));
        }
    }
}
