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

import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.openjpa.enhance.PersistenceCapable;
import org.apache.openjpa.event.CallbackModes;
import org.apache.openjpa.event.LifecycleEvent;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.ValueMetaData;
import org.apache.openjpa.util.CallbackException;
import org.apache.openjpa.util.Exceptions;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.OpenJPAException;
import org.apache.openjpa.util.OptimisticException;
import org.apache.openjpa.util.ProxyManager;
import org.apache.openjpa.util.UserException;

/**
 * Handles attaching instances.
 *
 * @author Marc Prud'hommeaux
 */
public class AttachManager {

    private static final Localizer _loc = Localizer.forPackage
        (AttachManager.class);

    private final BrokerImpl _broker;
    private final ProxyManager _proxy;
    private final OpCallbacks _call;
    private final boolean _copyNew;
    private final boolean _failFast;
    private final IdentityHashMap _attached = new IdentityHashMap();
    private final Collection<StateManagerImpl> _visitedNodes = new ArrayList();

    // reusable strategies
    private AttachStrategy _version;
    private AttachStrategy _detach;

    /**
     * Constructor. Supply broker attaching to.
     */
    public AttachManager(BrokerImpl broker, boolean copyNew, OpCallbacks call) {
        _broker = broker;
        _proxy = broker.getConfiguration().getProxyManagerInstance();
        _call = call;
        _copyNew = copyNew;
        _failFast = (broker.getConfiguration().getMetaDataRepositoryInstance().
            getMetaDataFactory().getDefaults().getCallbackMode()
            & CallbackModes.CALLBACK_FAIL_FAST) != 0;
    }

    /**
     * Return the behavior supplied on construction.
     */
    public OpCallbacks getBehavior() {
        return _call;
    }

    /**
     * Return whether to copy new instances being persisted.
     */
    public boolean getCopyNew() {
        return _copyNew;
    }

    /**
     * Return an attached version of the given instance.
     */
    public Object attach(Object pc) {
        if (pc == null)
            return null;

        CallbackException excep = null;
        try {
            return attach(pc, null, null, null, true);
        } catch (CallbackException ce) {
            excep = ce;
            return null; // won't be reached as the exceps will be rethrown
        } finally {
            List exceps = null;
            if (excep == null || !_failFast)
                exceps = invokeAfterAttach(null);
            else
                exceps = Collections.singletonList(excep);
            _attached.clear();
            throwExceptions(exceps, null, false);
        }
    }

    /**
     * Return attached versions of the given instances.
     */
    public Object[] attachAll(Collection instances) {
        Object[] attached = new Object[instances.size()];
        List exceps = null;
        List failed = null;
        boolean opt = true;
        boolean failFast = false;
        try {
            int i = 0;
            for (Iterator itr = instances.iterator(); itr.hasNext(); i++) {
                try {
                    attached[i] = attach(itr.next(), null, null, null, true);
                } catch (OpenJPAException ke) {
                    // track exceptions and optimistic failed objects
                    if (opt && !(ke instanceof OptimisticException))
                        opt = false;
                    if (opt && ke.getFailedObject() != null)
                        failed = add(failed, ke.getFailedObject());
                    exceps = add(exceps, ke);

                    if (ke instanceof CallbackException && _failFast) {
                        failFast = true;
                        break;
                    }
                }
                catch (RuntimeException re) {
                    exceps = add(exceps, re);
                }
            }
        } finally {
            // invoke post callbacks unless all failed
            if (!failFast && (exceps == null
                || exceps.size() < instances.size()))
                exceps = invokeAfterAttach(exceps);
            _attached.clear();
        }
        throwExceptions(exceps, failed, opt);
        return attached;
    }

    /**
     * Invoke postAttach() on any attached instances that implement
     * PostAttachCallback. This will be done after the entire graph has
     * been attached.
     */
    private List invokeAfterAttach(List exceps) {
        Set entries = _attached.entrySet();
        for (Object o : entries) {
            Map.Entry entry = (Map.Entry) o;
            Object attached = entry.getValue();
            StateManagerImpl sm = _broker.getStateManagerImpl(attached, true);
            if (sm.isNew())
                continue;
            try {
                _broker.fireLifecycleEvent(attached, entry.getKey(),
                        sm.getMetaData(), LifecycleEvent.AFTER_ATTACH);
            }
            catch (RuntimeException re) {
                exceps = add(exceps, re);
                if (_failFast && re instanceof CallbackException)
                    break;
            }
        }
        return exceps;
    }

    /**
     * Add an object to the list.
     */
    private List add(List list, Object obj) {
        if (list == null)
            list = new LinkedList();
        list.add(obj);
        return list;
    }

    /**
     * Throw exception for failures.
     */
    private void throwExceptions(List exceps, List failed, boolean opt) {
        if (exceps == null)
            return;
        if (exceps.size() == 1)
            throw (RuntimeException) exceps.get(0);

        Throwable[] t = (Throwable[]) exceps.toArray
            (new Throwable[exceps.size()]);
        if (opt && failed != null)
            throw new OptimisticException(failed, t);
        if (opt)
            throw new OptimisticException(t);
        throw new UserException(_loc.get("nested-exceps")).
            setNestedThrowables(t);
    }

    /**
     * Attach.
     *
     * @param toAttach the detached object
     * @param into the instance we're attaching into
     * @param owner state manager for <code>into</code>
     * @param ownerMeta the field we traversed to find <code>toAttach</code>
     * @param explicit whether to make new instances explicitly persistent
     */
    Object attach(Object toAttach, PersistenceCapable into,
        OpenJPAStateManager owner, ValueMetaData ownerMeta, boolean explicit) {
        if (toAttach == null)
            return null;

        // check if already attached
        Object attached = _attached.get(toAttach);
        if (attached != null)
            return attached;

        //### need to handle ACT_CASCADE
        int action = processArgument(toAttach);
        if ((action & OpCallbacks.ACT_RUN) == 0 &&
            (action & OpCallbacks.ACT_CASCADE) != 0) {
            if(_visitedNodes.contains(_broker.getStateManager(toAttach)))
                return toAttach;
            return handleCascade(toAttach,owner);
        }

        if ((action & OpCallbacks.ACT_RUN) == 0)
            return toAttach;

        //### need to handle ACT_RUN without also ACT_CASCADE
        ClassMetaData meta = _broker.getConfiguration().
            getMetaDataRepositoryInstance().getMetaData(
                ImplHelper.getManagedInstance(toAttach).getClass(),
                _broker.getClassLoader(), true);
        return getStrategy(toAttach).attach(this, toAttach, meta, into,
            owner, ownerMeta, explicit);
    }

    private Object handleCascade(Object toAttach, OpenJPAStateManager owner) {
        StateManagerImpl sm = _broker.getStateManagerImpl(toAttach, true);
        BitSet loaded = sm.getLoaded();
        FieldMetaData[] fmds = sm.getMetaData().getDefinedFields();
        for (FieldMetaData fmd : fmds) {
            if (fmd.getElement().getCascadeAttach() == ValueMetaData.CASCADE_IMMEDIATE) {
                FieldMetaData[] inverseFieldMappings = fmd.getInverseMetaDatas();
                if (inverseFieldMappings.length != 0) {
                    _visitedNodes.add(sm);
                    // Only try to attach this field is it is loaded
                    if (loaded.get(fmd.getIndex())) {
                        getStrategy(toAttach).attachField(this, toAttach, sm, fmd, true);
                    }
                }
            }
        }
        return toAttach;
    }

    /**
     * Determine the action to take on the given argument.
     */
    private int processArgument(Object obj) {
        if (_call == null)
            return OpCallbacks.ACT_RUN;
        return _call.processArgument(OpCallbacks.OP_ATTACH, obj,
            _broker.getStateManager(obj));
    }

    /**
     * Calculate proper attach strategy for instance.
     */
    private AttachStrategy getStrategy(Object toAttach) {
        PersistenceCapable pc = ImplHelper.toPersistenceCapable(toAttach,
            getBroker().getConfiguration());
        if (pc.pcGetStateManager() instanceof AttachStrategy)
            return (AttachStrategy) pc.pcGetStateManager();

        Object obj = pc.pcGetDetachedState();
        if (obj instanceof AttachStrategy)
            return (AttachStrategy) obj;
        if (obj == null || obj == PersistenceCapable.DESERIALIZED) {
            // new or detached without state
            if (_version == null)
                _version = new VersionAttachStrategy();
            return _version;
        }

        // detached state
        if (_detach == null)
            _detach = new DetachedStateAttachStrategy();
        return _detach;
    }

    /**
     * Owning broker.
     */
    BrokerImpl getBroker() {
        return _broker;
    }

    /**
     * System proxy manager.
     */
    ProxyManager getProxyManager() {
        return _proxy;
    }

    /**
     * If the passed in argument has already been attached, return
     * the (cached) attached copy.
     */
    PersistenceCapable getAttachedCopy(Object pc) {
        return ImplHelper.toPersistenceCapable(_attached.get(pc),
            getBroker().getConfiguration());
    }

    /**
     * Record the attached copy in the cache.
     */
    void setAttachedCopy(Object from, PersistenceCapable into) {
        _attached.put(from, into);
    }

    /**
     * Fire before-attach event.
     */
    void fireBeforeAttach(Object pc, ClassMetaData meta) {
        _broker.fireLifecycleEvent(pc, null, meta,
            LifecycleEvent.BEFORE_ATTACH);
    }

    /**
     * Return the detached oid of the given instance.
     */
    Object getDetachedObjectId(Object pc) {
        if (pc == null)
            return null;
        return getStrategy(pc).getDetachedObjectId(this, pc);
    }

    /**
     * Throw an exception if the given object is not managed; otherwise
     * return its state manager.
     */
    StateManagerImpl assertManaged(Object obj) {
        StateManagerImpl sm = _broker.getStateManagerImpl(obj, true);
        if (sm == null)
            throw new UserException(_loc.get("not-managed",
                Exceptions.toString(obj))).setFailedObject (obj);
		return sm;
	}
}
