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

import java.util.BitSet;

import org.apache.openjpa.kernel.AbstractPCData;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.PCData;
import org.apache.openjpa.kernel.PCDataImpl;
import org.apache.openjpa.kernel.PCState;
import org.apache.openjpa.kernel.StoreContext;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.ValueMetaData;

/**
 * Specialized {@link PCData} implementation for data caching. This
 * implementation is properly synchronized.
 *
 * @author Patrick Linskey
 */
public class DataCachePCDataImpl
    extends PCDataImpl
    implements DataCachePCData {

    private static final long serialVersionUID = 1L;
    private final long _exp;

    public DataCachePCDataImpl(Object oid, ClassMetaData meta) {
        this(oid, meta, DataCache.NAME_DEFAULT);
    }

    /**
     * Constructor.
     */
    public DataCachePCDataImpl(Object oid, ClassMetaData meta, String name) {
        super(oid, meta, name);

        int timeout = meta.getDataCacheTimeout();
        if (timeout > 0)
            _exp = System.currentTimeMillis() + timeout;
        else
            _exp = -1;
    }

    @Override
    public boolean isTimedOut() {
        return _exp != -1 && _exp < System.currentTimeMillis();
    }

    @Override
    public long getTimeOut() {
        return _exp;
    }

    @Override
    public synchronized Object getData(int index) {
        return super.getData(index);
    }

    @Override
    public synchronized void setData(int index, Object val) {
        super.setData(index, val);
    }

    @Override
    public synchronized void clearData(int index) {
        super.clearData(index);
    }

    @Override
    public synchronized Object getImplData() {
        return super.getImplData();
    }

    @Override
    public synchronized void setImplData(Object val) {
        super.setImplData(val);
    }

    @Override
    public synchronized Object getImplData(int index) {
        return super.getImplData(index);
    }

    @Override
    public synchronized void setImplData(int index, Object val) {
        super.setImplData(index, val);
    }

    @Override
    public synchronized Object getIntermediate(int index) {
        return super.getIntermediate(index);
    }

    @Override
    public synchronized void setIntermediate(int index, Object val) {
        super.setIntermediate(index, val);
    }

    @Override
    public synchronized boolean isLoaded(int index) {
        return super.isLoaded(index);
    }

    @Override
    public synchronized void setLoaded(int index, boolean loaded) {
        super.setLoaded(index, loaded);
    }

    @Override
    public synchronized Object getVersion() {
        return super.getVersion();
    }

    @Override
    public synchronized void setVersion(Object version) {
        super.setVersion(version);
    }

    @Override
    public synchronized void store(OpenJPAStateManager sm) {
        super.store(sm);
    }

    @Override
    public synchronized void store(OpenJPAStateManager sm, BitSet fields) {
        super.store(sm, fields);
    }

    /**
     * Store field-level information from the given state manager.
     * Special process of checking if the cached collection data is out of
     * order.
     */
    @Override
    protected void storeField(OpenJPAStateManager sm, FieldMetaData fmd) {
        if (fmd.getManagement() != FieldMetaData.MANAGE_PERSISTENT)
            return;
        int index = fmd.getIndex();

        // if the field is a collection and has "order by" set, don't cache
        // it if this store is coming from a create or update (i.e., only
        // enlist in cache if this is coming from a database read).
        if (fmd.getOrders().length > 0) {
            if (sm.getPCState() == PCState.PNEW)
                return;
            if (sm.getPCState() == PCState.PDIRTY) {
                clearData(index);
                return;
            }
        }

        super.storeField(sm, fmd);

        // If this field is used in "order by", we need to invalidate cache
        // for the collection that refer to this field.
        if ((sm.getPCState() == PCState.PDIRTY) && fmd.isUsedInOrderBy()) {
            clearInverseRelationCache(sm, fmd);
        }
    }

    /**
     * Check if this field is in use of "order by" by other field collections
     * in inverse relation. If it is, clear the other field cache because it
     * could be out of order.
     */
    protected void clearInverseRelationCache(OpenJPAStateManager sm, FieldMetaData fmd) {
        DataCache cache = sm.getMetaData().getDataCache();
        if (cache == null)
            return;
        ClassMetaData cmd = sm.getMetaData();
        FieldMetaData[] fields = cmd.getFields();
        for (int i = 0; i < fields.length; i++) {
            FieldMetaData[] inverses = fields[i].getInverseMetaDatas();
            if (inverses.length == 0)
                continue;
            for (FieldMetaData inverse : inverses) {
                if (inverse.getOrderDeclaration().indexOf(fmd.getName()) != -1) {
                    Object oid = sm.getContext().getObjectId(sm.fetch(i));
                    DataCachePCData data = cache.get(oid);
                    if (data instanceof DataCachePCDataImpl) {
                        ((DataCachePCDataImpl) data).clearData(inverse.getIndex());
                    }
                }
            }
        }
    }

    @Override
    protected Object toData(FieldMetaData fmd, Object val, StoreContext ctx) {
        // avoid caching large result set fields
        if (fmd.isLRS() || fmd.isStream())
            return NULL;
        return super.toData(fmd, val, ctx);
    }

    @Override
    protected Object toNestedData(ValueMetaData vmd, Object val,
        StoreContext ctx) {
        if (val == null)
            return null;

        // don't try to cache nested containers
        switch (vmd.getDeclaredTypeCode()) {
            case JavaTypes.COLLECTION:
            case JavaTypes.MAP:
            case JavaTypes.ARRAY:
                return NULL;
            default:
                return super.toNestedData(vmd, val, ctx);
        }
    }

    @Override
    public AbstractPCData newEmbeddedPCData(OpenJPAStateManager sm) {
        return new DataCachePCDataImpl(sm.getId(), sm.getMetaData(), getCache());
    }
}
