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

import org.apache.openjpa.datacache.DataCache;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;

/**
 * Default {@link PCData} implementation.
 *
 * @author Patrick Linskey
 * @author Abe White
 */
public class PCDataImpl
    extends AbstractPCData {

    private final Object _oid;
    private final Class<?> _type;
    private final String _cache;
    private final Object[] _data;
    private final BitSet _loaded;
    private Object _version = null;
    private Object _impl = null;
    private Object[] _fieldImpl = null;

    public PCDataImpl(Object oid, ClassMetaData meta) {
        this(oid, meta, DataCache.NAME_DEFAULT);
    }
    
    /**
     * Constructor.
     */
    public PCDataImpl(Object oid, ClassMetaData meta, String name) {
        _oid = oid;
        _type = meta.getDescribedType();
        _cache = name;

        int len = meta.getFields().length;
        _data = new Object[len];
        _loaded = new BitSet(len);
    }

    public Object getId() {
        return _oid;
    }

    public Class<?> getType() {
        return _type;
    }

    public BitSet getLoaded() {
        return _loaded;
    }

    public Object getData(int index) {
        // make sure index is actually loaded to avoid returning an
        // intermediate value
        return (_loaded.get(index)) ? _data[index] : null;
    }

    public void setData(int index, Object val) {
        _loaded.set(index);
        _data[index] = val;
    }

    public void clearData(int index) {
        _loaded.clear(index);
        _data[index] = null;
    }

    public Object getImplData() {
        return _impl;
    }

    public void setImplData(Object val) {
        _impl = val;
    }

    public Object getImplData(int index) {
        return (_fieldImpl != null) ? _fieldImpl[index] : null;
    }

    public void setImplData(int index, Object val) {
        if (val != null) {
            if (_fieldImpl == null)
                _fieldImpl = new Object[_data.length];
            _fieldImpl[index] = val;
        } else if (_fieldImpl != null)
            _fieldImpl[index] = null;
    }

    public Object getIntermediate(int index) {
        return (!_loaded.get(index)) ? _data[index] : null;
    }

    public void setIntermediate(int index, Object val) {
        _loaded.clear(index);
        _data[index] = val;
    }

    public boolean isLoaded(int index) {
        return _loaded.get(index);
    }

    public void setLoaded(int index, boolean loaded) {
        if (loaded)
            _loaded.set(index);
        else
            _loaded.clear(index);
    }

    public Object getVersion() {
        return _version;
    }

    public void setVersion(Object version) {
        _version = version;
    }

    public void load(OpenJPAStateManager sm, FetchConfiguration fetch,
        Object context) {
        loadVersion(sm);
        loadImplData(sm);

        FieldMetaData[] fmds = sm.getMetaData().getFields();
        ((StateManagerImpl)sm).setLoading(true);
        for (int i = 0; i < fmds.length; i++) {
            // load intermediate data for all unloaded fields and data for
            // fields in configured fetch groups
            if (!isLoaded(i))
                loadIntermediate(sm, fmds[i]);
            else if (!sm.getLoaded().get(i) && fetch.requiresFetch(fmds[i]) 
                != FetchConfiguration.FETCH_NONE)
                loadField(sm, fmds[i], fetch, context);
        }
    }

    public void load(OpenJPAStateManager sm, BitSet fields,
        FetchConfiguration fetch, Object context) {
        loadVersion(sm);
        loadImplData(sm);

        // attempt to load given fields
        int len = (fields == null) ? 0 : fields.length();
        FieldMetaData fmd;
        for (int i = 0; i < len; i++) {
            if (!fields.get(i))
                continue;

            fmd = sm.getMetaData().getField(i);
            boolean loading = false; 
            if(sm.getContext() != null && sm.getContext() instanceof BrokerImpl) { 
                loading = ((BrokerImpl) sm.getContext()).isLoading(sm.getObjectId());
            }
            if (!isLoaded(i) || loading) { // prevent reentrant calls. 
                loadIntermediate(sm, fmd);
            }
            else {
                loadField(sm, fmd, fetch, context);
                loadImplData(sm, fmd);
                fields.clear(i);
            }
        }
    }

    /**
     * Set version information into the given state manager.
     */
    protected void loadVersion(OpenJPAStateManager sm) {
        if (sm.getVersion() == null)
            sm.setVersion(getVersion());
    }

    /**
     * Set impl data information into the given state manager.
     */
    protected void loadImplData(OpenJPAStateManager sm) {
        Object impl = getImplData();
        if (sm.getImplData() == null && impl != null)
            sm.setImplData(impl, true);
    }

    /**
     * Set field-level information into the given state manager.
     */
    protected void loadField(OpenJPAStateManager sm, FieldMetaData fmd, FetchConfiguration fetch, Object context) {
        int index = fmd.getIndex();
        Object val = toField(sm, fmd, getData(index), fetch, context);
        
        // If val is null, make sure that we don't send back a null Embeddable or ElementCollection...perhaps others?
        // Probably should think about trying to shove this data back into the cache at this point so we don't
        // continually run through this code.
        if (val == null && fmd.isEmbeddedPC()) {
            val = sm.getContext().embed(null, null, sm, fmd).getManagedInstance();
        } else if (val == null && fmd.isElementCollection()) {
            val = sm.newProxy(index);
        }
        sm.storeField(index, val);
    }

    /**
     * Set field-level impl data into the given state manager.
     */
    protected void loadImplData(OpenJPAStateManager sm, FieldMetaData fmd) {
        int index = fmd.getIndex();
        Object impl = getImplData(index);
        if (impl != null)
            sm.setImplData(index, impl);
    }

    /**
     * Set intermediate information for the given field into the state manager.
     */
    protected void loadIntermediate(OpenJPAStateManager sm, FieldMetaData fmd) {
        int index = fmd.getIndex();
        Object inter = getIntermediate(index);
        if (inter != null && !sm.getLoaded().get(index))
            sm.setIntermediate(index, inter);
    }

    public void store(OpenJPAStateManager sm) {
        storeVersion(sm);
        storeImplData(sm);

        FieldMetaData[] fmds = sm.getMetaData().getFields();
        for (int i = 0; i < fmds.length; i++) {
            if (sm.getLoaded().get(i)) {
                storeField(sm, fmds[i]);
                storeImplData(sm, fmds[i], isLoaded(i));
            } else if (!isLoaded(i))
                storeIntermediate(sm, fmds[i]);
        }
    }

    public void store(OpenJPAStateManager sm, BitSet fields) {
        storeVersion(sm);
        storeImplData(sm);

        FieldMetaData[] fmds = sm.getMetaData().getFields();
        for (int i = 0; i < fmds.length; i++) {
            if (fields != null && fields.get(i)) {
                storeField(sm, fmds[i]);
                storeImplData(sm, fmds[i], isLoaded(i));
            } else if (!isLoaded(i))
                storeIntermediate(sm, fmds[i]);
        }
    }

    /**
     * Store version information from the given state manager.
     */
    protected void storeVersion(OpenJPAStateManager sm) {
        setVersion(sm.getVersion());
    }

    /**
     * Store impl data from the given state manager.
     */
    protected void storeImplData(OpenJPAStateManager sm) {
        if (sm.isImplDataCacheable())
            setImplData(sm.getImplData());
    }

    /**
     * Store field-level information from the given state manager.
     */
    protected void storeField(OpenJPAStateManager sm, FieldMetaData fmd) {
        if (fmd.getManagement() != fmd.MANAGE_PERSISTENT)
            return;

        int index = fmd.getIndex();
        OpenJPAStateManager dsm = null;
        if (sm.getPersistenceCapable().pcIsDetached()) {
            dsm = (DetachedStateManager) sm.getPersistenceCapable().
                pcGetStateManager();
            sm.getPersistenceCapable().pcReplaceStateManager(sm);
        }

        Object val = toData(fmd, sm.fetchField(index, false),
            sm.getContext());
        if (dsm != null)
            sm.getPersistenceCapable().pcReplaceStateManager(dsm);
        if (val != NULL)
            setData(index, val);
        else // unable to store field value; clear out any old values
            clearData(index);
    }

    /**
     * Store the intermediate field value for the given field.
     */
    protected void storeIntermediate(OpenJPAStateManager sm,
        FieldMetaData fmd) {
        int index = fmd.getIndex();
        Object val = sm.getIntermediate(index);
        if (val != null)
            setIntermediate(index, val);
    }

    /**
     * Store impl data for the given field.
     */
    protected void storeImplData(OpenJPAStateManager sm, FieldMetaData fmd,
        boolean fieldLoaded) {
        int index = fmd.getIndex();
        if (fieldLoaded) {
            // is there impl data to store?
            Object impl = sm.getImplData(index);
            if (impl != null && sm.isImplDataCacheable(index))
                setImplData(index, impl);
        } else
            setImplData(index, null);
    }

    /**
     * Return a new {@link PCData} implementation of the right type for
     * embedded instances. Returns a {@link PCDataImpl} by default.
     */
    public AbstractPCData newEmbeddedPCData(OpenJPAStateManager sm) {
        return new PCDataImpl(sm.getId (), sm.getMetaData (), _cache);
	}

    public String getCache() {
        return _cache;
    }
}
