/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.openjpa.util.proxy;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Vector;

import org.apache.openjpa.kernel.Broker;
import org.apache.openjpa.kernel.BrokerImpl;
import org.apache.openjpa.kernel.DetachedValueStateManager;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.StateManagerImpl;
import org.apache.openjpa.util.ChangeTracker;
import org.apache.openjpa.util.CollectionChangeTracker;
import org.apache.openjpa.util.Proxies;

/**
 * Utility methods used by collection proxies.
 *
 * @author Abe White
 */
public class ProxyCollections
    extends Proxies {

    /**
     * Call before invoking {@link List#add(int,Object)} on super.
     */
    public static void beforeAdd(ProxyCollection coll, int index, Object value){
        assertAllowedType(value, coll.getElementType());
        dirty(coll, true);
    }

    /**
     * Call before invoking {@link Vector#insertElementAt(Object,int)} on super.
     */
    public static void beforeInsertElementAt(ProxyCollection coll, Object value,
        int index) {
        beforeAdd(coll, index, value);
    }

    /**
     * Call before invoking {@link Collection#add(Object)} on super.
     */
    public static void beforeAdd(ProxyCollection coll, Object value) {
        assertAllowedType(value, coll.getElementType());
        // Must only dirty the collection outside of a delayed load
        if (!isDirectAccess(coll)) {
            dirty(coll, false);
        }
    }

    /**
     * Call after invoking {@link Collection#add(Object)} on super.
     *
     * @param added whether the object was added
     * @return <code>added</code>, for convenience
     */
    public static boolean afterAdd(ProxyCollection coll, Object value,
        boolean added) {
        if (!isDirectAccess(coll) && added && coll.getChangeTracker() != null) {
            setDirectAccess(coll,true);
            ((CollectionChangeTracker) coll.getChangeTracker()).added(value);
            setDirectAccess(coll,false);
        }
        return added;
    }

    /**
     * Call before invoking {@link Vector#addElement(Object)} on super.
     */
    public static void beforeAddElement(ProxyCollection coll, Object value) {
        beforeAdd(coll, value);
    }

    /**
     * Call after invoking {@link Vector#addElement(Object)} on super.
     */
    public static void afterAddElement(ProxyCollection coll, Object value) {
        afterAdd(coll, value, true);
    }

    /**
     * Call before invoking {@link LinkedList#addFirst(Object)} on super.
     */
    public static void beforeAddFirst(ProxyCollection coll, Object value) {
        beforeAdd(coll, 0, value);
    }

    /**
     * Call before invoking {@link LinkedList#addLast(Object)} on super.
     */
    public static void beforeAddLast(ProxyCollection coll, Object value) {
        beforeAdd(coll, value);
    }

    /**
     * Call after invoking {@link LinkedList#addLast(Object)} on super.
     */
    public static void afterAddLast(ProxyCollection coll, Object value) {
        afterAdd(coll, value, true);
    }

    /**
     * Call before invoking {@link Queue#offer(Object)} on super.
     */
    public static void beforeOffer(ProxyCollection coll, Object value) {
        beforeAdd(coll, value);
    }

    /**
     * Call after invoking {@link Queue#offer(Object)} on super.
     *
     * @param added whether the object was added
     * @return <code>added</code>, for convenience
     */
    public static boolean afterOffer(ProxyCollection coll, Object value,
        boolean added) {
        return afterAdd(coll, value, added);
    }

    /**
     * Override for {@link List#addAll(int, Collection)}.
     */
    public static boolean addAll(ProxyCollection coll, int index,
        Collection values) {
        List list = (List) coll;
        for (Iterator itr = values.iterator(); itr.hasNext(); index++)
            list.add(index, itr.next());
        return values.size() > 0;
    }

    /**
     * Override for {@link Collection#addAll}.
     */
    public static boolean addAll(ProxyCollection coll, Collection values) {
        boolean added = false;
        for (Object value : values) {
            added |= coll.add(value);
        }
        return added;
    }

    /**
     * Call before clearing collection.
     */
    public static void beforeClear(ProxyCollection coll) {
        dirty(coll, true);
        for (Object o : coll) {
            removed(coll, o, false);
        }
    }

    /**
     * Call before clearing vector.
     */
    public static void beforeRemoveAllElements(ProxyCollection coll) {
        beforeClear(coll);
    }

    /**
     * Wrap given iterator in a proxy.
     */
    public static Iterator afterIterator(final ProxyCollection coll,
        final Iterator itr) {
        // check for proxied; some coll impls delegate iterator methods
        if (itr instanceof ProxyIterator)
            return itr;
        return new ProxyIterator() {
            private Object _last = null;

            @Override
            public boolean hasNext() {
                return itr.hasNext();
            }

            @Override
            public Object next() {
                _last = itr.next();
                return _last;
            }

            @Override
            public void remove() {
                dirty(coll, false);
                itr.remove();
                if (coll.getChangeTracker() != null)
                    ((CollectionChangeTracker) coll.getChangeTracker()).
                        removed(_last);
                Proxies.removed(coll, _last, false);
            }
        };
    }

    /**
     * Wrap given iterator in a proxy.
     */
    public static ListIterator afterListIterator(final ProxyCollection coll,
        int idx, final ListIterator itr) {
        return afterListIterator(coll, itr);
    }

    /**
     * Wrap given iterator in a proxy.
     */
    public static ListIterator afterListIterator(final ProxyCollection coll,
        final ListIterator itr) {
        // check for proxied; some coll impls delegate iterator methods
        if (itr instanceof ProxyListIterator)
            return itr;
        return new ProxyListIterator() {
            private Object _last = null;

            @Override
            public boolean hasNext() {
                return itr.hasNext();
            }

            @Override
            public int nextIndex() {
                return itr.nextIndex();
            }

            @Override
            public Object next() {
                _last = itr.next();
                return _last;
            }

            @Override
            public boolean hasPrevious() {
                return itr.hasPrevious();
            }

            @Override
            public int previousIndex() {
                return itr.previousIndex();
            }

            @Override
            public Object previous() {
                _last = itr.previous();
                return _last;
            }

            @Override
            public void set(Object o) {
                assertAllowedType(o, coll.getElementType());
                dirty(coll, false);
                itr.set(o);
                if (coll.getChangeTracker() != null)
                    coll.getChangeTracker().stopTracking();
                Proxies.removed(coll, _last, false);
                _last = o;
            }

            @Override
            public void add(Object o) {
                assertAllowedType(o, coll.getElementType());
                dirty(coll, false);
                itr.add(o);
                if (coll.getChangeTracker() != null) {
                    if (hasNext())
                        coll.getChangeTracker().stopTracking();
                    else
                        ((CollectionChangeTracker) coll.getChangeTracker()).
                            added(o);
                }
                _last = o;
            }

            @Override
            public void remove() {
                dirty(coll, false);
                itr.remove();
                if (coll.getChangeTracker() != null)
                    ((CollectionChangeTracker) coll.getChangeTracker()).
                        removed(_last);
                Proxies.removed(coll, _last, false);
            }
        };
    }

    /**
     * Call before invoking {@link List#remove(int)} on super.
     */
    public static void beforeRemove(ProxyCollection coll, int index) {
        dirty(coll, false);
    }

    /**
     * Call after invoking {@link List#remove(int)} on super.
     *
     * @param removed the removed object
     * @return the removed object, for convenience
     */
    public static Object afterRemove(ProxyCollection coll, int index,
        Object removed) {
        if (coll.getChangeTracker() != null)
            ((CollectionChangeTracker) coll.getChangeTracker()).
                    removed(removed);
        removed(coll, removed, false);
        return removed;
    }

    /**
     * Call before invoking {@link Vector#removeElementAt(int)} on super.
     */
    public static void beforeRemoveElementAt(ProxyCollection coll, int index) {
        beforeRemove(coll, index);
    }

    /**
     * Call before invoking {@link Collection#remove} on super.
     */
    public static void beforeRemove(ProxyCollection coll, Object o) {
        // Must only dirty the collection outside of a delayed load
        if (!isDirectAccess(coll)) {
            dirty(coll, false);
        }
    }

    /**
     * Call after invoking {@link Collection#remove} on super.
     *
     * @param removed whether the object was removed
     * @return whether the object was removed, for convenience
     */
    public static boolean afterRemove(ProxyCollection coll, Object o,
        boolean removed){
        boolean isDelayed = isDelayed(coll);
        boolean direct = isDirectAccess(coll);
        if (!isDelayed) {
            if (!removed)
                return false;
        }
        if (!direct && coll.getChangeTracker() != null) {
            // switch on direct access to prevent the removed op from
            // inadvertently loading the collection
            setDirectAccess(coll, true);
            ((CollectionChangeTracker) coll.getChangeTracker()).removed(o);
            setDirectAccess(coll, false);
        }
        if (!isDelayed) {
            removed(coll, o, false);
        }
        return true;
    }

    private static boolean isDirectAccess(ProxyCollection coll) {
        if (coll instanceof DelayedProxy) {
            DelayedProxy dpxy = (DelayedProxy)coll;
            return dpxy.isDirectAccess();
        }
        return false;
    }

    private static void setDirectAccess(ProxyCollection coll, boolean direct) {
        if (coll instanceof DelayedProxy) {
            DelayedProxy dpxy = (DelayedProxy)coll;
            dpxy.setDirectAccess(direct);
        }
    }

    /**
     * Call before invoking {@link Vector#removeElement} on super.
     */
    public static void beforeRemoveElement(ProxyCollection coll, Object o) {
        beforeRemove(coll, o);
    }

    /**
     * Call after invoking {@link Vector#removeElement} on super.
     */
    public static boolean afterRemoveElement(ProxyCollection coll, Object o,
        boolean removed) {
        return afterRemove(coll, o, removed);
    }

    /**
     * Call before invoking {@link LinkedList#removeFirst} on super.
     */
    public static void beforeRemoveFirst(ProxyCollection coll) {
        beforeRemove(coll, 0);
    }

    /**
     * Call after invoking {@link LinkedList#removeFirst} on super.
     */
    public static Object afterRemoveFirst(ProxyCollection coll, Object removed){
        return afterRemove(coll, 0, removed);
    }

    /**
     * Call after invoking {@link LinkedList#removeLast} on super.
     */
    public static void beforeRemoveLast(ProxyCollection coll) {
        beforeRemove(coll, coll.size() - 1);
    }

    /**
     * Call after invoking {@link LinkedList#removeLast} on super.
     */
    public static Object afterRemoveLast(ProxyCollection coll, Object removed) {
        return afterRemove(coll, coll.size(), removed);
    }

    /**
     * Call before invoking {@link Queue#remove} on super.
     */
    public static void beforeRemove(ProxyCollection coll) {
        beforeRemove(coll, 0);
    }

    /**
     * Call after invoking {@link Queue#remove} on super.
     */
    public static Object afterRemove(ProxyCollection coll, Object removed){
        return afterRemove(coll, 0, removed);
    }

    /**
     * Call before invoking {@link Queue#poll} on super.
     */
    public static void beforePoll(ProxyCollection coll) {
        if (!coll.isEmpty())
            beforeRemove(coll, 0);
    }

    /**
     * Call after invoking {@link Queue#poll} on super.
     */
    public static Object afterPoll(ProxyCollection coll, Object removed) {
        if (removed != null)
            afterRemove(coll, 0, removed);
        return removed;
    }

    /**
     * Override for {@link Collection#removeAll}.
     */
    public static boolean removeAll(ProxyCollection coll, Collection<?> vals) {
        boolean removed = false;
        for (Object val : vals) {
            removed |= coll.remove(val);
        }
        return removed;
    }

    /**
     * Override for {@link Collection#retainAll}.
     */
    public static boolean retainAll(ProxyCollection coll, Collection<?> vals) {
        int size = coll.size();
        coll.removeIf(o -> !vals.contains(o));
        return coll.size() < size;
    }

    /**
     * Call before invoking {@link List#set} on super.
     */
    public static void beforeSet(ProxyCollection coll, int index,
        Object element) {
        assertAllowedType(element, coll.getElementType());
        dirty(coll, true);
    }

    /**
     * Call after invoking {@link List#set} on super.
     *
     * @param replaced the replaced object
     * @return the replaced object, for convenience
     */
    public static Object afterSet(ProxyCollection coll, int index,
        Object element, Object replaced) {
        if (replaced != element)
            removed(coll, replaced, false);
        return replaced;
    }

    /**
     * Call before invoking {@link Vector#setElementAt} on super.
     */
    public static void beforeSetElementAt(ProxyCollection coll, Object element,
        int index) {
        beforeSet(coll, index, element);
    }

    /**
     * Call after invoking {@link Vector#setElementAt} on super.
     */
    public static Object afterSetElementAt(ProxyCollection coll, Object element,
        int index, Object replaced) {
        return afterSet(coll, index, element, replaced);
    }

    /**
     * Marker interface for a proxied iterator.
     */
    public interface ProxyIterator
        extends Iterator {
    }

    /**
     * Marker interface for a proxied list iterator.
     */
    public interface ProxyListIterator
        extends ProxyIterator, ListIterator {
    }

    public static void loadCollection(ProxyCollection proxy) {
        loadCollection(proxy, false);
    }

    public static void loadCollection(ProxyCollection proxy, boolean detaching) {
        if (!isDelayed(proxy)) {
            return;
        }
        DelayedProxy dProxy = (DelayedProxy)proxy;
        if (dProxy.isDirectAccess()) {
            return;
        }
        boolean state[] = new boolean[2];
        try {
            dProxy.setDirectAccess(true);
            state = checkState(proxy);
            boolean tracking = false;
            ChangeTracker ct = proxy.getChangeTracker();
            Collection<?> added = null;
            Collection<?> removed = null;
            if (ct != null && ct.isTracking() ) {
                if (!ct.getAdded().isEmpty()) {
                    added = new ArrayList(ct.getAdded());
                }
                if (!ct.getRemoved().isEmpty()) {
                    removed = new ArrayList(ct.getRemoved());
                }
                tracking = true;
                ct.stopTracking();
            }
            if (proxy.size() > 0) {
                proxy.clear();
            }
            dProxy.getDelayedOwner().loadDelayedField(dProxy.getDelayedField());
            if (!detaching && tracking && !ct.isTracking()) {
                ct.startTracking();
            }
            // add new elements
            if (added != null && added.size() > 0) {
                dProxy.setDirectAccess(false);
                proxy.addAll(added);
                added.clear();
            }
            // purge removed elements
            if (removed != null && removed.size() > 0) {
                dProxy.setDirectAccess(false);
                proxy.removeAll(removed);
                removed.clear();
            }
        } finally {
            dProxy.setDirectAccess(false);
            if (state[0]) {
                dProxy.closeBroker();
            }
            if (state[1]) {
                clearStateManager(proxy);
            }
        }
    }

    public static boolean isDelayed(ProxyCollection proxy) {
        if (proxy instanceof DelayedProxy) {
            DelayedProxy dProxy = (DelayedProxy)proxy;
            OpenJPAStateManager sm = dProxy.getDelayedOwner();
            return (sm != null &&
                    sm.isDelayed(dProxy.getDelayedField()));
        }
        return false;
    }

    private static boolean[] checkState(ProxyCollection proxy) {
        boolean[] state = new boolean[2];
        DelayedProxy dProxy = (DelayedProxy)proxy;

        OpenJPAStateManager sm = dProxy.getDelayedOwner();
        if (sm != null) {
            // If the broker assigned to this proxy is null, closed or no longer
            // manages the pc, produce a new one
            Broker broker = sm.getContext().getBroker();
            if (dProxy.isDetached() || broker == null || broker.isClosed()
                || (!broker.isClosed() && !broker.isPersistent(sm.getPersistenceCapable()))) {
                state[0] = true;
                broker = dProxy.getBroker();
                ((StateManagerImpl)sm).setBroker((BrokerImpl)broker);
            }
            if (dProxy.isDetached() || sm.getPersistenceCapable().pcGetStateManager() == null) {
                state[1] = true;
                if (dProxy.getOwnerStateManager() != null) {
                    sm.getPersistenceCapable().pcReplaceStateManager(dProxy.getOwnerStateManager());
                    ((StateManagerImpl)dProxy.getOwnerStateManager()).setBroker((BrokerImpl)broker);
                } else {
                    sm.getPersistenceCapable().pcReplaceStateManager(
                            new DetachedValueStateManager(sm.getPersistenceCapable(), sm.getContext()));
                }
            }
        }
        return state;
    }

    private static void clearStateManager(ProxyCollection proxy) {
        OpenJPAStateManager sm = proxy.getOwner();
        if (sm != null) {
            sm.getPersistenceCapable().pcReplaceStateManager(null);
        }
    }
}
