/*
 * 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.kernel.EagerFetchModes;
import org.apache.openjpa.jdbc.kernel.JDBCFetchConfiguration;
import org.apache.openjpa.jdbc.kernel.JDBCStore;
import org.apache.openjpa.jdbc.meta.Embeddable;
import org.apache.openjpa.jdbc.meta.Joinable;
import org.apache.openjpa.jdbc.meta.ValueHandler;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.ColumnIO;
import org.apache.openjpa.jdbc.schema.ForeignKey;
import org.apache.openjpa.jdbc.schema.PrimaryKey;
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.jdbc.sql.RowManager;
import org.apache.openjpa.jdbc.sql.SQLBuffer;
import org.apache.openjpa.jdbc.sql.Select;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.ValueStrategies;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.MetaDataException;
import org.apache.openjpa.util.UserException;

/**
 * Mapping for a single-valued field that delegates to a {@link ValueHandler}.
 *
 * @author Abe White
 * @since 0.4.0
 */
public class HandlerFieldStrategy
    extends AbstractFieldStrategy
    implements Joinable, Embeddable {

    
    private static final long serialVersionUID = 1L;

    private static final Object NULL = new Object();

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

    protected Column[] _cols = null;
    protected ColumnIO _io = null;
    protected Object[] _args = null;
    protected boolean _load = false;
    protected boolean _lob = false;

    @Override
    public void map(boolean adapt) {
        if (field.getHandler() == null)
            throw new MetaDataException(_loc.get("no-handler", field));
        assertNotMappedBy();

        // map join key (if any)
        field.mapJoin(adapt, false);
        field.getKeyMapping().getValueInfo().assertNoSchemaComponents
            (field.getKey(), !adapt);
        field.getElementMapping().getValueInfo().assertNoSchemaComponents
            (field.getElement(), !adapt);

        _io = new ColumnIO();
        _cols = HandlerStrategies.map(field, field.getName(), _io, adapt);
        if (field.getValueStrategy() == ValueStrategies.AUTOASSIGN) {
            // first see if any columns already marked autoassign; if not mark
            // them all
            boolean marked = false;
            for (int i = 0; !marked && i < _cols.length; i++)
                if (_cols[i].isAutoAssigned())
                    marked = true;
            if (!marked)
                for (Column col : _cols) {
                    col.setAutoAssigned(true);
                }
        }

        // add primary key columns to table pk if logical
        field.mapPrimaryKey(adapt);
        PrimaryKey pk = field.getTable().getPrimaryKey();
        if (field.isPrimaryKey() && pk != null && (adapt || pk.isLogical()))
            for (Column col : _cols) {
                pk.addColumn(col);
            }

        // set joinable
        if (!field.getHandler().objectValueRequiresLoad(field))
            for (Column col : _cols) {
                field.getDefiningMapping().setJoinable(col, this);
            }
    }

    @Override
    public void initialize() {
        _load = field.getHandler().objectValueRequiresLoad(field);
        if (_load)
            field.setUsesIntermediate(true);
        for (int i = 0; !_lob && i < _cols.length; i++)
            _lob = _cols[i].isLob();

        Object args = field.getHandler().getResultArgument(field);
        if (args == null)
            _args = null;
        else if (_cols.length == 1)
            _args = new Object[]{ args };
        else
            _args = (Object[]) args;
    }

    @Override
    public void insert(OpenJPAStateManager sm, JDBCStore store, RowManager rm)
        throws SQLException {
        if (field.getColumnIO().isInsertable(0, false)) {
            Row row = field.getRow(sm, store, rm, Row.ACTION_INSERT);
            if (row != null) {
                Object value = sm.fetch(field.getIndex());
                if (!HandlerStrategies.set(field, value, store, row, _cols,
                    _io, field.getNullValue() == FieldMetaData.NULL_NONE))
                    if (field.getValueStrategy() != ValueStrategies.AUTOASSIGN)
                        throw new UserException(_loc.get("cant-set-value", row
                            .getFailedObject(), field, value));
            }
        }
    }

    @Override
    public void update(OpenJPAStateManager sm, JDBCStore store, RowManager rm)
        throws SQLException {
        if (field.getColumnIO().isUpdatable(0, false)) {
            Row row = field.getRow(sm, store, rm, Row.ACTION_UPDATE);
            if (row != null) {
                Object value = sm.fetch(field.getIndex());
                if (!HandlerStrategies.set(field, value, store, row, _cols,
                    _io, field.getNullValue() == FieldMetaData.NULL_NONE))
                    if (field.getValueStrategy() != ValueStrategies.AUTOASSIGN)
                        throw new UserException(_loc.get("cant-set-value", row
                            .getFailedObject(), field, value));
            }
        }
    }

    @Override
    public void delete(OpenJPAStateManager sm, JDBCStore store, RowManager rm)
        throws SQLException {
        field.deleteRow(sm, store, rm);
    }

    @Override
    public int supportsSelect(Select sel, int type, OpenJPAStateManager sm,
        JDBCStore store, JDBCFetchConfiguration fetch) {
        if ((type == Select.TYPE_JOINLESS && sel.isSelected(field.getTable()))
            || (_load && type == Select.TYPE_TWO_PART))
            return 1;
        return 0;
    }

    @Override
    public int select(Select sel, OpenJPAStateManager sm, JDBCStore store,
        JDBCFetchConfiguration fetch, int eagerMode) {
        if (_cols.length == 0)
            return -1;

        if (sm != null && sm.getIntermediate(field.getIndex()) != null)
            return -1;
        if (_lob && !field.isPrimaryKey() && (sel.isDistinct() ||
                eagerMode == EagerFetchModes.EAGER_NONE))
            return -1;
        sel.select(_cols, field.join(sel));
        return 1;
    }

    @Override
    public void load(OpenJPAStateManager sm, JDBCStore store,
        JDBCFetchConfiguration fetch, Result res)
        throws SQLException {
        if (_cols.length == 0 || !res.containsAll(_cols))
            return;

        Object val = HandlerStrategies.loadDataStore(field, res, null, _cols);
        if (!_load)
            sm.store(field.getIndex(), field.getHandler().
                toObjectValue(field, val));
        else {
            if (val == null)
                val = NULL;
            sm.setIntermediate(field.getIndex(), val);
        }
    }

    @Override
    public void load(OpenJPAStateManager sm, JDBCStore store,
        JDBCFetchConfiguration fetch)
        throws SQLException {
        // even if no columns, allow a handler to load a generated value
        if (_cols.length == 0) {
            if (_load)
                sm.store(field.getIndex(), field.getHandler().
                    toObjectValue(field, null, sm, store, fetch));
            else
                sm.store(field.getIndex(), field.getHandler().
                    toObjectValue(field, null));
            return;
        }

        // load cached intermediate value?
        if (_load) {
            Object ds = sm.getIntermediate(field.getIndex());
            if (ds != null) {
                if (ds == NULL)
                    ds = null;
                sm.store(field.getIndex(), field.getHandler().
                    toObjectValue(field, ds, sm, store, fetch));
                return;
            }
        }

        Select sel = store.getSQLFactory().newSelect();
        sel.select(_cols);
        field.wherePrimaryKey(sel, sm, store);

        Result res = sel.execute(store, fetch);
        Object val = null;
        try {
            if (res.next())
                val = HandlerStrategies.loadDataStore(field, res, null, _cols);
        } finally {
            res.close();
        }

        loadEmbedded(sm, store, fetch, val);
    }

    @Override
    public Object toDataStoreValue(Object val, JDBCStore store) {
        return HandlerStrategies.toDataStoreValue(field, val, _cols, store);
    }

    @Override
    public void appendIsNull(SQLBuffer sql, Select sel, Joins joins) {
        joins = join(joins, false);
        for (int i = 0; i < _cols.length; i++) {
            if (i > 0) {
                sql.append(" AND ");
            }
            // Some databases do not allow IS NULL for every column type - let the DBDictionary decide.
            DBDictionary dict = sel.getDictionary();
            sql.append(dict.getIsNullSQL(sel.getColumnAlias(_cols[i], joins), _cols[i].getType()));
        }
    }

    @Override
    public void appendIsNotNull(SQLBuffer sql, Select sel, Joins joins) {
        joins = join(joins, false);
        if (_cols.length > 1) {
            sql.append("(");
        }
        for (int i = 0; i < _cols.length; i++) {
            if (i > 0) {
                sql.append(" OR ");
            }
            // Some databases do not allow IS NOT NULL for every column type - let the DBDictionary decide.
            DBDictionary dict = sel.getDictionary();
            sql.append(dict.getIsNotNullSQL(sel.getColumnAlias(_cols[i], joins), _cols[i].getType()));
        }
        if (_cols.length > 1)
            sql.append(")");
    }

    @Override
    public Joins join(Joins joins, boolean forceOuter) {
        return field.join(joins, forceOuter, false);
    }

    @Override
    public Joins joinRelation(Joins joins, boolean forceOuter,
        boolean traverse) {
        if (traverse)
            HandlerStrategies.assertJoinable(field);
        return joins;
    }

    @Override
    public Object loadProjection(JDBCStore store, JDBCFetchConfiguration fetch,
        Result res, Joins joins)
        throws SQLException {
        return HandlerStrategies.loadObject(field, null, store, fetch, res,
            joins, _cols, _load);
    }

    @Override
    public boolean isVersionable() {
        return !_lob && !field.isJoinOuter()
            && field.getHandler().isVersionable(field);
    }

    @Override
    public void where(OpenJPAStateManager sm, JDBCStore store, RowManager rm,
        Object prevValue)
        throws SQLException {
        Row row = field.getRow(sm, store, rm, Row.ACTION_UPDATE);
        if (row != null)
            HandlerStrategies.where(field, prevValue, store, row, _cols);
    }

    ///////////////////////////
    // Joinable implementation
    ///////////////////////////

    @Override
    public int getFieldIndex() {
        return field.getIndex();
    }

    @Override
    public Object getPrimaryKeyValue(Result res, Column[] cols, ForeignKey fk,
        JDBCStore store, Joins joins)
        throws SQLException {
        Column col;
        Object val = null;
        if (cols.length == 1) {
            col = cols[0];
            if (fk != null){
                col = fk.getColumn(col);
            }

            //OJ-1793: Get the args from the handler and first check to see if the
            //args are null.  If they aren't null then use the first element in the args
            //array rather than passing into 'getObject' the entire args array.  This is
            //akin to what is done in the 'else if' leg below.
            Object[] args = (Object[]) field.getHandler().getResultArgument(field);
            val = res.getObject(col, (args == null) ? null : args[0],
                    joins);
        } else if (cols.length > 1) {
            Object[] vals = new Object[cols.length];
            Object[] args = (Object[]) field.getHandler().
                getResultArgument(field);
            for (int i = 0; i < vals.length; i++) {
                col = cols[i];
                if (fk != null)
                    col = fk.getColumn(col);
                vals[i] = res.getObject(col, (args == null) ? null : args[i],
                    joins);
            }
            val = vals;
        }
        return field.getHandler().toObjectValue(field, val);
    }

    @Override
    public Column[] getColumns() {
        return _cols;
    }

    @Override
    public Object[] getResultArguments() {
        return _args;
    }

    @Override
    public Object getJoinValue(Object fieldVal, Column col, JDBCStore store) {
        Object val = HandlerStrategies.toDataStoreValue(field, fieldVal,
            _cols, store);
        if (val == null || _cols.length < 2)
            return val;

        for (int i = 0; i < _cols.length; i++)
            if (_cols[i] == col)
                return ((Object[]) val)[i];
        throw new InternalException();
    }

    @Override
    public Object getJoinValue(OpenJPAStateManager sm, Column col,
        JDBCStore store) {
        return getJoinValue(sm.fetch(field.getIndex()), col, store);
    }

    @Override
    public void setAutoAssignedValue(OpenJPAStateManager sm, JDBCStore store,
        Column col, Object autoInc) {
        Object data;
        if (_cols.length == 1)
            data = JavaTypes.convert(autoInc, col.getJavaType());
        else {
            // multiple columns; have to get current value, replace this col's
            // value with the given one, and reset
            data = field.getHandler().toDataStoreValue(field,
                sm.fetch(field.getIndex()), store);
            if (data == null)
                data = new Object[_cols.length];
            for (int i = 0; i < _cols.length; i++) {
                if (_cols[i] == col) {
                    ((Object[]) data)[i] = JavaTypes.convert(autoInc,
                        col.getJavaType());
                    break;
                }
            }
        }

        Object val = field.getHandler().toObjectValue(field, data);
        sm.store(field.getIndex(), val);
    }

    /////////////////////////////
    // Embeddable implementation
    /////////////////////////////

    @Override
    public ColumnIO getColumnIO() {
        return _io;
    }

    @Override
    public Object toEmbeddedDataStoreValue(Object val, JDBCStore store) {
        // don't use HandlerStrategies.toDataStoreValue b/c we want relation ids
        // to be represented by state managers, not the serialized id value
        val = field.getHandler().toDataStoreValue(field, val, store);
        if (val == null && _cols.length > 1)
            return new Object[_cols.length];
        return val;
    }

    @Override
    public Object toEmbeddedObjectValue(Object val) {
        if (!_load)
            return field.getHandler().toObjectValue(field, val);
        return UNSUPPORTED;
    }

    @Override
    public void loadEmbedded(OpenJPAStateManager sm, JDBCStore store,
        JDBCFetchConfiguration fetch, Object val)
        throws SQLException {
        if (val == null && _cols.length > 1)
            val = new Object[_cols.length];
        if (_load)
            sm.store(field.getIndex(), field.getHandler().
                toObjectValue(field, val, sm, store, fetch));
        else
            sm.store(field.getIndex(), field.getHandler().
                toObjectValue(field, val));
    }
}
