/*
 * 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.io.IOException;
import java.io.ObjectOutput;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.openjpa.conf.Compatibility;
import org.apache.openjpa.conf.DetachOptions;
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.JavaTypes;
import org.apache.openjpa.meta.ValueMetaData;
import org.apache.openjpa.util.CallbackException;
import org.apache.openjpa.util.ObjectNotFoundException;
import org.apache.openjpa.util.Proxy;
import org.apache.openjpa.util.ProxyManager;
import org.apache.openjpa.util.UserException;

/**
 * Handles detaching instances.
 *
 * @author Marc Prud'hommeaux
 */
public class DetachManager
    implements DetachState {

    private static Localizer _loc = Localizer.forPackage(DetachManager.class);

    private final BrokerImpl _broker;
    private final boolean _copy;
    private final boolean _full;
    private final ProxyManager _proxy;
    private final DetachOptions _opts;
    private final OpCallbacks _call;
    private final boolean _failFast;
    private boolean _flushed = false;
    private boolean _flushBeforeDetach;
    private boolean _cascadeWithDetach;
    private boolean _reloadOnDetach;

    // if we're not detaching full, we need to track all detached objects;
    // if we are, then we use a special field manager for more efficient
    // detachment than the standard one
    private final IdentityHashMap _detached;
    private final DetachFieldManager _fullFM;

    /**
     * Used to prepare a detachable instance that does not externalize
     * detached state.
     */
    static boolean preSerialize(StateManagerImpl sm) {
        if (!sm.isPersistent())
            return false;

        if (sm.getBroker().getConfiguration().getCompatibilityInstance()
                .getFlushBeforeDetach()) {
            flushDirty(sm);
        }

        ClassMetaData meta = sm.getMetaData();
        boolean setState = meta.getDetachedState() != null
            && !ClassMetaData.SYNTHETIC.equals(meta.getDetachedState());
        BitSet idxs = (setState) ? new BitSet(meta.getFields().length) : null;
        preDetach(sm.getBroker(), sm, idxs, false, true);

        if (setState) {
            sm.getPersistenceCapable().pcSetDetachedState(getDetachedState
                (sm, idxs));
            return false; // don't null state
        }
        return true;
    }

    /**
     * Used by classes that externalize detached state.
     *
     * @return whether to use a detached state manager
     */
    static boolean writeDetachedState(StateManagerImpl sm, ObjectOutput out,
        BitSet idxs)
        throws IOException {
        if (!sm.isPersistent()) {
            out.writeObject(null); // state
            out.writeObject(null); // sm
            return false;
        }

        // dirty state causes flush
        flushDirty(sm);

        Broker broker = sm.getBroker();
        preDetach(broker, sm, idxs, false, true);

        // write detached state object and state manager
        DetachOptions opts = broker.getConfiguration().
            getDetachStateInstance();
        if (!opts.getDetachedStateManager()
            || !useDetachedStateManager(sm, opts)) {
            out.writeObject(getDetachedState(sm, idxs));
            out.writeObject(null);
            return false;
        }
        out.writeObject(null);
        out.writeObject(new DetachedStateManager(sm.getPersistenceCapable(),
            sm, idxs, opts.getAccessUnloaded(), broker.getMultithreaded()));
        return true;
    }

    /**
     * Ready the object for detachment, including loading the fields to be
     * detached and updating version information.
     *
     * @param idxs the indexes of fields to detach will be set as a side
     * effect of this method
     */
    private static void preDetach(Broker broker, StateManagerImpl sm,
        BitSet idxs, boolean full,
        boolean reloadOnDetach) {
        // make sure the existing object has the right fields fetched; call
        // even if using currently-loaded fields for detach to make sure
        // version is set
        int detachMode = broker.getDetachState();
        int loadMode = StateManagerImpl.LOAD_FGS;
        BitSet exclude = null;
        if (detachMode == DETACH_LOADED)
            exclude = StoreContext.EXCLUDE_ALL;
        else if (detachMode == DETACH_ALL)
            loadMode = StateManagerImpl.LOAD_ALL;
        try {
            if (detachMode != DETACH_LOADED ||
                    reloadOnDetach ||
                    (!reloadOnDetach && !full)) {
                sm.load(broker.getFetchConfiguration(), loadMode, exclude,
                    null, false);
            }
        } catch (ObjectNotFoundException onfe) {
            // consume the exception
        }

        // create bitset of fields to detach; if mode is all we can use
        // currently loaded bitset clone, since we know all fields are loaded
        if (idxs != null) {
            if (detachMode == DETACH_FETCH_GROUPS)
                setFetchGroupFields(broker, sm, idxs);
            else
                idxs.or(sm.getLoaded());

            // clear lrs fields
            FieldMetaData[] fmds = sm.getMetaData().getFields();
            for (int i = 0; i < fmds.length; i++)
                if (fmds[i].isLRS())
                    idxs.clear(i);
        }
    }

    /**
     * Generate the detached state for the given instance.
     */
    private static Object getDetachedState(StateManagerImpl sm, BitSet fields) {
        // if datastore, store id in first element
        int offset = (sm.getMetaData().getIdentityType() ==
            ClassMetaData.ID_DATASTORE) ? 1 : 0;

        // make version state array one larger for new instances; marks new
        // instances without affecting serialization size much
        Object[] state;
        if (sm.isNew())
            state = new Object[3 + offset];
        else
            state = new Object[2 + offset];

        if (offset > 0) {
            Object id;
            if (sm.isEmbedded() || sm.getObjectId() == null)
                id = sm.getId();
            else
                id = sm.getObjectId();
            state[0] = id.toString();
        }
        state[offset] = sm.getVersion();
        state[offset + 1] = fields;
        return state;
    }

    /**
     * Flush or invoke pre-store callbacks on the given broker if
     * needed. Return true if flushed/stored, false otherwise.
     */
    private static boolean flushDirty(StateManagerImpl sm) {
        if (!sm.isDirty() || !sm.getBroker().isActive())
            return false;

        // only flush if there are actually any dirty non-flushed fields
        BitSet dirtyFields = sm.getDirty();
        BitSet flushedFields = sm.getFlushed();
        for (int i = 0; i < dirtyFields.size(); i++) {
            if (dirtyFields.get(i) && !flushedFields.get(i)) {
                if (sm.getBroker().getRollbackOnly())
                    sm.getBroker().preFlush();
                else
                    sm.getBroker().flush();
                return true;
            }
        }
        return false;
    }

    /**
     * Create a bit set for the fields in the current fetch groups.
     */
    private static void setFetchGroupFields(Broker broker,
        StateManagerImpl sm, BitSet idxs) {
        FetchConfiguration fetch = broker.getFetchConfiguration();
        FieldMetaData[] fmds = sm.getMetaData().getFields();
        for (int i = 0; i < fmds.length; i++) {
            if (fmds[i].isPrimaryKey() || fetch.requiresFetch(fmds[i])
                != FetchConfiguration.FETCH_NONE)
                idxs.set(i);
        }
    }


    /**
     * Constructor.
     *
     * @param broker owning broker
     * @param full whether the entire broker cache is being detached; if
     * this is the case, we assume the broker has already
     * flushed if needed, and that we're detaching in-place
     */
    public DetachManager(BrokerImpl broker, boolean full, OpCallbacks call) {
        _broker = broker;
        _proxy = broker.getConfiguration().getProxyManagerInstance();
        _opts = broker.getConfiguration().getDetachStateInstance();
        _flushed = full;
        _call = call;
        _failFast = (broker.getConfiguration().getMetaDataRepositoryInstance().
            getMetaDataFactory().getDefaults().getCallbackMode()
            & CallbackModes.CALLBACK_FAIL_FAST) != 0;

        // we can only rely on our "full" shortcuts if we know we won't be
        // loading any more data
        _full = full && broker.getDetachState() == DetachState.DETACH_LOADED;
        if (_full) {
            _detached = null;
            _fullFM = new DetachFieldManager();
        } else {
            _detached = new IdentityHashMap();
            _fullFM = null;
        }
        Compatibility compatibility =
            broker.getConfiguration().getCompatibilityInstance();
        _flushBeforeDetach = compatibility.getFlushBeforeDetach();
        _reloadOnDetach = compatibility.getReloadOnDetach();
        _cascadeWithDetach = compatibility.getCascadeWithDetach();
        if (full) {
            _copy = false;
        }
        else {
            _copy = compatibility.getCopyOnDetach();
        }
    }

    /**
     * Return a detached version of the given instance.
     */
    public Object detach(Object toDetach) {
        List exceps = null;
        try {
            return detachInternal(toDetach);
        } catch (CallbackException ce) {
            exceps = new ArrayList(1);
            exceps.add(ce);
            return null; // won't be reached as exception will be rethrown
        } finally {
            if (exceps == null || !_failFast)
                exceps = invokeAfterDetach(Collections.singleton(toDetach),
                    exceps);
            if (_detached != null)
                _detached.clear();
            throwExceptions(exceps);
        }
    }

    /**
     * Return detached versions of all the given instances. If not copying,
     * null will be returned.
     */
    public Object[] detachAll(Collection instances) {
        List exceps = null;
        List detached = null;
        if (_copy)
            detached = new ArrayList(instances.size());

        boolean failFast = false;
        try {
            Object detach;
            for (Object instance : instances) {
                detach = detachInternal(instance);
                if (_copy)
                    detached.add(detach);
            }
        }
        catch (RuntimeException re) {
            if (re instanceof CallbackException && _failFast)
                failFast = true;
            exceps = add(exceps, re);
        } finally {
            if (!failFast)
                exceps = invokeAfterDetach(instances, exceps);
            if (_detached != null)
                _detached.clear();
        }
        throwExceptions(exceps);

        if (_copy)
            return detached.toArray();
        return null;
    }

    /**
     * Invoke postDetach() on any detached instances that implement
     * PostDetachCallback. This will be done after the entire graph has
     * been detached. This method has the side-effect of also clearing
     * out the map of all detached instances.
     */
    private List invokeAfterDetach(Collection objs, List exceps) {
        Iterator itr = (_full) ? objs.iterator()
            : _detached.entrySet().iterator();

        Object orig, detached;
        Map.Entry entry;
        while (itr.hasNext()) {
            if (_full) {
                orig = itr.next();
                detached = orig;
            } else {
                entry = (Map.Entry) itr.next();
                orig = entry.getKey();
                detached = entry.getValue();
            }

            StateManagerImpl sm = _broker.getStateManagerImpl(orig, true);
            try {
                if (sm != null)
                    _broker.fireLifecycleEvent(detached, orig,
                        sm.getMetaData(), LifecycleEvent.AFTER_DETACH);
            } catch (CallbackException ce) {
                exceps = add(exceps, ce);
                if (_failFast)
                    break; // don't continue processing
            }
        }
        return exceps;
    }

    /**
     * Add an exception to the list.
     */
    private List add(List exceps, RuntimeException re) {
        if (exceps == null)
            exceps = new LinkedList();
        exceps.add(re);
        return exceps;
    }

    /**
     * Throw all gathered exceptions.
     */
    private void throwExceptions(List exceps) {
        if (exceps == null)
            return;

        if (exceps.size() == 1)
            throw (RuntimeException) exceps.get(0);
        throw new UserException(_loc.get("nested-exceps")).
            setNestedThrowables((Throwable[]) exceps.toArray
                (new Throwable[exceps.size()]));
    }

    /**
     * Detach.
     */
    private Object detachInternal(Object toDetach) {
        if (toDetach == null)
            return null;

        // already detached?
        if (_detached != null) {
            Object detached = _detached.get(toDetach);
            if (detached != null)
                return detached;
        }

        StateManagerImpl sm = _broker.getStateManagerImpl(toDetach, true);
        if (_call != null && (_call.processArgument(OpCallbacks.OP_DETACH,
            toDetach, sm) & OpCallbacks.ACT_RUN) == 0)
            return toDetach;
        if (sm == null)
            return toDetach;

        // Call PreDetach first as we can't tell if the new system
        // fired an event or just did not fail.
        _broker.fireLifecycleEvent(toDetach, null, sm.getMetaData(),
            LifecycleEvent.BEFORE_DETACH);

        if(! _flushed)  {
            if(_flushBeforeDetach) {
                // any dirty instances cause a flush to occur
                flushDirty(sm);
            }
            _flushed = true;
        }

        BitSet fields = new BitSet();
        preDetach(_broker, sm, fields, _full,
            _reloadOnDetach);

        // create and store new object before copy to avoid endless recursion
        PersistenceCapable pc = sm.getPersistenceCapable();
        PersistenceCapable detachedPC;
        if (_copy)
            detachedPC = pc.pcNewInstance(null, true);
        else
            detachedPC = pc;
        if (_detached != null)
            _detached.put(toDetach, detachedPC);

        // detach fields and set detached variables
        DetachedStateManager detSM = null;
        if (_opts.getDetachedStateManager()
            && useDetachedStateManager(sm, _opts)
            && !(sm.isNew() && !sm.isDeleted() && !sm.isFlushed()))
            detSM = new DetachedStateManager(detachedPC, sm, fields,
                _opts.getAccessUnloaded(), _broker.getMultithreaded());
        if (_full) {
            _fullFM.setStateManager(sm);
            if (_copy || _reloadOnDetach) {
                _fullFM.detachVersion();
            }
            _fullFM.reproxy(detSM);
            _fullFM.setStateManager(null);
        } else {
            InstanceDetachFieldManager fm = new InstanceDetachFieldManager(detachedPC, detSM);
            fm.setStateManager(sm);
            fm.detachFields(fields);
        }

        if (!Boolean.FALSE.equals(sm.getMetaData().usesDetachedState()))
            detachedPC.pcSetDetachedState(getDetachedState(sm, fields));
        if (!_copy)
            sm.release(false, true);
        if (detSM != null)
            detachedPC.pcReplaceStateManager(detSM);
        return detachedPC;
    }

    private static boolean useDetachedStateManager(StateManagerImpl sm,
        DetachOptions opts) {
        ClassMetaData meta = sm.getMetaData();
        return !Boolean.FALSE.equals(meta.usesDetachedState()) &&
            ClassMetaData.SYNTHETIC.equals(meta.getDetachedState()) &&
            opts.getDetachedStateManager();
    }

    /**
     * Base detach field manager.
     */
    private static class DetachFieldManager
        extends TransferFieldManager {

        protected StateManagerImpl sm;

        /**
         * Set the source state manager.
         */
        public void setStateManager(StateManagerImpl sm) {
            this.sm = sm;
        }

        /**
         * Transfer the current version object from the state manager to the
         * detached instance.
         */
        public void detachVersion() {
            FieldMetaData fmd = sm.getMetaData().getVersionField();
            if (fmd == null)
                return;

            Object val = JavaTypes.convert(sm.getVersion(),
                fmd.getTypeCode());
            val = fmd.getFieldValue(val, sm.getBroker());
            switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.LONG:
            case JavaTypes.SHORT:
            case JavaTypes.INT:
            case JavaTypes.BYTE:
                longval = (val == null) ? 0L : ((Number) val).longValue();
                break;
            case JavaTypes.DOUBLE:
            case JavaTypes.FLOAT:
                dblval = (val == null) ? 0D : ((Number) val).doubleValue();
                break;
            default:
                objval = val;
            }
            sm.replaceField(getDetachedPersistenceCapable(), this,
                fmd.getIndex());
        }

        /**
         * Unproxies second class object fields.
         */
        public void reproxy(DetachedStateManager dsm) {
            for (FieldMetaData fmd : sm.getMetaData().getProxyFields()) {
                switch (fmd.getDeclaredTypeCode()) {
                case JavaTypes.COLLECTION:
                case JavaTypes.MAP:
                    // lrs proxies not detached
                    if (fmd.isLRS()) {
                        objval = null;
                        sm.replaceField(getDetachedPersistenceCapable(), this, fmd.getIndex());
                        break;
                    }
                    // no break
                case JavaTypes.CALENDAR:
                case JavaTypes.DATE:
                case JavaTypes.OBJECT:
                    sm.provideField(getDetachedPersistenceCapable(), this, fmd.getIndex());
                    if (objval instanceof Proxy) {
                        Proxy proxy = (Proxy) objval;
                        if (proxy.getChangeTracker() != null)
                            proxy.getChangeTracker().stopTracking();
                        proxy.setOwner(dsm, (dsm == null) ? -1 : fmd.getIndex());
                    }
                }
            }
            clear();
        }

        /**
         * Return the instance being detached.
         */
        protected PersistenceCapable getDetachedPersistenceCapable() {
            return sm.getPersistenceCapable();
        }
    }

    /**
     * FieldManager that can copy all the fields from one
     * PersistenceCapable instance to another. One of the
     * instances must be managed by a StateManager, and the
     * other must be unmanaged.
     *
     * @author Marc Prud'hommeaux
     */
    private class InstanceDetachFieldManager
        extends DetachFieldManager {

        private final PersistenceCapable _to;
        private final DetachedStateManager _detSM;

        /**
         * Constructor. Supply instance to to copy to.
         */
        public InstanceDetachFieldManager(PersistenceCapable to,
            DetachedStateManager detSM) {
            _to = to;
            _detSM = detSM;
        }

        @Override
        protected PersistenceCapable getDetachedPersistenceCapable() {
            return _to;
        }

        /**
         * Detach the fields of the state manager given on construction to
         * the persistence capable given on construction.
         * Only the fields in the given bit set will be copied.
         */
        public void detachFields(BitSet fgfields) {
            PersistenceCapable from = sm.getPersistenceCapable();
            FieldMetaData[] pks = sm.getMetaData().getPrimaryKeyFields();
            FieldMetaData[] fmds = sm.getMetaData().getFields();

            if (_copy)
                _to.pcReplaceStateManager(sm);
            try {
                // we start with pk fields: objects might rely on pk fields for
                // equals and hashCode methods, and this ensures that pk fields
                // are set properly if we return any partially-detached objects
                // due to reentrant calls when traversing relations
                for (FieldMetaData pk : pks) {
                    detachField(from, pk.getIndex(), true);
                }
                detachVersion();
                for (int i = 0; i < fmds.length; i++)
                    if (!fmds[i].isPrimaryKey() && !fmds[i].isVersion())
                        detachField(from, i, fgfields.get(i));
            } finally {
                // clear the StateManager from the target object
                if (_copy)
                    _to.pcReplaceStateManager(null);
            }
        }

        /**
         * Detach (or clear) the given field index.
         */
        private void detachField(PersistenceCapable from, int i, boolean fg) {
            // tell the state manager to provide the fields from the source to
            // this field manager, which will then replace the field with a
            // detached version
            if (fg)
                sm.provideField(from, this, i);
            else if (!_copy) {
                // if not copying and field should not be detached, clear it
                clear();
                sm.replaceField(_to, this, i);
            }
        }

        @Override
        public void storeBooleanField(int field, boolean curVal) {
            super.storeBooleanField(field, curVal);
            sm.replaceField(_to, this, field);
        }

        @Override
        public void storeByteField(int field, byte curVal) {
            super.storeByteField(field, curVal);
            sm.replaceField(_to, this, field);
        }

        @Override
        public void storeCharField(int field, char curVal) {
            super.storeCharField(field, curVal);
            sm.replaceField(_to, this, field);
        }

        @Override
        public void storeDoubleField(int field, double curVal) {
            super.storeDoubleField(field, curVal);
            sm.replaceField(_to, this, field);
        }

        @Override
        public void storeFloatField(int field, float curVal) {
            super.storeFloatField(field, curVal);
            sm.replaceField(_to, this, field);
        }

        @Override
        public void storeIntField(int field, int curVal) {
            super.storeIntField(field, curVal);
            sm.replaceField(_to, this, field);
        }

        @Override
        public void storeLongField(int field, long curVal) {
            super.storeLongField(field, curVal);
            sm.replaceField(_to, this, field);
        }

        @Override
        public void storeShortField(int field, short curVal) {
            super.storeShortField(field, curVal);
            sm.replaceField(_to, this, field);
        }

        @Override
        public void storeStringField(int field, String curVal) {
            super.storeStringField(field, curVal);
            sm.replaceField(_to, this, field);
        }

        @Override
        public void storeObjectField(int field, Object curVal) {
            super.storeObjectField(field, detachField(curVal, field));
            sm.replaceField(_to, this, field);
        }

        /**
         * Set the owner of the field's proxy to the detached state manager.
         */
        private Object reproxy(Object obj, int field) {
            if (obj != null && _detSM != null && obj instanceof Proxy)
                ((Proxy) obj).setOwner(_detSM, field);
            return obj;
        }

        /**
         * Detach the given value if needed.
         */
        private Object detachField(Object curVal, int field) {
            if (curVal == null)
                return null;

            FieldMetaData fmd = sm.getMetaData().getField(field);

            boolean cascade = false;
            if(_cascadeWithDetach
                || fmd.getCascadeDetach() ==
                    ValueMetaData.CASCADE_IMMEDIATE
                || fmd.getKey().getCascadeDetach() ==
                    ValueMetaData.CASCADE_IMMEDIATE
                || fmd.getElement().getCascadeDetach() ==
                    ValueMetaData.CASCADE_IMMEDIATE) {
                cascade = true;
            }

            Object newVal = null;
            switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.ARRAY:
                if (_copy)
                    newVal = _proxy.copyArray(curVal);
                else
                    newVal = curVal;
                if (cascade) {
                    detachArray(newVal, fmd);
                }
                return newVal;
            case JavaTypes.COLLECTION:
                if (_copy) {
                    if (_detSM != null) {
                        newVal = _proxy.newCollectionProxy(fmd.getProxyType(),
                            fmd.getElement().getDeclaredType(),
                            fmd.getInitializer() instanceof Comparator ?
                            (Comparator) fmd.getInitializer() : null,
                            sm.getBroker().getConfiguration().
                            getCompatibilityInstance().getAutoOff());
                        ((Collection) newVal).addAll((Collection) curVal);
                    } else
                        newVal = _proxy.copyCollection((Collection) curVal);
                } else
                    newVal = curVal;
                if (cascade) {
                    detachCollection((Collection) newVal, (Collection) curVal,
                        fmd);
                }
                return reproxy(newVal, field);
            case JavaTypes.MAP:
                if (_copy) {
                    if (_detSM != null) {
                        newVal = _proxy.newMapProxy(fmd.getProxyType(),
                            fmd.getKey().getDeclaredType(),
                            fmd.getElement().getDeclaredType(),
                            fmd.getInitializer() instanceof Comparator ?
                                (Comparator) fmd.getInitializer() : null,
                                sm.getBroker().getConfiguration().
                                getCompatibilityInstance().getAutoOff());
                        ((Map) newVal).putAll((Map) curVal);
                    } else
                        newVal = _proxy.copyMap((Map) curVal);
                } else
                    newVal = curVal;
                if (cascade) {
                    detachMap((Map) newVal, (Map) curVal, fmd);
                }
                return reproxy(newVal, field);
            case JavaTypes.CALENDAR:
                newVal = (_copy) ? _proxy.copyCalendar((Calendar) curVal) :
                    curVal;
                return reproxy(newVal, field);
            case JavaTypes.DATE:
                newVal = (_copy) ? _proxy.copyDate((Date) curVal) : curVal;
                return reproxy(newVal, field);
            case JavaTypes.OBJECT:
                if (_copy)
                    newVal = _proxy.copyCustom(curVal);
                return reproxy((newVal == null) ? curVal : newVal, field);
            case JavaTypes.PC:
            case JavaTypes.PC_UNTYPED:
                if (cascade) {
                    return detachInternal(curVal);
                }
                return curVal;
            default:
                return curVal;
            }
        }

        /**
         * Make sure all the values in the given array are detached.
         */
        private void detachArray(Object array, FieldMetaData fmd) {
            if (!fmd.getElement().isDeclaredTypePC())
                return;

            int len = Array.getLength(array);
            for (int i = 0; i < len; i++)
                Array.set(array, i, detachInternal(Array.get(array, i)));
        }

        /**
         * Make sure all the values in the given collection are detached.
         */
        private void detachCollection(Collection coll, Collection orig,
            FieldMetaData fmd) {
            // coll can be null if not copyable
            if (_copy && coll == null)
                throw new UserException(_loc.get("not-copyable", fmd));
            if (!fmd.getElement().isDeclaredTypePC())
                return;

            // unfortunately we have to clear the original and re-add to copy
            if (_copy)
                coll.clear();
            Object detached;
            for (Object o : orig) {
                detached = detachInternal(o);
                if (_copy)
                    coll.add(detached);
            }
        }

        /**
         * Make sure all the values in the given map are detached.
         */
        private void detachMap(Map map, Map orig, FieldMetaData fmd) {
            // map can be null if not copyable
            if (_copy && map == null)
                throw new UserException(_loc.get("not-copyable", fmd));
            boolean keyPC = fmd.getKey().isDeclaredTypePC();
            boolean valPC = fmd.getElement().isDeclaredTypePC();
            if (!keyPC && !valPC)
                return;

            // if we have to copy keys, just clear and re-add; otherwise
            // we can use the entry set to reset the values only
            Map.Entry entry;
            if (!_copy || keyPC) {
                if (_copy)
                    map.clear();
                Object key, val;
                for (Object o : orig.entrySet()) {
                    entry = (Map.Entry) o;
                    key = entry.getKey();
                    if (keyPC)
                        key = detachInternal(key);
                    val = entry.getValue();
                    if (valPC)
                        val = detachInternal(val);
                    if (_copy)
                        map.put(key, val);
                }
            } else {
                for (Object o : map.entrySet()) {
                    entry = (Map.Entry) o;
                    entry.setValue(detachInternal(entry.getValue()));
                }
			}
		}
	}
}
