/*
 * 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.io.Serializable;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.TimeZone;

import org.apache.openjpa.enhance.PersistenceCapable;
import org.apache.openjpa.lib.util.Localizer;
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.ChangeTracker;
import org.apache.openjpa.util.Exceptions;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.InvalidStateException;
import org.apache.openjpa.util.LRSProxy;
import org.apache.openjpa.util.MapChangeTracker;
import org.apache.openjpa.util.ObjectId;
import org.apache.openjpa.util.Proxies;
import org.apache.openjpa.util.Proxy;
import org.apache.openjpa.util.ProxyManager;
import org.apache.openjpa.util.UserException;

/**
 * FieldManager type used to hold onto a single field value and then
 * dispense it via the fetch methods. The manager can also perform actions
 * on the held field.
 */
class SingleFieldManager extends TransferFieldManager implements Serializable {
    private static final Localizer _loc = Localizer.forPackage(SingleFieldManager.class);

    private final StateManagerImpl _sm;
    private final BrokerImpl _broker;
    private final boolean _checkDbOnCascadePersist; 
    
    public SingleFieldManager(StateManagerImpl sm, BrokerImpl broker) {
        _sm = sm;
        _broker = broker;
        _checkDbOnCascadePersist =
            _broker.getConfiguration().getCompatibilityInstance().getCheckDatabaseForCascadePersistToDetachedEntity();
    }

    /**
     * Proxy the held field if needed. Return true if the field needs to
     * be replaced with the now-proxied instance.
     */
    public boolean proxy(boolean reset, boolean replaceNull) {
        FieldMetaData fmd = _sm.getMetaData().getField(field);
        Proxy proxy = null;
        boolean ret = false;
        switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.DATE:
                if (objval == null)
                    return false;
                proxy = checkProxy(fmd);
                if (proxy == null) {
                    proxy = (Proxy) _sm.newFieldProxy(field);
                    ((Date) proxy).setTime(((Date) objval).getTime());
                    if (proxy instanceof Timestamp && objval instanceof Timestamp)
                        ((Timestamp) proxy).setNanos(((Timestamp) objval).getNanos());
                    ret = true;
                }
                break;
            case JavaTypes.CALENDAR:
                if (objval == null)
                    return false;
                proxy = checkProxy(fmd);
                if (proxy == null) {
                    proxy = (Proxy) _sm.newFieldProxy(field);
                    ((Calendar) proxy).setTime(((Calendar) objval).getTime());
                    ret = true;
                } else {
                    Object init = fmd.getInitializer();
                    if (init != null && init instanceof TimeZone) {
                        ((Calendar) proxy).setTimeZone((TimeZone)init);
                    }
                }
                break;
            case JavaTypes.COLLECTION:
                if (objval == null && !replaceNull)
                    return false;
                proxy = checkProxy(fmd);
                if (proxy == null) {
                    proxy = (Proxy) _sm.newFieldProxy(field);
                    if (objval != null)
                        ((Collection) proxy).addAll((Collection) objval);
                    ret = true;
                }
                break;
            case JavaTypes.MAP:
                if (objval == null && !replaceNull)
                    return false;
                proxy = checkProxy(fmd);
                if (proxy == null) {
                    proxy = (Proxy) _sm.newFieldProxy(field);
                    if (objval != null)
                        ((Map) proxy).putAll((Map) objval);
                    ret = true;
                }
                break;
            case JavaTypes.OBJECT:
                if (objval == null)
                    return false;
                proxy = checkProxy(fmd);
                if (proxy == null) {
                    proxy = getProxyManager().newCustomProxy(objval,
                        _sm.getBroker().getConfiguration().
                        getCompatibilityInstance().getAutoOff());
                    ret = proxy != null;
                }
                break;
        }

        if (proxy != null) {
            proxy.setOwner(_sm, field);
            ChangeTracker tracker = proxy.getChangeTracker();
            if (reset && tracker != null) {
                if (fmd.getDeclaredTypeCode() == JavaTypes.MAP) {
                    // track values if key is derived from value, else keys
                    boolean keys = fmd.getKey().getValueMappedBy() == null;
                    ((MapChangeTracker) tracker).setTrackKeys(keys);
                }
                tracker.startTracking();
            }
            objval = proxy;
        }
        return ret;
    }

    /**
     * If the current field is a usable proxy and it should be a proxy, return it; else return null.
     * 
     * This method will skim out Calendar instances that were proxied before we knew if they need to be proxied.
     */
    private Proxy checkProxy(FieldMetaData fmd) {
        if (!(objval instanceof Proxy))
            return null;

        Proxy proxy = (Proxy) objval;
        if (proxy.getOwner() == null || Proxies.isOwner(proxy, _sm, field)) {
            if (fmd.getProxyType().isAssignableFrom(proxy.getClass()) 
                    || (fmd.isLRS() && (objval instanceof LRSProxy))) {
                return proxy;
            }
        }
        return null;
    }

    /**
     * Unproxies the current field if needed.
     */
    public void unproxy() {
        if (objval == null)
            return;

        FieldMetaData fmd = _sm.getMetaData().getField(field);
        switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.COLLECTION:
            case JavaTypes.MAP:
            case JavaTypes.DATE:
            case JavaTypes.OBJECT:
                if (objval instanceof Proxy) {
                    Proxy proxy = (Proxy) objval;
                    proxy.setOwner(null, -1);
                    if (proxy.getChangeTracker() != null)
                        proxy.getChangeTracker().stopTracking();
                }
        }
    }

    /**
     * Release the currently embedded field (make it transient).
     */
    public void releaseEmbedded() {
        if (objval == null)
            return;

        FieldMetaData fmd = _sm.getMetaData().getField(field);
        switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.PC:
                if (fmd.isEmbeddedPC())
                    releaseEmbedded(fmd, objval);
                break;
            case JavaTypes.ARRAY:
                if (fmd.getElement().isEmbeddedPC())
                    releaseEmbedded(fmd.getElement(), (Object[]) objval);
                break;
            case JavaTypes.COLLECTION:
                if (fmd.getElement().isEmbeddedPC())
                    releaseEmbedded(fmd.getElement(), (Collection) objval);
                break;
            case JavaTypes.MAP:
                if (fmd.getKey().isEmbeddedPC())
                    releaseEmbedded(fmd.getKey(), ((Map) objval).keySet());
                if (fmd.getElement().isEmbeddedPC())
                    releaseEmbedded(fmd.getElement(), ((Map) objval).values());
                break;
        }
    }

    /**
     * Release the given embedded objects.
     */
    private void releaseEmbedded(ValueMetaData vmd, Object[] objs) {
        for (int i = 0; i < objs.length; i++)
            releaseEmbedded(vmd, objs[i]);
    }

    /**
     * Release the given embedded objects.
     */
    private void releaseEmbedded(ValueMetaData vmd, Collection objs) {
        for (Iterator itr = objs.iterator(); itr.hasNext();)
            releaseEmbedded(vmd, itr.next());
    }

    /**
     * Release the given embedd object.
     */
    private void releaseEmbedded(ValueMetaData vmd, Object obj) {
        if (obj == null)
            return;

        StateManagerImpl sm = _broker.getStateManagerImpl(obj, false);
        if (sm != null && sm.getOwner() == _sm
            && sm.getOwnerIndex() == vmd.getFieldMetaData().getIndex())
            sm.release(true);
    }

    /**
     * Persist the stored field safely, preventing infinite recursion using
     * the given set of already-persisted objects. This method is only called
     * for fields that we know have cascade-immediate settings.
     */
    public void persist(OpCallbacks call) {
        if (objval == null)
            return;

        FieldMetaData fmd = _sm.getMetaData().getField(field);
        switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.PC:
            case JavaTypes.PC_UNTYPED:
                if (!_broker.isDetachedNew() && _broker.isDetached(objval, _checkDbOnCascadePersist))
                    return; // allow but ignore
                _broker.persist(objval, true, call);
                break;
            case JavaTypes.ARRAY:
                _broker.persistAll(Arrays.asList((Object[]) objval), true, 
                    call);
                break;
            case JavaTypes.COLLECTION:
                _broker.persistAll((Collection) objval, true, call);
                break;
            case JavaTypes.MAP:
                if (fmd.getKey().getCascadePersist()
                    == ValueMetaData.CASCADE_IMMEDIATE)
                    _broker.persistAll(((Map) objval).keySet(), true, call);
                if (fmd.getElement().getCascadePersist()
                    == ValueMetaData.CASCADE_IMMEDIATE)
                    _broker.persistAll(((Map) objval).values(), true, call);
                break;
        }
    }

    /**
     * Delete and/or dereference field values.
     */
    public void delete(OpCallbacks call) {
        delete(true, call);
    }

    /**
     * Dereference field values.
     */
    public void dereferenceDependent() {
        delete(false, null, true);
    }

    private void delete(boolean immediate, OpCallbacks call) {
        delete(immediate, call, false);
    }
        
    /**
     * Delete or dereference the stored field as necessary.
     */
    private void delete(boolean immediate, OpCallbacks call, boolean deref) {
        if (objval == null)
            return;

        FieldMetaData fmd = _sm.getMetaData().getField(field);
        if (fmd.getCascadeDelete() != ValueMetaData.CASCADE_NONE) {
            // immediate cascade works on field value; dependent deref
            // works on external value
            if ((immediate || fmd.isEmbeddedPC())
                && fmd.getCascadeDelete() == ValueMetaData.CASCADE_IMMEDIATE) {
                if (fmd.isEmbeddedPC() && deref) {
                    StateManagerImpl sm = _broker.getStateManagerImpl(objval, false);
                    if (sm != null) 
                        dereferenceEmbedDependent(sm);
                }
                delete(fmd, objval, call);
            }
            else if (fmd.getCascadeDelete() == ValueMetaData.CASCADE_AUTO)
                dereferenceDependent(fmd.getExternalValue(objval, _broker));
            return;
        }

        Object external = null;
        ValueMetaData vmd = fmd.getKey();
        if ((immediate || vmd.isEmbeddedPC())
            && vmd.getCascadeDelete() == ValueMetaData.CASCADE_IMMEDIATE)
            delete(vmd, ((Map) objval).keySet(), call);
        else if (vmd.getCascadeDelete() == ValueMetaData.CASCADE_AUTO) {
            external = fmd.getExternalValue(objval, _broker);
            if (external == null)
                return;
            dereferenceDependent(((Map) external).keySet());
        }

        vmd = fmd.getElement();
        if ((immediate || vmd.isEmbeddedPC())
            && vmd.getCascadeDelete() == ValueMetaData.CASCADE_IMMEDIATE) {
            switch (fmd.getDeclaredTypeCode()) {
                case JavaTypes.COLLECTION:
                    delete(vmd, (Collection) objval, call);
                    break;
                case JavaTypes.ARRAY:
                    delete(vmd, (Object[]) objval, call);
                    break;
                case JavaTypes.MAP:
                    delete(vmd, ((Map) objval).values(), call);
                    break;
            }
        } else if (vmd.getCascadeDelete() == ValueMetaData.CASCADE_AUTO) {
            if (external == null) {
                external = fmd.getExternalValue(objval, _broker);
                if (external == null)
                    return;
            }

            switch (fmd.getTypeCode()) {
                case JavaTypes.COLLECTION:
                    dereferenceDependent((Collection) external);
                    break;
                case JavaTypes.ARRAY:
                    dereferenceDependent((Object[]) external);
                    break;
                case JavaTypes.MAP:
                    dereferenceDependent(((Map) external).values());
                    break;
            }
        }
    }

    /**
     * Delete the objects in the given value.
     */
    private void delete(ValueMetaData vmd, Object[] objs, OpCallbacks call) {
        for (int i = 0; i < objs.length; i++)
            delete(vmd, objs[i], call);
    }

    /**
     * Delete the objects embedded in the given value.
     */
    private void delete(ValueMetaData vmd, Collection objs, OpCallbacks call) {
        for (Iterator itr = objs.iterator(); itr.hasNext();)
            delete(vmd, itr.next(), call);
    }

    /**
     * Delete an object embedded in the given value.
     */
    void delete(ValueMetaData vmd, Object obj, OpCallbacks call) {
        if (obj == null)
            return;

        // delete if unknowned or this isn't an embedded field or if owned by us
        StateManagerImpl sm = _broker.getStateManagerImpl(obj, false);
        if (sm != null && (sm.getOwner() == null || !vmd.isEmbeddedPC()
            || (sm.getOwner() == _sm
            && sm.getOwnerIndex() == vmd.getFieldMetaData().getIndex())))
            _broker.delete(sm.getManagedInstance(), sm, call);
    }

    /**
     * Dereference all valid persistent objects in the given collection.
     */
    private void dereferenceDependent(Object[] objs) {
        for (int i = 0; i < objs.length; i++)
            dereferenceDependent(objs[i]);
    }

    /**
     * Dereference all valid persistent objects in the given collection.
     */
    private void dereferenceDependent(Collection objs) {
        for (Iterator itr = objs.iterator(); itr.hasNext();)
            dereferenceDependent(itr.next());
    }

    /**
     * Dereference the given object.
     */
    void dereferenceDependent(Object obj) {
        if (obj == null)
            return;
        StateManagerImpl sm = _broker.getStateManagerImpl(obj, false);
        if (sm != null)
            sm.setDereferencedDependent(true, true);
    }
    
    void dereferenceEmbedDependent(StateManagerImpl sm) {
    	sm.setDereferencedEmbedDependent(true);
    }
    
    /**
     * Recursively invoke the broker to gather cascade-refresh objects in
     * the current field into the given set. This method is only called
     * for fields that we know have cascade-refresh settings.
     */
    public void gatherCascadeRefresh(OpCallbacks call) {
        if (objval == null)
            return;

        FieldMetaData fmd = _sm.getMetaData().getField(field);
        switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.PC:
            case JavaTypes.PC_UNTYPED:
                _broker.gatherCascadeRefresh(objval, call);
                break;
            case JavaTypes.ARRAY:
                gatherCascadeRefresh((Object[]) objval, call);
                break;
            case JavaTypes.COLLECTION:
                gatherCascadeRefresh((Collection) objval, call);
                break;
            case JavaTypes.MAP:
                if (fmd.getKey().getCascadeRefresh()
                    == ValueMetaData.CASCADE_IMMEDIATE)
                    gatherCascadeRefresh(((Map) objval).keySet(), call);
                if (fmd.getElement().getCascadeRefresh()
                    == ValueMetaData.CASCADE_IMMEDIATE)
                    gatherCascadeRefresh(((Map) objval).values(), call);
                break;
        }
    }

    /**
     * Gather each element.
     */
    private void gatherCascadeRefresh(Object[] arr, OpCallbacks call) {
        for (int i = 0; i < arr.length; i++)
            _broker.gatherCascadeRefresh(arr[i], call);
    }

    /**
     * Gather each element.
     */
    private void gatherCascadeRefresh(Collection coll, OpCallbacks call) {
        for (Iterator itr = coll.iterator(); itr.hasNext();)
            _broker.gatherCascadeRefresh(itr.next(), call);
    }

    /**
     * Perform pre-flush tasks on the current field. This includes checking
     * for nulls, persisting pcs, embedding embedded fields, and ref'ing
     * pc fields. Return true if the field needs to be replaced with the
     * new value.
     */
    public boolean preFlush(boolean logical, OpCallbacks call) {
        // only care about object fields
        FieldMetaData fmd = _sm.getMetaData().getField(field);
        if (fmd.getDeclaredTypeCode() < JavaTypes.OBJECT)
            return false;

        // manage inverses
        InverseManager manager = _broker.getInverseManager();
        if (manager != null)
            manager.correctRelations(_sm, fmd, objval);
        
        // perform pers-by-reach and dependent refs
        return preFlush(fmd, logical, call);
    }

    /**
     * Return true if the last-provided field has a default value.
     */
    public boolean isDefaultValue() {
        return dblval == 0 && longval == 0
            && (objval == null || "".equals(objval));
    }

    /**
     * Write the stored field or its default value to the given stream.
     */
    public void serialize(ObjectOutput out, boolean def)
        throws IOException {
        FieldMetaData fmd = _sm.getMetaData().getField(field);
        switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.BOOLEAN:
                out.writeBoolean(!def && longval == 1);
                break;
            case JavaTypes.BYTE:
                out.writeByte((def) ? (byte) 0 : (byte) longval);
                break;
            case JavaTypes.CHAR:
                out.writeChar((def) ? (char) 0 : (char) longval);
                break;
            case JavaTypes.DOUBLE:
                out.writeDouble((def) ? 0D : dblval);
                break;
            case JavaTypes.FLOAT:
                out.writeFloat((def) ? 0F : (float) dblval);
                break;
            case JavaTypes.INT:
                out.writeInt((def) ? 0 : (int) longval);
                break;
            case JavaTypes.LONG:
                out.writeLong((def) ? 0L : longval);
                break;
            case JavaTypes.SHORT:
                out.writeShort((def) ? (short) 0 : (short) longval);
                break;
            default:
                out.writeObject((def) ? null : objval);
        }
    }

    /**
     * Helper method to perform pre flush actions on the current object.
     */
    private boolean preFlush(FieldMetaData fmd, boolean logical, OpCallbacks call) {
        // check for illegal nulls
        if (objval == null) {
            // If we have an AUTOASSIGN strategy that means that we have a field that is GenerationType.IDENTITY so
            // skip checking to see if the value is null as it will get assigned later in flush processing. 
            if (fmd.getValueStrategy() != ValueStrategies.AUTOASSIGN) {
                if (fmd.getNullValue() == FieldMetaData.NULL_EXCEPTION || fmd.getDeclaredTypeCode() == JavaTypes.OID)
                    throw new InvalidStateException(_loc.get("null-value", fmd.getName(), _sm.getManagedInstance()))
                        .setFatal(true);
            }
            return false;
        }

        // nothing else to do for non-persistent
        if (fmd.getManagement() != FieldMetaData.MANAGE_PERSISTENT)
            return false;

        // don't allow managed objectid field value
        if (fmd.getDeclaredTypeCode() == JavaTypes.OID) {
            _sm.assertNotManagedObjectId(objval);
            if (_sm.getObjectId() != null
                && !objval.equals(((ObjectId) _sm.getObjectId()).getId()))
                throw new InvalidStateException(_loc.get("changed-oid",
                    _sm.getObjectId(), objval,
                    Exceptions.toString(_sm.getManagedInstance()))).
                    setFatal(true);
        }

        // check for pcs in field value
        if (preFlush(fmd, fmd.getDeclaredTypeCode(),
            fmd.getKey().getDeclaredTypeCode(),
            fmd.getElement().getDeclaredTypeCode(), false, logical, call))
            return true;

        // also check for pcs in externalized values
        if (fmd.isExternalized())
            preFlush(fmd, fmd.getTypeCode(), fmd.getKey().getTypeCode(),
                fmd.getElement().getTypeCode(), true, logical, call);
        return false;
    }

    /**
     * Make new objects persistent and ref other objects so referenced
     * dependent objects won't be deleted.
     */
    private boolean preFlush(FieldMetaData fmd, int type, int keyType,
        int elemType, boolean external, boolean logical, OpCallbacks call) {
        Object val = objval;
        if (val == null)
            return false;

        boolean copy = false;
        switch (type) {
            case JavaTypes.PC:
                if (fmd.isEmbeddedPC()) {
                    objval = embed(fmd, val);
                    copy = true;
                } else {
                    if (external)
                        val = fmd.getExternalValue(val, _broker);
                    if (val != null)
                        preFlushPC(fmd, val, logical, call);
                }
                break;
            case JavaTypes.PC_UNTYPED:
                if (external)
                    val = fmd.getExternalValue(val, _broker);
                if (val != null)
                    preFlushPC(fmd, val, logical, call);
                break;
            case JavaTypes.ARRAY:
                if (fmd.getElement().isEmbeddedPC())
                    embed(fmd.getElement(), (Object[]) val);
                else if (elemType == JavaTypes.PC
                    || elemType == JavaTypes.PC_UNTYPED) {
                    if (external)
                        val = fmd.getExternalValue(val, _broker);
                    if (val != null)
                        preFlushPCs(fmd.getElement(), (Object[]) val, logical, 
                            call);
                }
                break;
            case JavaTypes.COLLECTION:
                if (fmd.getElement().isEmbeddedPC()) {
                    objval = embed(fmd.getElement(), (Collection) val);
                    copy = true;
                } else if (elemType == JavaTypes.PC
                    || elemType == JavaTypes.PC_UNTYPED) {
                    boolean flushed = false;
                    if (external)
                        val = fmd.getExternalValue(val, _broker);
                    else if (val instanceof Proxy) {
                        // shortcut change trackers; also ensures we don't 
                        // iterate lrs fields
                        ChangeTracker ct = ((Proxy) val).getChangeTracker();
                        if (ct != null && ct.isTracking()) {
                            preFlushPCs(fmd.getElement(), ct.getAdded(), 
                                logical, call);
                            preFlushPCs(fmd.getElement(), ct.getChanged(),
                                logical, call);
                            flushed = true;
                        }
                    }
                    if (!flushed && val != null)
                        preFlushPCs(fmd.getElement(), (Collection) val, logical,
                            call);
                }
                break;
            case JavaTypes.MAP:
                boolean keyEmbed = fmd.getKey().isEmbeddedPC();
                boolean valEmbed = fmd.getElement().isEmbeddedPC();
                if (keyEmbed || valEmbed) {
                    objval = embed(fmd, (Map) val, keyEmbed, valEmbed);
                    copy = keyEmbed;
                }

                if (!keyEmbed && (keyType == JavaTypes.PC
                    || keyType == JavaTypes.PC_UNTYPED)) {
                    boolean flushed = false;
                    if (external) {
                        val = fmd.getExternalValue(val, _broker);
                        external = false;
                    } else if (val instanceof Proxy) {
                        // shortcut change trackers; also ensures we don't 
                        // iterate lrs fields
                        MapChangeTracker ct = (MapChangeTracker) ((Proxy) val).
                            getChangeTracker();
                        if (ct != null && ct.isTracking() && ct.getTrackKeys())
                        {
                            preFlushPCs(fmd.getKey(), ct.getAdded(), logical,
                                call);
                            preFlushPCs(fmd.getKey(), ct.getChanged(), logical,
                                call);
                            flushed = true;
                        }
                    }
                    if (!flushed && val != null)
                        preFlushPCs(fmd.getKey(), ((Map) val).keySet(), logical,
                            call);
                }

                if (!valEmbed && (elemType == JavaTypes.PC
                    || elemType == JavaTypes.PC_UNTYPED)) {
                    boolean flushed = false;
                    if (external)
                        val = fmd.getExternalValue(val, _broker);
                    else if (val instanceof Proxy) {
                        // shortcut change trackers; also ensures we don't 
                        // iterate lrs fields
                        MapChangeTracker ct = (MapChangeTracker) ((Proxy) val).
                            getChangeTracker();
                        if (ct != null && ct.isTracking()) {
                            if (ct.getTrackKeys()) {
                                preFlushPCs(fmd.getElement(), ct.getAdded(),
                                    (Map) val, logical, call);
                                preFlushPCs(fmd.getElement(), ct.getChanged(),
                                    (Map) val, logical, call);
                            } else {
                                preFlushPCs(fmd.getElement(), ct.getAdded(),
                                    logical, call);
                                preFlushPCs(fmd.getElement(), ct.getChanged(),
                                    logical, call);
                            }
                            flushed = true;
                        }
                    }
                    if (!flushed && val != null)
                        preFlushPCs(fmd.getElement(), ((Map) val).values(),
                            logical, call);
                }
                break;
        }
        return copy;
    }

    /**
     * Make new objects persistent and ref all valid persistent objects for
     * the given keys.
     */
    private void preFlushPCs(ValueMetaData vmd, Collection keys, Map map,
        boolean logical, OpCallbacks call) {
        for (Iterator itr = keys.iterator(); itr.hasNext();)
            preFlushPC(vmd, map.get(itr.next()), logical, call);
    }

    /**
     * Make new objects persistent and ref all valid persistent objects in
     * the given array.
     */
    private void preFlushPCs(ValueMetaData vmd, Object[] objs,
        boolean logical, OpCallbacks call) {
        for (int i = 0; i < objs.length; i++)
            preFlushPC(vmd, objs[i], logical, call);
    }

    /**
     * Make new objects persistent and ref all valid persistent objects in
     * the given collection.
     */
    private void preFlushPCs(ValueMetaData vmd, Collection objs,
        boolean logical, OpCallbacks call) {
        for (Iterator itr = objs.iterator(); itr.hasNext();)
            preFlushPC(vmd, itr.next(), logical, call);
    }

    /**
     * Perform pre flush operations on the given object.
     */
    private void preFlushPC(ValueMetaData vmd, Object obj, boolean logical, OpCallbacks call) {
        if (obj == null)
            return;

        OpenJPAStateManager sm;        

        if (vmd.getCascadePersist() == ValueMetaData.CASCADE_NONE) {
            if (!_broker.isDetachedNew() && _broker.isDetached(obj, _checkDbOnCascadePersist)) {
                return; // allow but ignore
            }

            sm = _broker.getStateManager(obj);
            if (sm == null || !sm.isPersistent()) {
                if (((StoreContext)_broker).getAllowReferenceToSiblingContext() 
                 && ImplHelper.isManageable(obj) 
                 && ((PersistenceCapable)obj).pcGetStateManager() != null) {
                    return; 
                } else {
                    throw new InvalidStateException(_loc.get("cant-cascade-persist", 
                            vmd.toString(), Exceptions.toString(obj),
                            sm == null ? " unmanaged" : sm.getPCState().getClass().getSimpleName()))
                    .setFailedObject(obj);
                }
            }
        } else {
            if (vmd.getCascadePersist() == ValueMetaData.CASCADE_IMMEDIATE) {
                if (!_broker.isDetachedNew() && _broker.isDetached(obj, _checkDbOnCascadePersist)) {
                    return; // allow but ignore
                }
            }        	
            sm = _broker.getStateManager(obj);
            if (sm == null || !sm.isProvisional()) { 
                sm = _broker.persist(obj, null, true, call);
                // ensure generated IDs get assigned properly
                if (!logical)
                    ((StateManagerImpl)sm).assignObjectId(false, true);
                
                // Call preFetch on this and any related persistent fields.
                // This will ensure IDs get assigned to those that need them.
                if (_broker.isFlushing()) {
                    ((StateManagerImpl)sm).preFlush(logical, call);
                }
            }
        }

        if (sm != null) {
            // if deleted and not managed inverse, die
            if (sm.isDeleted() && (_broker.getInverseManager() == null
                || vmd.getFieldMetaData().getInverseMetaDatas().length == 0))
                throw new UserException(_loc.get("ref-to-deleted",
                    Exceptions.toString(obj), vmd,
                    Exceptions.toString(_sm.getManagedInstance()))).
                    setFailedObject(obj);

            StateManagerImpl smimpl = (StateManagerImpl) sm;
            smimpl.nonprovisional(logical, call);
            smimpl.setDereferencedDependent(false, true);
        }
    }

    /**
     * Make all elements of the given array embedded.
     */
    private void embed(ValueMetaData vmd, Object[] arr) {
        for (int i = 0; i < arr.length; i++)
            arr[i] = embed(vmd, arr[i]);
    }

    /**
     * Create a copy of the given collection containing embedded elements.
     */
    private Collection embed(ValueMetaData vmd, Collection orig) {
        // we have to copy to get a collection of the right type and size,
        // though we immediately clear it
        if (orig == null)
            throw new UserException(_loc.get("not-copyable",
                vmd.getFieldMetaData()));
        Collection coll = null;
        try {
            coll = getProxyManager().copyCollection(orig);
        } catch (Exception e) {
            coll = (Collection) _sm.newFieldProxy(vmd.getFieldMetaData().getIndex());
        }
        coll.clear();
        for (Iterator itr = orig.iterator(); itr.hasNext();)
            coll.add(embed(vmd, itr.next()));
        return coll;
    }

    /**
     * Embed the elements of the given map.
     */
    private Map embed(FieldMetaData fmd, Map orig, boolean keyEmbed,
        boolean valEmbed) {
        Map map;
        Map.Entry entry;

        // if we have to replace keys, we need to copy the map; otherwise
        // we can mutate the values directly
        if (keyEmbed) {
            if (orig == null)
                throw new UserException(_loc.get("not-copyable", fmd));
            // we have to copy to get a collection of the right type and size,
            // though we immediately clear it
            try {
                map = getProxyManager().copyMap(orig);
            } catch (Exception e) {
                map = (Map) _sm.newFieldProxy(fmd.getIndex());
            }
            map.clear();
            Object key, val;
            for (Iterator itr = orig.entrySet().iterator(); itr.hasNext();) {
                entry = (Map.Entry) itr.next();
                key = embed(fmd.getKey(), entry.getKey());
                val = entry.getValue();
                if (valEmbed)
                    val = embed(fmd.getElement(), val);
                map.put(key, val);
            }
        } else {
            map = orig;
            for (Iterator itr = map.entrySet().iterator(); itr.hasNext();) {
                entry = (Map.Entry) itr.next();
                entry.setValue(embed(fmd.getElement(),
                    entry.getValue()));
            }
        }
        return map;
    }

    /**
     * Make the given object embedded.
     */
    private Object embed(ValueMetaData vmd, Object obj) {
        if (obj == null)
            return null;
        return _broker.embed(obj, null, _sm, vmd).getManagedInstance();
    }

    /**
     * Return the proxy manager.
     */
    private ProxyManager getProxyManager ()
	{
		return _broker.getConfiguration ().getProxyManagerInstance ();
	}
}
