/*
 * 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.jdbc.meta.strats;

import java.util.List;

import org.apache.openjpa.jdbc.identifier.DBIdentifier;
import org.apache.openjpa.jdbc.kernel.JDBCStore;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.FieldMapping;
import org.apache.openjpa.jdbc.meta.FieldStrategy;
import org.apache.openjpa.jdbc.meta.JavaSQLTypes;
import org.apache.openjpa.jdbc.meta.RelationId;
import org.apache.openjpa.jdbc.meta.ValueMapping;
import org.apache.openjpa.jdbc.meta.ValueMappingInfo;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.ForeignKey;
import org.apache.openjpa.kernel.DetachedValueStateManager;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.StoreContext;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.util.MetaDataException;
import org.apache.openjpa.util.UserException;

/**
 * Helper methods for relation mappings.
 *
 * @author Abe White
 */
public class RelationStrategies {

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

    /**
     * Return an exception indicating that we cannot join to the given relation.
     */
    public static MetaDataException unjoinable(ValueMapping vm) {
        return new MetaDataException(_loc.get("cant-join", vm));
    }

    /**
     * Return an exception indicating that the relation cannot be loaded
     * because it has independent subclasses and does not represent a full oid.
     */
    public static MetaDataException unloadable(ValueMapping vm) {
        return new MetaDataException(_loc.get("cant-load", vm));
    }

    /**
     * Return an exception indicating that the relation is invalid
     * because it has is based on an inverse foreign key and has independent
     * subclasses.
     */
    public static MetaDataException uninversable(ValueMapping vm) {
        return new MetaDataException(_loc.get("cant-inverse", vm));
    }

    /**
     * Return the given object as its foreign key values.
     *
     * @see FieldStrategy#toDataStoreValue
     */
    public static Object toDataStoreValue(ValueMapping vm, Object val,
        JDBCStore store) {
        ClassMapping rel;
        if (val == null) {
            ClassMapping[] clss = vm.getIndependentTypeMappings();
            rel = (clss.length > 0) ? clss[0] : vm.getTypeMapping();
        } else if (val.getClass() == vm.getType())
            rel = vm.getTypeMapping(); // common case
        else {
            rel = vm.getMappingRepository().getMapping(val.getClass(),
                store.getContext().getClassLoader(), true);
        }
        if (!rel.isMapped())
            throw new UserException(_loc.get("unmapped-datastore-value",
                rel.getDescribedType()));

        Column[] cols;
        if (vm.getJoinDirection() == ValueMapping.JOIN_INVERSE)
            cols = rel.getPrimaryKeyColumns();
        else
            cols = vm.getForeignKey(rel).getPrimaryKeyColumns();
        return rel.toDataStoreValue(val, cols, store);
    }

    /**
     * Map a logical foreign key to an unmapped base class relation.
     */
    public static void mapRelationToUnmappedPC(ValueMapping vm,
        String name, boolean adapt) {
        mapRelationToUnmappedPC(vm, DBIdentifier.newColumn(name), adapt);
    }

    public static void mapRelationToUnmappedPC(ValueMapping vm,
        DBIdentifier name, boolean adapt) {
        if (vm.getTypeMapping().getIdentityType() == ClassMetaData.ID_UNKNOWN)
            throw new MetaDataException(_loc.get("rel-to-unknownid", vm));

        ValueMappingInfo vinfo = vm.getValueInfo();
        Column[] tmplates = newUnmappedPCTemplateColumns(vm, name);
        vm.setColumns(vinfo.getColumns(vm, name, tmplates,
            vm.getFieldMapping().getTable(), adapt));
        vm.setColumnIO(vinfo.getColumnIO());
    }

    /**
     * Create template columns for a logical foreign key to an unmapped base
     * class relation.
     */
    private static Column[] newUnmappedPCTemplateColumns(ValueMapping vm,
        DBIdentifier name) {
        ClassMapping rel = vm.getTypeMapping();
        if (rel.getIdentityType() == ClassMetaData.ID_DATASTORE) {
            Column col = new Column();
            col.setIdentifier(name);
            col.setJavaType(JavaTypes.LONG);
            col.setRelationId(true);
            return new Column[]{ col };
        }

        FieldMapping[] pks = rel.getPrimaryKeyFieldMappings();
        Column[] cols = new Column[pks.length];
        for (int i = 0; i < pks.length; i++) {
            cols[i] = mapPrimaryKey(vm, pks[i]);
            if (cols.length == 1)
                cols[i].setIdentifier(name);
            else if (DBIdentifier.isNull(cols[i].getIdentifier())) {
                DBIdentifier sName = DBIdentifier.combine(cols[i].getIdentifier(), pks[i].getName());
                cols[i].setIdentifier(sName);
            }
            else {
                DBIdentifier sName = DBIdentifier.combine(cols[i].getIdentifier(), cols[i].getName());
                cols[i].setIdentifier(sName);
            }
            cols[i].setTargetField(pks[i].getName());
            cols[i].setRelationId(true);
        }
        return cols;
    }

    /**
     * Create a default column for the given primary key field. Uses the
     * user's raw mapping info if given. Only supports simple field types.
     * The column name will be set to the name of the related primary key
     * column, if any.
     */
    private static Column mapPrimaryKey(ValueMapping vm, FieldMapping pk) {
        List cols = pk.getValueInfo().getColumns();
        if (cols.size() > 1)
            throw new MetaDataException(_loc.get("bad-unmapped-rel", vm, pk));

        Column tmplate = null;
        if (cols.size() == 1)
            tmplate = (Column) cols.get(0);

        Column col = new Column();
        switch (pk.getTypeCode()) {
            case JavaTypes.BOOLEAN:
            case JavaTypes.BOOLEAN_OBJ:
            case JavaTypes.BYTE:
            case JavaTypes.BYTE_OBJ:
            case JavaTypes.CHAR:
            case JavaTypes.CHAR_OBJ:
            case JavaTypes.DOUBLE:
            case JavaTypes.DOUBLE_OBJ:
            case JavaTypes.FLOAT:
            case JavaTypes.FLOAT_OBJ:
            case JavaTypes.INT:
            case JavaTypes.INT_OBJ:
            case JavaTypes.LONG:
            case JavaTypes.LONG_OBJ:
            case JavaTypes.NUMBER:
            case JavaTypes.SHORT:
            case JavaTypes.SHORT_OBJ:
            case JavaTypes.STRING:
            case JavaTypes.BIGINTEGER:
            case JavaTypes.BIGDECIMAL:
                col.setJavaType(pk.getTypeCode());
                break;
            case JavaTypes.DATE:
                col.setJavaType(JavaSQLTypes.getDateTypeCode(pk.getType()));
                break;
            default:
                throw new MetaDataException(
                    _loc.get("bad-unmapped-rel", vm, pk));
        }

        if (tmplate != null) {
            col.setIdentifier(tmplate.getIdentifier());
            col.setType(tmplate.getType());
            col.setTypeName(tmplate.getTypeName());
            col.setSize(tmplate.getSize());
            col.setDecimalDigits(tmplate.getDecimalDigits());
        }
        return col;
    }

    /**
     * Return the state manager for the given instance, using a detached
     * state manager if the instnace is not managed.
     */
    public static OpenJPAStateManager getStateManager(Object obj,
        StoreContext ctx) {
        if (obj == null)
            return null;
        OpenJPAStateManager sm = ctx.getStateManager(obj);
        if (sm == null) // must be detached
            return new DetachedValueStateManager(obj, ctx);
        return sm;
    }

    /**
     * Affirms if all of the given columns represent a {@linkplain RelationId relationship identifier}.
     */
    public static boolean isRelationId(Column[] cols) {
        if (cols == null || cols.length == 0)
            return false;
        for (Column col : cols) {
            if (!col.isRelationId())
                return false;
        }
        return true;
    }

    /**
     * Affirms if all of the foreign key columns represent a {@linkplain RelationId relationship identifier}.
     */
    public static boolean isRelationId(ForeignKey fk) {
        return fk != null && isRelationId(fk.getColumns());
    }

}
