/*
 * 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.io.ObjectStreamException;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.function.IntFunction;

import org.apache.openjpa.kernel.AutoDetach;
import org.apache.openjpa.kernel.Broker;
import org.apache.openjpa.kernel.BrokerFactory;
import org.apache.openjpa.kernel.DetachedStateManager;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.util.ChangeTracker;
import org.apache.openjpa.util.CollectionChangeTracker;
import org.apache.openjpa.util.DelayedCollectionChangeTrackerImpl;
import org.apache.openjpa.util.Proxies;
import org.apache.openjpa.util.Proxy;

/**
 * LinkedList proxy with delay loading capability.  Allows non-indexed
 * add and remove operations to occur on an unloaded collection.  Operations
 * that require a load will trigger a load.
 */
@SuppressWarnings({"rawtypes","unchecked"})
public class DelayedLinkedListProxy extends LinkedList implements ProxyCollection, DelayedProxy {

    private transient OpenJPAStateManager sm;
    private transient int field;
    private transient CollectionChangeTracker changeTracker;
    private transient Class elementType;

    private transient OpenJPAStateManager _ownerSm;
    private transient boolean _directAccess = false;
    private transient BrokerFactory _brokerFactory = null;
    private transient Broker _broker = null;
    private transient OpenJPAStateManager _delayedSm;
    private transient int _delayedField;
    private transient boolean _detached = false;

    public DelayedLinkedListProxy(Collection paramCollection) {
        super(paramCollection);
    }

    public DelayedLinkedListProxy() {
    }

    @Override
    public void setOwner(OpenJPAStateManager paramOpenJPAStateManager,
            int paramInt) {
        // If clearing the owner of this proxy, store away what is necessary for
        // delayed loading
        if (sm != null && detaching(paramOpenJPAStateManager, paramInt)) {
            _detached = true;
            _delayedSm = sm;
            _delayedField = field;
        } else {
            _detached = false;
        }

        this.sm = paramOpenJPAStateManager;
        if (sm != null && sm.getPersistenceCapable() != null) {
            _ownerSm = (OpenJPAStateManager) sm.getPersistenceCapable()
                    .pcGetStateManager();
        }
        this.field = paramInt;
        if (sm != null && sm.getContext() != null) {
            _brokerFactory = sm.getContext().getBroker().getBrokerFactory();
        }
    }

    private boolean detaching(OpenJPAStateManager paramOpenJPAStateManager,
            int paramInt) {
        if ((paramOpenJPAStateManager == null && paramInt == -1)
                || (paramOpenJPAStateManager != null && paramOpenJPAStateManager instanceof DetachedStateManager)) {
            return true;
        }
        return false;
    }

    @Override
    public OpenJPAStateManager getOwner() {
        return this.sm;
    }

    @Override
    public int getOwnerField() {
        return this.field;
    }

    @Override
    public ChangeTracker getChangeTracker() {
        return this.changeTracker;
    }

    protected void setChangeTracker(CollectionChangeTracker ct) {
        changeTracker = ct;
    }

    @Override
    public Object copy(Object paramObject) {
        return new LinkedList((Collection) paramObject);
    }

    @Override
    public Class getElementType() {
        return this.elementType;
    }

    protected void setElementType(Class<?> elemType) {
        elementType = elemType;
    }

    @Override
    public ProxyCollection newInstance(Class paramClass,
            Comparator paramComparator, boolean paramBoolean1,
            boolean paramBoolean2) {
        DelayedLinkedListProxy localproxy = new DelayedLinkedListProxy();
        localproxy.elementType = paramClass;
        if (paramBoolean1)
            localproxy.changeTracker = new DelayedCollectionChangeTrackerImpl(
                    localproxy, true, true, paramBoolean2);
        return localproxy;
    }

    // //////////////////////////////////////
    // DelayedProxy methods
    // //////////////////////////////////////
    @Override
    public int getDelayedField() {
        if (field == -1 || _detached) {
            return _delayedField;
        }
        return field;
    }

    @Override
    public OpenJPAStateManager getDelayedOwner() {
        if (sm == null || _detached) {
            return _delayedSm;
        }
        return sm;
    }

    @Override
    public boolean isDirectAccess() {
        return _directAccess;
    }

    @Override
    public void setDirectAccess(boolean direct) {
        _directAccess = direct;
    }

    public BrokerFactory getBrokerFactory() {
        return _brokerFactory;
    }

    @Override
    public void load() {
        ProxyCollections.loadCollection(this);
    }

    @Override
    public Broker getBroker() {
        if (_broker == null || _broker.isClosed()) {
            if (_brokerFactory != null) {
                _broker = _brokerFactory.newBroker();
            }
        }
        return _broker;
    }

    @Override
    public void closeBroker() {
        if (_broker != null && !_broker.isClosed()) {
            _broker.setAutoDetach(AutoDetach.DETACH_CLOSE);
            _broker.close();
            _broker = null;
        }
    }

    @Override
    public OpenJPAStateManager getOwnerStateManager() {
        return _ownerSm;
    }

    @Override
    public boolean isDetached() {
        return _detached;
    }

    public boolean isDelayLoad() {
        return ProxyCollections.isDelayed(this);
    }

    // //////////////////////////////////////
    // Implementation method wrappers
    // //////////////////////////////////////

    @Override
    public Object clone() {
        if (_directAccess) {
            return super.clone();
        }
        if (isDelayLoad()) {
            load();
        }
        Proxy localProxy = (Proxy) super.clone();
        localProxy.setOwner(null, 0);
        return localProxy;
    }

    @Override
    public void add(int paramInt, Object paramObject) {
        if (!_directAccess) {
            if (isDelayLoad()) {
                load();
            }
        }
        ProxyCollections.beforeAdd(this, paramInt, paramObject);
        super.add(paramInt, paramObject);
    }

    @Override
    public boolean add(Object paramObject) {
        if (_directAccess) {
            return super.add(paramObject);
        }
        ProxyCollections.beforeAdd(this, paramObject);
        boolean bool = super.add(paramObject);
        return ProxyCollections.afterAdd(this, paramObject, bool);
    }

    @Override
    public void clear() {
        if (!_directAccess) {
            if (isDelayLoad()) {
                load();
            }
            ProxyCollections.beforeClear(this);
        }
        super.clear();
    }

    @Override
    public boolean addAll(Collection paramCollection) {
        if (_directAccess) {
            return super.addAll(paramCollection);
        }
        return ProxyCollections.addAll(this, paramCollection);
    }

    @Override
    public boolean addAll(int paramInt, Collection paramCollection) {
        if (_directAccess) {
            return super.addAll(paramInt, paramCollection);
        }
        if (isDelayLoad()) {
            load();
        }
        return ProxyCollections.addAll(this, paramInt, paramCollection);
    }

    @Override
    public boolean remove(Object paramObject) {
        if (_directAccess) {
            return super.remove(paramObject);
        }
        ProxyCollections.beforeRemove(this, paramObject);
        setDirectAccess(true);
        boolean bool = super.remove(paramObject);
        setDirectAccess(false);
        return ProxyCollections.afterRemove(this, paramObject, bool);
    }

    @Override
    public Object remove(int paramInt) {
        if (_directAccess) {
            return super.remove(paramInt);
        }
        if (isDelayLoad()) {
            load();
        }
        ProxyCollections.beforeRemove(this, paramInt);
        Object localObject = super.remove(paramInt);
        return ProxyCollections.afterRemove(this, paramInt, localObject);
    }

    @Override
    public Object remove() {
        if (_directAccess) {
            return super.remove();
        }
        // queue operations require proper ordering. the collection
        // must be loaded in order to ensure order.
        if (isDelayLoad()) {
            load();
        }
        ProxyCollections.beforeRemove(this);
        Object localObject = super.remove();
        return ProxyCollections.afterRemove(this, localObject);
    }

    @Override
    public Object set(int paramInt, Object paramObject) {
        if (_directAccess) {
            return super.set(paramInt, paramObject);
        }
        if (isDelayLoad()) {
            load();
        }
        ProxyCollections.beforeSet(this, paramInt, paramObject);
        Object localObject = super.set(paramInt, paramObject);
        return ProxyCollections.afterSet(this, paramInt, paramObject,
                localObject);
    }

    @Override
    public Object poll() {
        if (_directAccess) {
            return super.poll();
        }
        // queue operations require proper ordering. the collection
        // must be loaded in order to ensure order.
        if (isDelayLoad()) {
            load();
        }
        ProxyCollections.beforePoll(this);
        Object localObject = super.poll();
        return ProxyCollections.afterPoll(this, localObject);
    }

    @Override
    public ListIterator listIterator(int paramInt) {
        if (_directAccess) {
            return super.listIterator(paramInt);
        }
        if (isDelayLoad()) {
            load();
        }
        ListIterator localListIterator = super.listIterator(paramInt);
        return ProxyCollections.afterListIterator(this, paramInt,
                localListIterator);
    }

    @Override
    public void addFirst(Object paramObject) {
        if (_directAccess) {
            super.addFirst(paramObject);
            return;
        }
        if (isDelayLoad()) {
            load();
        }
        ProxyCollections.beforeAddFirst(this, paramObject);
        super.addFirst(paramObject);
    }

    @Override
    public void addLast(Object paramObject) {
        if (_directAccess) {
            super.addLast(paramObject);
            return;
        }
        if (isDelayLoad()) {
            load();
        }
        ProxyCollections.beforeAddLast(this, paramObject);
        super.addLast(paramObject);
        ProxyCollections.afterAddLast(this, paramObject);
    }

    @Override
    public boolean offer(Object paramObject) {
        if (_directAccess) {
            return super.offer(paramObject);
        }
        ProxyCollections.beforeOffer(this, paramObject);
        boolean bool = super.offer(paramObject);
        return ProxyCollections.afterOffer(this, paramObject, bool);
    }

    @Override
    public Object removeFirst() {
        if (_directAccess) {
            return super.removeFirst();
        }
        if (isDelayLoad()) {
            load();
        }
        ProxyCollections.beforeRemoveFirst(this);
        Object localObject = super.removeFirst();
        return ProxyCollections.afterRemoveFirst(this, localObject);
    }

    @Override
    public Object removeLast() {
        if (_directAccess) {
            return super.removeLast();
        }
        if (isDelayLoad()) {
            load();
        }
        ProxyCollections.beforeRemoveLast(this);
        Object localObject = super.removeLast();
        return ProxyCollections.afterRemoveLast(this, localObject);
    }

    @Override
    public Iterator iterator() {
        if (_directAccess) {
            return super.iterator();
        }
        if (isDelayLoad()) {
            load();
        }
        Iterator localIterator = super.iterator();
        return ProxyCollections.afterIterator(this, localIterator);
    }

    @Override
    public ListIterator listIterator() {
        if (_directAccess) {
            return super.listIterator();
        }
        if (isDelayLoad()) {
            load();
        }
        ListIterator localListIterator = super.listIterator();
        return ProxyCollections.afterListIterator(this, localListIterator);
    }

    @Override
    public boolean removeAll(Collection paramCollection) {
        if (_directAccess) {
            return super.removeAll(paramCollection);
        }
        return ProxyCollections.removeAll(this, paramCollection);
    }

    @Override
    public boolean retainAll(Collection paramCollection) {
        if (_directAccess) {
            return super.retainAll(paramCollection);
        }
        if (isDelayLoad()) {
            load();
        }
        return ProxyCollections.retainAll(this, paramCollection);
    }

    @Override
    public boolean removeFirstOccurrence(Object paramObject) {
        if (_directAccess) {
            return super.removeFirstOccurrence(paramObject);
        }
        if (isDelayLoad()) {
            load();
        }
        Proxies.dirty(this, true);
        return super.removeFirstOccurrence(paramObject);
    }

    @Override
    public boolean removeLastOccurrence(Object paramObject) {
        if (_directAccess) {
            return super.removeLastOccurrence(paramObject);
        }
        if (isDelayLoad()) {
            load();
        }
        Proxies.dirty(this, true);
        return super.removeLastOccurrence(paramObject);
    }

    protected Object writeReplace() throws ObjectStreamException {
        if (isDelayLoad()) {
            load();
        }
        return Proxies.writeReplace(this, true);
    }

    @Override
    public boolean equals(Object paramObject) {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.equals(paramObject);
    }

    @Override
    public int hashCode() {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.hashCode();
    }

    @Override
    public List subList(int fromIndex, int toIndex) {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.subList(fromIndex, toIndex);
    }

    @Override
    public int lastIndexOf(Object o) {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.lastIndexOf(o);
    }

    @Override
    public int indexOf(Object o) {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.indexOf(o);
    }

    @Override
    public Object get(int index) {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.get(index);
    }

    @Override
    public boolean containsAll(Collection c) {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.containsAll(c);
    }

    @Override
    public Object[] toArray() {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.toArray();
    }

    @Override
    public Object[] toArray(Object[] array) {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.toArray(array);
    }

    @Override
    public Object[] toArray(IntFunction generator) {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.toArray(generator);
    }

    @Override
    public boolean contains(Object object) {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.contains(object);
    }

    @Override
    public boolean isEmpty() {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.isEmpty();
    }

    @Override
    public int size() {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.size();
    }

    @Override
    public boolean offerFirst(Object paramObject) {
        if (_directAccess) {
            return super.offerFirst(paramObject);
        }
        if (isDelayLoad()) {
            load();
        }
        return super.offerFirst(paramObject);
    }

    @Override
    public boolean offerLast(Object paramObject) {
        if (_directAccess) {
            return super.offerLast(paramObject);
        }
        if (isDelayLoad()) {
            load();
        }
        return super.offerLast(paramObject);
    }

    @Override
    public Object pollFirst() {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.pollFirst();

    }

    @Override
    public Object pollLast() {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.pollLast();
    }

    @Override
    public Object getFirst() {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.getFirst();
    }

    @Override
    public Object getLast() {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.getLast();
    }

    @Override
    public Object peekFirst() {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.peekFirst();
    }

    @Override
    public Object peekLast() {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.peekLast();
    }

    @Override
    public Object element() {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.element();
    }

    @Override
    public Object peek() {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.peek();
    }

    @Override
    public void push(Object o) {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        super.push(o);
    }

    @Override
    public Object pop() {
        if (!_directAccess && isDelayLoad()) {
            load();
        }
        return super.pop();
    }

    @Override
    public Iterator descendingIterator() {
        if (_directAccess) {
            return super.descendingIterator();
        }
        if (isDelayLoad()) {
            load();
        }
        Iterator localIterator = super.descendingIterator();
        return ProxyCollections.afterIterator(this, localIterator);
    }
}
