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

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.datacache.DataCache;
import org.apache.openjpa.datacache.DataCacheManager;
import org.apache.openjpa.lib.conf.Configurable;
import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.lib.log.Log;
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.util.InvalidStateException;

/**
 * Class which manages inverse relations before flushing
 * to the datastore. Ensures that inverse fields are set.
 *  Currently limited to managing PC and Collection-type relations.
 *
 * @author Steve Kim
 */
public class InverseManager implements Configurable {

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

    protected static final Object NONE = new Object();

    protected DataCacheManager _mgr;

    /**
     * Constant representing the {@link #ACTION_MANAGE} action
     */
    public static final int ACTION_MANAGE = 0;

    /**
     * Constant representing the {@link #ACTION_WARN} action
     */
    public static final int ACTION_WARN = 1;

    /**
     * Constant representing the {@link #ACTION_EXCEPTION} action
     */
    public static final int ACTION_EXCEPTION = 2;

    private boolean _manageLRS = false;
    private int _action = ACTION_MANAGE;
    private Log _log;

    /**
     * Return whether to manage LRS fields.
     */
    public boolean getManageLRS() {
        return _manageLRS;
    }

    /**
     * Set whether to false LRS relations. Defaults to false.
     */
    public void setManageLRS(boolean manage) {
        _manageLRS = manage;
    }

    /**
     * Return the action constant to use during relationship checking.
     * Defaults to {@link #ACTION_MANAGE}.
     */
    public int getAction() {
        return _action;
    }

    /**
     * Set the action constant to use during relationship checking.
     * Defaults to {@link #ACTION_MANAGE}.
     */
    public void setAction(int action) {
        _action = action;
    }

    /**
     * Set the action string to use during relationship checking.
     * Options include <code>manage, exception, warn</code>.
     * This method is primarily for string-based automated configuration.
     */
    public void setAction(String action) {
        if ("exception".equals(action))
            _action = ACTION_EXCEPTION;
        else if ("warn".equals(action))
            _action = ACTION_WARN;
        else if ("manage".equals(action))
            _action = ACTION_MANAGE;
        else
            throw new IllegalArgumentException(action);
    }

    @Override
    public void startConfiguration() {
    }

    @Override
    public void endConfiguration() {
    }

    @Override
    public void setConfiguration(Configuration conf) {
        _log = conf.getLog(OpenJPAConfiguration.LOG_RUNTIME);
        _mgr = ((OpenJPAConfiguration)conf).getDataCacheManagerInstance();
    }

    /**
     * Correct relations from the given dirty field to inverse instances.
     * Field <code>fmd</code> of the instance managed by <code>sm</code> has
     * value <code>value</code>. Ensure that all inverses relations from
     * <code>value</code> are consistent with this.
     */
    public void correctRelations(OpenJPAStateManager sm, FieldMetaData fmd,
        Object value) {
        if (fmd.getDeclaredTypeCode() != JavaTypes.PC &&
            ((fmd.getDeclaredTypeCode() != JavaTypes.COLLECTION  &&
              fmd.getDeclaredTypeCode() != JavaTypes.MAP) ||
                fmd.getElement().getDeclaredTypeCode() != JavaTypes.PC))
            return;

        // ignore LRS fields
        if (!getManageLRS() && fmd.isLRS())
            return;

        FieldMetaData[] inverses = fmd.getInverseMetaDatas();
        if (inverses.length == 0)
            return;

        // clear any restorable relations
        clearInverseRelations(sm, fmd, inverses, value);

        if (value != null) {
            StoreContext ctx = sm.getContext();
            switch (fmd.getDeclaredTypeCode()) {
                case JavaTypes.PC:
                    createInverseRelations(ctx, sm.getManagedInstance(),
                        value, fmd, inverses);
                    break;
                case JavaTypes.COLLECTION:
                    for (Object o : (Collection) value)
                        createInverseRelations(ctx, sm.getManagedInstance(),
                                o, fmd, inverses);
                    break;
            }
        }
    }

    /**
     * Create the inverse relations for all the given inverse fields.
     * A relation exists from <code>fromRef</code> to <code>toRef</code>; this
     * method creates the inverses.
     */
    protected void createInverseRelations(StoreContext ctx,
        Object fromRef, Object toRef, FieldMetaData fmd,
        FieldMetaData[] inverses) {
        OpenJPAStateManager other = ctx.getStateManager(toRef);
        if (other == null || other.isDeleted())
            return;

        boolean owned;
        for (FieldMetaData invers : inverses) {
            if (!getManageLRS() && invers.isLRS())
                continue;

            // if this is the owned side of the relation and has not yet been
            // loaded, no point in setting it now, cause it'll have the correct
            // value the next time it is loaded after the flush
            owned = fmd == invers.getMappedByMetaData()
                    && _action == ACTION_MANAGE
                    && !isLoaded(other, invers.getIndex());

            switch (invers.getDeclaredTypeCode()) {
                case JavaTypes.PC:
                    if (!owned || invers.getCascadeDelete()
                            == ValueMetaData.CASCADE_AUTO)
                        storeField(other, invers, NONE, fromRef);
                    break;
                case JavaTypes.COLLECTION:
                    if (!owned || invers.getElement().getCascadeDelete()
                            == ValueMetaData.CASCADE_AUTO)
                        addToCollection(other, invers, fromRef);
                    break;
            }
        }
    }

    /**
     * Return whether the given field is loaded for the given instance.
     */
    private boolean isLoaded(OpenJPAStateManager sm, int field) {
        if (sm.getLoaded().get(field))
            return true;

        // if the field isn't loaded in the state manager, it still might be
        // loaded in the data cache, in which case we still have to correct
        // it to keep the cache in sync
        DataCache cache = _mgr.selectCache(sm);
        if (cache == null)
            return false;

        // can't retrieve an embedded object directly, so always assume the
        // field is loaded and needs to be corrected
        if (sm.isEmbedded())
            return true;

        PCData pc = cache.get(sm.getObjectId());
        if (pc == null)
            return false;
        return pc.isLoaded(field);
    }

    /**
     * Remove all relations between the initial value of <code>fmd</code> for
     * the instance managed by <code>sm</code> and its inverses. Relations
     * shared with <code>newValue</code> can be left intact.
     */
    protected void clearInverseRelations(OpenJPAStateManager sm,
        FieldMetaData fmd, FieldMetaData[] inverses, Object newValue) {
        // don't bother clearing unflushed new instances
        if (sm.isNew() && !sm.getFlushed().get(fmd.getIndex()))
            return;
        if (fmd.getDeclaredTypeCode() == JavaTypes.PC) {
            Object initial = sm.fetchInitialField(fmd.getIndex());
            clearInverseRelations(sm, initial, fmd, inverses);
        } else {
            Object obj = sm.fetchInitialField(fmd.getIndex());
            Collection initial = null;
            if (obj instanceof Collection)
                initial = (Collection) obj;
            else if (obj instanceof Map)
                initial = ((Map)obj).values();

            if (initial == null)
                return;

            // clear all relations not also in the new value
            Collection coll = null;
            if (newValue instanceof Collection)
                coll = (Collection) newValue;
            else if (newValue instanceof Map)
                coll = ((Map)newValue).values();
            Object elem;
            for (Object o : initial) {
                elem = o;
                if (coll == null || !coll.contains(elem))
                    clearInverseRelations(sm, elem, fmd, inverses);
            }
        }
    }

    /**
     * Clear all inverse the relations from <code>val</code> to the instance
     * managed by <code>sm</code>.
     */
    protected void clearInverseRelations(OpenJPAStateManager sm, Object val,
        FieldMetaData fmd, FieldMetaData[] inverses) {
        if (val == null)
            return;
        OpenJPAStateManager other = sm.getContext().getStateManager(val);
        if (other == null || other.isDeleted())
            return;

        boolean owned;
        for (FieldMetaData invers : inverses) {
            if (!getManageLRS() && invers.isLRS())
                continue;

            // if this is the owned side of the relation and has not yet been
            // loaded, no point in setting it now, cause it'll have the correct
            // value the next time it is loaded after the flush
            owned = fmd == invers.getMappedByMetaData()
                    && _action == ACTION_MANAGE
                    && !isLoaded(other, invers.getIndex());

            switch (invers.getDeclaredTypeCode()) {
                case JavaTypes.PC:
                    if (!owned || invers.getCascadeDelete()
                            == ValueMetaData.CASCADE_AUTO)
                        storeNull(other, invers, sm.getManagedInstance());
                    break;
                case JavaTypes.COLLECTION:
                    if (!owned || invers.getElement().getCascadeDelete()
                            == ValueMetaData.CASCADE_AUTO)
                        removeFromCollection(other, invers,
                                sm.getManagedInstance());
                    break;
            }
        }
    }

    /**
     * Store null value at the given field. Verify that the given compare
     * value is the value being nulled. Pass NONE for no comparison.
     */
    protected void storeNull(OpenJPAStateManager sm, FieldMetaData fmd,
        Object compare) {
        storeField(sm, fmd, compare, null);
    }

    /**
     * Store a given value at the given field. Compare the given
     * argument if not NONE.
     */
    protected void storeField(OpenJPAStateManager sm, FieldMetaData fmd,
        Object compare, Object val) {
        Object oldValue = sm.fetchObjectField(fmd.getIndex());
        if (oldValue == val)
            return;
        if (compare != NONE && oldValue != compare)
            return;

        switch (_action) {
            case ACTION_MANAGE:
                sm.settingObjectField(sm.getPersistenceCapable(),
                    fmd.getIndex(),
                    oldValue, val, OpenJPAStateManager.SET_USER);
                break;
            case ACTION_WARN:
                warnConsistency(sm, fmd);
                break;
            case ACTION_EXCEPTION:
                throwException(sm, fmd);
            default:
                throw new IllegalStateException();
        }
    }

    /**
     * Remove the given instance from the collection.
     */
    protected void removeFromCollection(OpenJPAStateManager sm,
        FieldMetaData fmd,
        Object val) {
        Collection coll = (Collection) sm.fetchObjectField(fmd.getIndex());
        if (coll != null) {
            switch (_action) {
                case ACTION_MANAGE:
                    remove:
                    for (int i = 0; coll.remove(val); i++)
                        if (i == 0 && coll instanceof Set)
                            break remove;
                    break;
                case ACTION_WARN:
                    if (coll.contains(val))
                        warnConsistency(sm, fmd);
                    break;
                case ACTION_EXCEPTION:
                    if (coll.contains(val))
                        throwException(sm, fmd);
                    break;
                default:
                    throw new IllegalStateException();
            }
        }
    }

    /**
     * Add the given value to the collection at the selected field.
     */
    protected void addToCollection(OpenJPAStateManager sm, FieldMetaData fmd,
        Object val) {
        Collection coll = (Collection) sm.fetchObjectField(fmd.getIndex());
        if (coll == null) {
            coll = (Collection) sm.newFieldProxy(fmd.getIndex());
            sm.storeObjectField(fmd.getIndex(), coll);
        }
        if (!coll.contains(val)) {
            switch (_action) {
                case ACTION_MANAGE:
                    coll.add(val);
                    break;
                case ACTION_WARN:
                    warnConsistency(sm, fmd);
                    break;
                case ACTION_EXCEPTION:
                    throwException(sm, fmd);
                default:
                    throw new IllegalStateException();
            }
        }
    }

    /**
     * Log an inconsistency warning
     */
    protected void warnConsistency(OpenJPAStateManager sm, FieldMetaData fmd) {
        if (_log.isWarnEnabled())
            _log.warn(_loc.get("inverse-consistency", fmd, sm.getId(),
                sm.getContext()));
    }

    /**
     * Throw an inconsistency exception
     */
    protected void throwException(OpenJPAStateManager sm, FieldMetaData fmd) {
        throw new InvalidStateException(_loc.get("inverse-consistency",
            fmd, sm.getId(), sm.getContext())).setFailedObject
            (sm.getManagedInstance()).setFatal(true);
	}
}
