/*
 * 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.lang.reflect.Field;
import java.util.Collection;
import java.util.Map;

import org.apache.openjpa.enhance.PersistenceCapable;
import org.apache.openjpa.enhance.Reflection;
import org.apache.openjpa.enhance.StateManager;
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.JavaTypes;
import org.apache.openjpa.meta.ValueMetaData;
import org.apache.openjpa.meta.ValueStrategies;
import org.apache.openjpa.util.ApplicationIds;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.ObjectNotFoundException;
import org.apache.openjpa.util.OptimisticException;

/**
 * Handles attaching instances using version and primary key fields.
 *
 * @author Steve Kim
 */
class VersionAttachStrategy
    extends AttachStrategy
    implements DetachState {

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

    @Override
    protected Object getDetachedObjectId(AttachManager manager,
        Object toAttach) {
        Broker broker = manager.getBroker();
        ClassMetaData meta = broker.getConfiguration().
            getMetaDataRepositoryInstance().getMetaData(
                ImplHelper.getManagedInstance(toAttach).getClass(),
                broker.getClassLoader(), true);
        return ApplicationIds.create(ImplHelper.toPersistenceCapable(toAttach,
            broker.getConfiguration()),
            meta);
    }

    @Override
    protected void provideField(Object toAttach, StateManagerImpl sm,
        int field) {
        sm.provideField(ImplHelper.toPersistenceCapable(toAttach,
            sm.getContext().getConfiguration()), this, field);
    }

    @Override
    public Object attach(AttachManager manager, Object toAttach,
        ClassMetaData meta, PersistenceCapable into, OpenJPAStateManager owner,
        ValueMetaData ownerMeta, boolean explicit) {
        BrokerImpl broker = manager.getBroker();
        PersistenceCapable pc = ImplHelper.toPersistenceCapable(toAttach,
            meta.getRepository().getConfiguration());

        boolean embedded = ownerMeta != null && ownerMeta.isEmbeddedPC();

        // OJ-2405: If toAttach has a StateManagerImpl, then it is important to check if it
        // is being managed by different broker.  If it is, then it should not be
        // considered "new".
        boolean isNew = !broker.isDetached(pc) && !isManagedByAnotherPCtx(pc, broker);
        Object version = null;
        StateManagerImpl sm;

        // if the state manager for the embedded instance is null, then
        // it should be treated as a new instance (since the
        // newly persisted owner may create a new embedded instance
        // in the constructor); fixed bug #1075.
        // also, if the user has attached a detached obj from somewhere
        // else in the graph to an embedded field that was previously null,
        // copy into a new embedded instance
        if (embedded && (isNew || into == null
            || broker.getStateManager(into) == null)) {
            if (into == null)
                into = pc.pcNewInstance(null, false);
            sm = (StateManagerImpl) broker.embed(into, null, owner, ownerMeta);
            into = sm.getPersistenceCapable();
        } else if (isNew) {
            Object oid = null;
            if (!isPrimaryKeysGenerated(meta))
                oid = ApplicationIds.create(pc, meta);

            sm = persist(manager, pc, meta, oid, explicit);
            into = sm.getPersistenceCapable();
        } else if (!embedded && into == null) {
            Object id = getDetachedObjectId(manager, toAttach);
            if (id != null)
                into =
                    ImplHelper.toPersistenceCapable(broker.find(id, true, null),
                        broker.getConfiguration());
            if (into == null)
                throw new OptimisticException(_loc.get("attach-version-del",
                    ImplHelper.getManagedInstance(pc).getClass(), id, version))
                    .setFailedObject(toAttach);

            sm = manager.assertManaged(into);
            if (meta.getDescribedType()
                != sm.getMetaData().getDescribedType()) {
                throw new ObjectNotFoundException(_loc.get
                    ("attach-wrongclass", id, toAttach.getClass(),
                        sm.getMetaData().getDescribedType())).
                    setFailedObject(toAttach);
            }
        } else
            sm = manager.assertManaged(into);

        // mark that we attached the instance *before* we
        // fill in values to avoid endless recursion
        manager.setAttachedCopy(toAttach, into);

        // if persisting in place, just attach field values
        if (pc == into) {
            attachFieldsInPlace(manager, sm);
            return into;
        }

        if (isNew) {
            broker.fireLifecycleEvent(toAttach, null, meta,
                LifecycleEvent.BEFORE_PERSIST);
        } else {
            // invoke any preAttach on the detached instance
            manager.fireBeforeAttach(toAttach, meta);
        }

        // assign the detached pc the same state manager as the object we're
        // copying into during the attach process
        StateManager smBefore = pc.pcGetStateManager();
        pc.pcReplaceStateManager(sm);
        int detach = (isNew) ? DETACH_ALL : broker.getDetachState();
        FetchConfiguration fetch = broker.getFetchConfiguration();
        try {
            FieldMetaData[] fmds = sm.getMetaData().getFields();
            for (FieldMetaData fmd : fmds) {
                switch (detach) {
                    case DETACH_ALL:
                        attachField(manager, toAttach, sm, fmd, true);
                        break;
                    case DETACH_FETCH_GROUPS:
                        if (fetch.requiresFetch(fmd)
                                != FetchConfiguration.FETCH_NONE)
                            attachField(manager, toAttach, sm, fmd, true);
                        break;
                    case DETACH_LOADED:
                        attachField(manager, toAttach, sm, fmd, false);
                        break;
                }
            }
        } finally {
            pc.pcReplaceStateManager(smBefore);
        }
        if (!embedded && !isNew)
            compareVersion(sm, pc);
        return ImplHelper.getManagedInstance(into);
    }

    /**
     * Make sure the version information is correct in the detached object.
     */
    private void compareVersion(StateManagerImpl sm, PersistenceCapable pc) {
        Object version = pc.pcGetVersion();
        // In the event that the version field is a primitive and it is the types default value, we can't differentiate
        // between a value that was set to be the default, and one that defaulted to that value.
        if (version != null
                && JavaTypes.isPrimitiveDefault(version, sm.getMetaData().getVersionField().getTypeCode())) {
            Field pcVersionInitField = null;
            try {
                pcVersionInitField = pc.getClass().getDeclaredField("pcVersionInit");
                Object pcField = Reflection.get(pc, pcVersionInitField);
                if (pcField != null) {
                    boolean bool = (Boolean) pcField;
                    if (!bool) {
                        // If this field if false, that means that the pcGetVersion returned a default value rather than
                        // and actual value.
                        version = null;
                    }
                }
            } catch (Exception e) {
                // Perhaps this is an Entity that was enhanced before the pcVersionInit field was added.
            }
        }
        if (version == null) {
            return;
        }
        // don't need to load unloaded fields since its implicitly
        // a single field value
        StoreManager store = sm.getBroker().getStoreManager();
        switch (store.compareVersion(sm, version, sm.getVersion())) {
            case StoreManager.VERSION_LATER:
                // we have a later version: set it into the object.
                // lock validation will occur at commit time
                sm.setVersion(version);
                break;
            case StoreManager.VERSION_EARLIER:
            case StoreManager.VERSION_DIFFERENT:
                sm.setVersion(version);
                throw new OptimisticException(sm.getManagedInstance());
            case StoreManager.VERSION_SAME:
                // no action required
                break;
        }
    }

    /**
     * Attach the fields of an in-place persisted instance.
     */
    private void attachFieldsInPlace(AttachManager manager,
        StateManagerImpl sm) {
        FieldMetaData[] fmds = sm.getMetaData().getFields();
        for (int i = 0; i < fmds.length; i++) {
            if (fmds[i].getManagement() != FieldMetaData.MANAGE_PERSISTENT)
                continue;

            Object cur, attached;
            switch (fmds[i].getDeclaredTypeCode()) {
                case JavaTypes.PC:
                case JavaTypes.PC_UNTYPED:
                    cur = sm.fetchObjectField(i);
                    attached = attachInPlace(manager, sm, fmds[i], cur);
                    break;
                case JavaTypes.ARRAY:
                    if (!fmds[i].getElement().isDeclaredTypePC())
                        continue;
                    cur = sm.fetchObjectField(i);
                    attached =
                        attachInPlace(manager, sm, fmds[i], (Object[]) cur);
                    break;
                case JavaTypes.COLLECTION:
                    if (!fmds[i].getElement().isDeclaredTypePC())
                        continue;
                    cur = sm.fetchObjectField(i);
                    attached = attachInPlace(manager, sm, fmds[i],
                        (Collection) cur);
                    break;
                case JavaTypes.MAP:
                    if (!fmds[i].getElement().isDeclaredTypePC()
                        && !fmds[i].getKey().isDeclaredTypePC())
                        continue;
                    cur = sm.fetchObjectField(i);
                    attached = attachInPlace(manager, sm, fmds[i], (Map) cur);
                    break;
                default:
                    continue;
            }

            if (cur != attached)
                sm.settingObjectField(sm.getPersistenceCapable(), i,
                    cur, attached, StateManager.SET_REMOTE);
        }
    }

    /**
     * Attach the given pc.
     */
    private Object attachInPlace(AttachManager manager, StateManagerImpl sm,
        ValueMetaData vmd, Object pc) {
        if (pc == null)
            return null;
        Object attached = manager.getAttachedCopy(pc);
        if (attached != null)
            return attached;

        OpenJPAStateManager into = manager.getBroker().getStateManager(pc);
        PersistenceCapable intoPC = (into == null) ? null
            : into.getPersistenceCapable();
        if (vmd.isEmbedded())
            return manager.attach(pc, intoPC, sm, vmd, false);
        return manager.attach(pc, intoPC, null, null, false);
    }

    /**
     * Attach the given array.
     */
    private Object[] attachInPlace(AttachManager manager, StateManagerImpl sm,
        FieldMetaData fmd, Object[] arr) {
        if (arr == null)
            return null;

        for (int i = 0; i < arr.length; i++)
            arr[i] = attachInPlace(manager, sm, fmd.getElement(), arr[i]);
        return arr;
    }

    /**
     * Attach the given collection.
     */
    private Collection attachInPlace(AttachManager manager,
        StateManagerImpl sm, FieldMetaData fmd, Collection coll) {
        if (coll == null || coll.isEmpty())
            return coll;

        // copy if elements embedded or contains detached, which will mean
        // we'll have to copy the existing elements
        Collection copy = null;
        if (fmd.getElement().isEmbedded())
            copy = (Collection) sm.newFieldProxy(fmd.getIndex());
        else {
            for (Object o : coll) {
                if (manager.getBroker().isDetached(o)) {
                    copy = (Collection) sm.newFieldProxy(fmd.getIndex());
                    break;
                }
            }
        }

        Object attached;
        for (Object o : coll) {
            attached = attachInPlace(manager, sm, fmd.getElement(),
                    o);
            if (copy != null)
                copy.add(attached);
        }
        return (copy == null) ? coll : copy;
    }

    /**
     * Attach the given map.
     */
    private Map attachInPlace(AttachManager manager, StateManagerImpl sm,
        FieldMetaData fmd, Map map) {
        if (map == null || map.isEmpty())
            return map;

        Map copy = null;
        Map.Entry entry;
        boolean keyPC = fmd.getKey().isDeclaredTypePC();
        boolean valPC = fmd.getElement().isDeclaredTypePC();

        // copy if embedded pcs or detached pcs, which will require us to
        // copy elements
        if (fmd.getKey().isEmbeddedPC() || fmd.getElement().isEmbeddedPC())
            copy = (Map) sm.newFieldProxy(fmd.getIndex());
        else {
            for (Object o : map.entrySet()) {
                entry = (Map.Entry) o;
                if ((keyPC && manager.getBroker().isDetached(entry.getKey()))
                        || (valPC && manager.getBroker().isDetached
                        (entry.getValue()))) {
                    copy = (Map) sm.newFieldProxy(fmd.getIndex());
                    break;
                }
            }
        }

        Object key, val;
        for (Object o : map.entrySet()) {
            entry = (Map.Entry) o;
            key = entry.getKey();
            if (keyPC)
                key = attachInPlace(manager, sm, fmd.getKey(), key);
            val = entry.getValue();
            if (valPC)
                val = attachInPlace(manager, sm, fmd.getElement(), val);
            if (copy != null)
                copy.put(key, val);
        }
        return (copy == null) ? map : copy;
	}

    /**
     * Find a PersistenceCapable instance of an Object if it exists in the
     * database. If the object is null or can't be found in the database.
     *
     * @param pc An object which will be attached into the current context. The
     * object may or may not correspond to a row in the database.
     *
     * @return If the object is null or can't be found in the database this
     * method returns null. Otherwise a PersistenceCapable representation of the
     * object is returned.
     */
    protected PersistenceCapable findFromDatabase(AttachManager manager,
        Object pc) {
        Object oid = manager.getBroker().newObjectId(pc.getClass(),
            manager.getDetachedObjectId(pc));

        if (oid != null) {
            return ImplHelper.toPersistenceCapable(
                manager.getBroker().find(oid, true, null),
                manager.getBroker().getConfiguration());
        } else {
            return null;
        }
    }

    private boolean isPrimaryKeysGenerated(ClassMetaData meta) {
        FieldMetaData[] pks = meta.getPrimaryKeyFields();
        for (FieldMetaData pk : pks) {
            if (pk.getValueStrategy() != ValueStrategies.NONE)
                return true;
        }
        return false;
    }

    private static boolean isManagedByAnotherPCtx(PersistenceCapable pc, BrokerImpl broker) {
        StateManager sm = pc.pcGetStateManager();
        if (sm != null && sm instanceof StateManagerImpl) {
            StateManagerImpl smi = (StateManagerImpl) sm;
            Broker associatedBroker = smi.getBroker();

            if (broker != associatedBroker) {
                return true;
            }
        }

        return false;
    }
}
