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

import org.apache.openjpa.jdbc.identifier.DBIdentifier;
import org.apache.openjpa.jdbc.kernel.JDBCFetchConfiguration;
import org.apache.openjpa.jdbc.kernel.JDBCStore;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.RelationId;
import org.apache.openjpa.jdbc.meta.ValueHandler;
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.ColumnIO;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.Joins;
import org.apache.openjpa.jdbc.sql.Result;
import org.apache.openjpa.jdbc.sql.Row;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.util.InvalidStateException;

/**
 * Utility methods for strategies using value handlers.
 *
 * @author Abe White
 * @since 0.4.0
 */
public class HandlerStrategies {

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

    /**
     * Map the given value.
     */
    public static Column[] map(ValueMapping vm, String name, ColumnIO io,
        boolean adapt) {
        ValueMappingInfo vinfo = vm.getValueInfo();
        vinfo.assertNoJoin(vm, true);
        vinfo.assertNoForeignKey(vm, !adapt);

        DBDictionary dict = vm.getMappingRepository().getDBDictionary();
        DBIdentifier colName = DBIdentifier.newColumn(name, dict != null ? dict.delimitAll() : false);
        Column[] cols = vm.getHandler().map(vm, colName.getName(), io, adapt);
        if (cols.length > 0 && cols[0].getTable() == null) {
            cols = vinfo.getColumns(vm, colName, cols,
                vm.getFieldMapping().getTable(), adapt);
            if (vinfo.isImplicitRelation()) {
                for (Column col : cols) {
                    col.setImplicitRelation(true);
                }
            }
            ColumnIO mappedIO = vinfo.getColumnIO();
            vm.setColumns(cols);
            vm.setColumnIO(mappedIO);
            if (mappedIO != null) {
                for (int i = 0; i < cols.length; i++) {
                    io.setInsertable(i, mappedIO.isInsertable(i, false));
                    io.setNullInsertable(i, mappedIO.isInsertable(i, true));
                    io.setUpdatable(i, mappedIO.isUpdatable(i, false));
                    io.setNullUpdatable(i, mappedIO.isUpdatable(i, true));
                }
            }
        }
        vm.mapConstraints(colName, adapt);
        return cols;
    }

    /**
     * Set the given value into the given row.
     * Return false if the given value can not be set, for example, due to
     * null constraints on the columns.
     */
    public static boolean set(ValueMapping vm, Object val, JDBCStore store,
        Row row, Column[] cols, ColumnIO io, boolean nullNone)
        throws SQLException {
        if (!canSetAny(row, io, cols))
            return false;

        ValueHandler handler = vm.getHandler();
        val = handler.toDataStoreValue(vm, val, store);
        boolean isSet = false;
        if (val == null) {
            for (int i = 0; i < cols.length; i++)
                if (canSet(row, io, i, true)) {
                    isSet = true;
                    set(row, cols[i], null, handler, nullNone);
                }
        } else if (cols.length == 1) {
            if (canSet(row, io, 0, val == null)) {
                isSet = true;
                set(row, cols[0], val, handler, nullNone);
            }
        } else {
            Object[] vals = (Object[]) val;
            for (int i = 0; i < vals.length; i++)
                if (canSet(row, io, i, vals[i] == null)) {
                    isSet = true;
                    set(row, cols[i], vals[i], handler, nullNone);
                }
        }
        return isSet;
    }

    /**
     * Return true if the given column index is settable.
     */
    private static boolean canSet(Row row, ColumnIO io, int i,
        boolean nullValue) {
        if (row.getAction() == Row.ACTION_INSERT)
        	return io.isInsertable(i, nullValue);
        if (row.getAction() == Row.ACTION_UPDATE)
        	return io.isUpdatable(i, nullValue);
        return true;
    }

    /**
     * Return true if the any column up to the given index is settable.
     */
    private static boolean canSetAny(Row row, ColumnIO io, Column[] cols) {
        if (row.getAction() == Row.ACTION_INSERT)
            return io.isAnyInsertable(cols, false);
        if (row.getAction() == Row.ACTION_UPDATE)
            return io.isAnyUpdatable(cols, false);
        return true;
    }

    /**
     * Set a value into a row, taking care not to override column defaults
     * with nulls unless the user wants us to.
     */
    private static void set(Row row, Column col, Object val,
        ValueHandler handler, boolean nullNone)
        throws SQLException {
        if (val == null)
            row.setNull(col, nullNone);
        else if (col.isRelationId() && handler instanceof RelationId)
            row.setRelationId(col, (OpenJPAStateManager) val,
                (RelationId) handler);
        else
            row.setObject(col, val);
    }

    /**
     * Add where conditions to the given row.
     */
    public static void where(ValueMapping vm, Object val, JDBCStore store,
        Row row, Column[] cols)
        throws SQLException {
        if (cols.length == 0)
            return;

        val = toDataStoreValue(vm, val, cols, store);
        if (val == null)
            for (Column col : cols) {
                row.whereNull(col);
            }
        else if (cols.length == 1)
            where(row, cols[0], val);
        else {
            Object[] vals = (Object[]) val;
            for (int i = 0; i < vals.length; i++)
                where(row, cols[i], vals[i]);
        }
    }

    /**
     * Set a where condition on the given row.
     */
    private static void where(Row row, Column col, Object val)
        throws SQLException {
        if (val == null)
            row.whereNull(col);
        else
            row.whereObject(col, val);
    }

    /**
     * Load the Object value from the given result.
     */
    public static Object loadObject(ValueMapping vm, OpenJPAStateManager sm,
        JDBCStore store, JDBCFetchConfiguration fetch, Result res,
        Joins joins, Column[] cols, boolean objectValueRequiresLoad)
        throws SQLException {
        if (cols.length == 0)
            throw new InvalidStateException(_loc.get("cant-project-owned",
                vm));

        Object val = loadDataStore(vm, res, joins, cols);
        if (objectValueRequiresLoad)
            return vm.getHandler().toObjectValue(vm, val, sm, store, fetch);
        return vm.getHandler().toObjectValue(vm, val);
    }

    /**
     * Load the datastore value from the given result. This method does
     * <b>not</b> process the loaded value through
     * {@link ValueHandler#toObjectValue}.
     */
    public static Object loadDataStore(ValueMapping vm, Result res,
        Joins joins, Column[] cols)
        throws SQLException {
        if (cols.length == 0)
            return null;
        if (cols.length == 1)
            return res.getObject(cols[0], vm.getHandler().getResultArgument(vm), joins);

        Object[] vals = new Object[cols.length];
        Object[] args = (Object[]) vm.getHandler().getResultArgument(vm);
        for (int i = 0; i < cols.length; i++)
            vals[i] = res.getObject(cols[i], (args == null) ? null : args[i],
                joins);
        return vals;
    }

    /**
     * Convert the given object to its datastore value(s). Relation ids are
     * converted to their final values immediately.
     */
    public static Object toDataStoreValue(ValueMapping vm, Object val,
        Column[] cols, JDBCStore store) {
        ValueHandler handler = vm.getHandler();
        val = handler.toDataStoreValue(vm, val, store);
        if (val == null) {
            if (cols.length > 1)
                return new Object[cols.length];
            return null;
        }

        // relation ids are returned as state managers; resolve the final
        // datastore value immediately
        Object[] vals;
        for (int i = 0; i < cols.length; i++) {
            if (!cols[i].isRelationId())
                continue;
            if (!(handler instanceof RelationId))
                break;
            if (cols.length == 1) {
                val = ((RelationId) handler).toRelationDataStoreValue
                    ((OpenJPAStateManager) val, cols[i]);
            } else {
                vals = (Object[]) val;
                vals[i] = ((RelationId) handler).toRelationDataStoreValue
                    ((OpenJPAStateManager) vals[i], cols[i]);
            }
        }
        return val;
    }

    /**
     * Throw the proper exception if the given handler-controlled value
     * represents an unjoinable relation.
     */
    public static void assertJoinable(ValueMapping vm) {
        ClassMapping rel = vm.getTypeMapping();
        if (rel != null && (rel.getTable() == null
            || !rel.getTable().equals(vm.getFieldMapping().getTable())))
            throw RelationStrategies.unjoinable(vm);
    }
}
