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

import java.sql.SQLException;
import java.util.Objects;

import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.RelationId;
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.Table;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.util.InvalidStateException;

/**
 * Primary table row that tracks foreign keys and auto-inc columns.
 *
 * @author Abe White
 */
public class PrimaryRow
    extends RowImpl {

    // VALID flag in superclass uses 2 << 0
    private static final byte PK_SET = 2 << 1;
    private static final byte PK_WHERE = 2 << 2;
    private static final byte DEPENDENT = 2 << 4;

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

    private OpenJPAStateManager _pk = null;
    private ColumnIO _pkIO = null;
    private OpenJPAStateManager[] _fkSet = null;
    private ColumnIO[] _fkIO = null;
    private OpenJPAStateManager[] _fkWhere = null;
    private OpenJPAStateManager[] _relSet = null;
    private RelationId[] _callbacks = null;
    private Object _failed = null;
    private int _idx = -1;

    /**
     * Constructor; supply table and action.
     */
    public PrimaryRow(Table table, int action, OpenJPAStateManager owner) {
        this(table.getColumns(), action, owner);
    }

    protected PrimaryRow(Column[] cols, int action, OpenJPAStateManager owner) {
        super(cols, action);
        _pk = owner;
    }

    /**
     * Mark this row as dependent on some other row.
     */
    @Override
    public boolean isDependent() {
        return (flags & DEPENDENT) > 0;
    }

    /**
     * Mark this row as dependent on some other row.
     */
    public void setDependent(boolean dependent) {
        if (dependent)
            flags |= DEPENDENT;
        else
            flags &= ~DEPENDENT;
    }

    /**
     * The index of this row in ordered row list.
     */
    public int getIndex() {
        return _idx;
    }

    /**
     * The index of this row in ordered row list.
     */
    public void setIndex(int idx) {
        _idx = idx;
    }

    @Override
    public Object getFailedObject() {
        return _failed;
    }

    @Override
    public void setFailedObject(Object failed) {
        _failed = failed;
    }

    @Override
    public OpenJPAStateManager getPrimaryKey() {
        return _pk;
    }

    @Override
    public void setPrimaryKey(OpenJPAStateManager sm)
        throws SQLException {
        setPrimaryKey(null, sm);
    }

    @Override
    public void setPrimaryKey(ColumnIO io, OpenJPAStateManager sm) {
        _pk = sm;
        flags |= PK_SET;
        _pkIO = io;

        // force valid
        setValid(true);
    }

    @Override
    public void wherePrimaryKey(OpenJPAStateManager sm)
        throws SQLException {
        _pk = sm;
        flags |= PK_WHERE;

        // force valid
        if (getAction() == ACTION_DELETE)
            setValid(true);
    }

    /**
     * Return the I/O information for the given set foreign key.
     */
    public ColumnIO getForeignKeyIO(ForeignKey fk) {
        return _fkIO == null ? null : _fkIO.length <= fk.getIndex() ? null : _fkIO[fk.getIndex()];
    }

    /**
     * Return the value for the given foreign key. Values not needed for
     * constraint analyses are not recorded.
     */
    public OpenJPAStateManager getForeignKeySet(ForeignKey fk) {
        return _fkSet == null ? null : _fkSet.length <= fk.getIndex() ? null : _fkSet[fk.getIndex()];
    }

    /**
     * Return the value for the given foreign key. Values not needed for
     * constraint analyses are not recorded.
     */
    public OpenJPAStateManager getForeignKeyWhere(ForeignKey fk) {
        return _fkWhere == null ? null : _fkWhere.length <= fk.getIndex() ? null : _fkWhere[fk.getIndex()];
    }

    @Override
    public void setForeignKey(ForeignKey fk, OpenJPAStateManager sm)
        throws SQLException {
        setForeignKey(fk, null, sm);
    }

    @Override
    public void setForeignKey(ForeignKey fk, ColumnIO io,
        OpenJPAStateManager sm)
        throws SQLException {
        if (!delayForeignKey(fk, sm, true))
            super.setForeignKey(fk, io, sm);
        else
            recordForeignKey(fk, io, sm, true);
    }

    @Override
    public void whereForeignKey(ForeignKey fk, OpenJPAStateManager sm)
        throws SQLException {
        if (!delayForeignKey(fk, sm, false))
            super.whereForeignKey(fk, sm);
        else
            recordForeignKey(fk, null, sm, false);
    }

    @Override
    public void clearForeignKey(ForeignKey fk)
        throws SQLException {
        super.clearForeignKey(fk);
        if (_fkSet != null && _fkSet.length > fk.getIndex())
            _fkSet[fk.getIndex()] = null;
        if (_fkIO != null && _fkIO.length > fk.getIndex())
            _fkIO[fk.getIndex()] = null;
    }

    /**
     * If this is a delete, delay foreign keys to other deleted objects if the
     * key is restricted or cascade. If this is an update or insert, delay
     * foreign keys to other inserts if the key is not logical. If the foreign
     * key is to a new record and the columns are auto-inc, record it.
     */
    private boolean delayForeignKey(ForeignKey fk, OpenJPAStateManager sm,
        boolean set) {
        if (sm == null)
            return false;

        if (getAction() == ACTION_DELETE)
            return sm.isDeleted() && !fk.isDeferred()
                && (fk.getDeleteAction() == ForeignKey.ACTION_RESTRICT ||
                    fk.getDeleteAction() == ForeignKey.ACTION_CASCADE);

        if (!sm.isNew() || sm.isFlushed())
            return false;
        if (!fk.isDeferred() && !fk.isLogical())
            return true;
        if (fk.isPrimaryKeyAutoAssigned())
            return true;
        return false;
    }

    /**
     * Record a delayed foreign key.
     */
    private void recordForeignKey(ForeignKey fk, ColumnIO io,
        OpenJPAStateManager sm, boolean set) {
        if (set) {
            // force valid
            if (canSetAny(io, fk.getColumns().length
                + fk.getConstantColumns().length, false))
                setValid(true);

            if (_fkSet == null)
                _fkSet = new OpenJPAStateManager[getTable().
                    getForeignKeys().length];
            _fkSet[fk.getIndex()] = sm;

            if (_fkIO != null)
                _fkIO[fk.getIndex()] = io;
            else if (io != null && ((getAction() == ACTION_INSERT
                && !io.isAllInsertable(fk, false))
                || (getAction() != ACTION_INSERT
                && !io.isAllUpdatable(fk, false)))) {
                _fkIO = new ColumnIO[_fkSet.length];
                _fkIO[fk.getIndex()] = io;
            }
        } else {
            // force valid
            if (getAction() == ACTION_DELETE)
                setValid(true);

            if (_fkWhere == null)
                _fkWhere = new OpenJPAStateManager[getTable().
                    getForeignKeys().length];
            _fkWhere[fk.getIndex()] = sm;
        }
    }

    /**
     * Return the recorded value for the given relation id column. Only
     * values that are dependent on a new, unflushed auto-assigned instance
     * are recorded.
     */
    public OpenJPAStateManager getRelationIdSet(Column col) {
        return (_relSet == null) ? null : _relSet[getRelationIdIndex(col)];
    }

    /**
     * Return the recorded callbacks for the given relation id column. Only
     * values that are dependent on a new, unflushed auto-assigned instance
     * are recorded.
     */
    public RelationId getRelationIdCallback(Column col) {
        return (_callbacks == null) ? null
            : _callbacks[getRelationIdIndex(col)];
    }

    @Override
    public void setRelationId(Column col, OpenJPAStateManager sm,
        RelationId rel)
        throws SQLException {
        if (sm == null || sm.getObjectId() != null || !sm.isNew()
            || sm.isFlushed() || !isPrimaryKeyAutoAssigned(sm))
            super.setRelationId(col, sm, rel);
        else {
            if (_relSet == null) {
                Column[] cols = getTable().getRelationIdColumns();
                _relSet = new OpenJPAStateManager[cols.length];
                _callbacks = new RelationId[cols.length];
            }
            int idx = getRelationIdIndex(col);
            _relSet[idx] = sm;
            _callbacks[idx] = rel;
        }
    }

    @Override
    public void clearRelationId(Column col)
        throws SQLException {
        super.clearRelationId(col);
        if (_relSet != null) {
            int idx = getRelationIdIndex(col);
            _relSet[idx] = null;
            _callbacks[idx] = null;
        }
    }

    /**
     * Return the index into our relation id array of the value for the
     * given column.
     */
    private int getRelationIdIndex(Column col) {
        Column[] cols = getTable().getRelationIdColumns();
        for (int i = 0; i < cols.length; i++)
            if (cols[i] == col)
                return i;
        return -1;
    }

    /**
     * Return true if any primary key columns of the given instance are
     * auto-assigned.
     */
    private static boolean isPrimaryKeyAutoAssigned(OpenJPAStateManager sm) {
        ClassMapping cls = (ClassMapping) sm.getMetaData();
        while (cls.getJoinablePCSuperclassMapping() != null)
            cls = cls.getJoinablePCSuperclassMapping();
        Column[] cols = cls.getPrimaryKeyColumns();
        for (Column col : cols)
            if (col.isAutoAssigned())
                return true;
        return false;
    }

    @Override
    protected void setObject(Column col, Object val, int metaType,
        boolean overrideDefault)
        throws SQLException {
        // make sure we're not setting two different values
        // unless the given column is an implicit relationship and value
        // changes from logical default to non-default
        Object prev = getSet(col);
        if (prev != null) {
            if (prev == NULL)
                prev = null;
            if (!rowValueEquals(prev, val)) {
                if (isDefaultValue(prev) || allowsUpdate(col, prev, val)) {
                    super.setObject(col, val, metaType, overrideDefault);
                    return;
                } else if (!isDefaultValue(val)) {
                    throw new InvalidStateException(_loc.get("diff-values",
                            new Object[]{ col.getFullDBIdentifier().getName(),
                                    (prev == null) ? null : prev.getClass(), prev,
                                    (val == null) ? null : val.getClass(), val })).
                            setFatal(true);
                } else {
                    // since not allow to update and the new value is 0 or null,
                    // just return.
                    return;
                }
            }
        }
        super.setObject(col, val, metaType, overrideDefault);
    }

    /**
     * Allow the given key column value to be updated if the old value is a default value
     * or the new value is default.
     * For primary keys we even disallow setting the current value to default
     */
    boolean allowsUpdate(Column col, Object old, Object cur) {
        if (col.isPrimaryKey() && isDefaultValue(old) && !isDefaultValue(cur)) {
            // for primary keys we disallow re-setting it to default
            return false;
        }

        return !(col.isPrimaryKey() || col.isRelationId() || col.isImplicitRelation() || col.isUni1MFK())
               || isDefaultValue(old) || isDefaultValue(cur);
    }

    boolean isDefaultValue(Object val) {
        return val == null || val == NULL
                || (val instanceof Number && ((Number)val).longValue() == 0);
    }

    /**
     * Return true if the two values should be considered equal.
     */
    private static boolean rowValueEquals(Object o1, Object o2) {
        if (Objects.equals(o1, o2))
            return true;

        // check for numeric equality (bug #1151)
        return o1 instanceof Number && o2 instanceof Number
            && ((Number) o1).doubleValue() == ((Number) o2).doubleValue();
    }

    @Override
    protected String generateSQL(DBDictionary dict) {
        try {
            if ((flags & PK_SET) > 0)
                super.setPrimaryKey(_pkIO, _pk);
            if ((flags & PK_WHERE) > 0)
                super.wherePrimaryKey(_pk);
            if (_fkSet != null) {
                ForeignKey[] fks = getTable().getForeignKeys();
                ColumnIO io;
                for (int i = 0; i < _fkSet.length; i++) {
                    if (_fkSet[i] != null) {
                        io = (_fkIO == null) ? null : _fkIO[i];
                        super.setForeignKey(fks[i], io, _fkSet[i]);
                    }
                }
            }
            if (_relSet != null) {
                Column[] cols = getTable().getRelationIdColumns();
                for (int i = 0; i < _relSet.length; i++)
                    if (_relSet[i] != null)
                        super.setRelationId(cols[i], _relSet[i], _callbacks[i]);
            }
            if (_fkWhere != null) {
                ForeignKey[] fks = getTable().getForeignKeys();
                for (int i = 0; i < _fkWhere.length; i++)
                    if (_fkWhere[i] != null)
                        super.whereForeignKey(fks[i], _fkWhere[i]);
            }
        }
        catch (SQLException se) {
            throw SQLExceptions.getStore(se, dict);
        }
        return super.generateSQL(dict);
    }

    @Override
    protected RowImpl newInstance(Column[] cols, int action) {
        return new PrimaryRow(cols, action, _pk);
    }

    @Override
    public void copyInto(RowImpl row, boolean whereOnly) {
        super.copyInto(row, whereOnly);
        if (!(row instanceof PrimaryRow))
            return;

        PrimaryRow prow = (PrimaryRow) row;
        prow._pk = _pk;
        prow._pkIO = _pkIO;
        if ((flags & PK_WHERE) > 0)
            prow.flags |= PK_WHERE;
        if (!whereOnly && (flags & PK_SET) > 0)
            prow.flags |= PK_SET;

        if (_fkWhere != null) {
            if (prow._fkWhere == null)
                prow._fkWhere = new OpenJPAStateManager[_fkWhere.length];
            System.arraycopy(_fkWhere, 0, prow._fkWhere, 0, _fkWhere.length);
        }
        if (!whereOnly && _fkSet != null) {
            if (prow._fkSet == null)
                prow._fkSet = new OpenJPAStateManager[_fkSet.length];
            System.arraycopy(_fkSet, 0, prow._fkSet, 0, _fkSet.length);
            if (_fkIO != null) {
                if (prow._fkIO == null)
                    prow._fkIO = new ColumnIO[_fkIO.length];
                System.arraycopy(_fkIO, 0, prow._fkIO, 0, _fkIO.length);
            }
        }
        if (!whereOnly && _relSet != null) {
            if (prow._relSet == null) {
                prow._relSet = new OpenJPAStateManager[_relSet.length];
                prow._callbacks = new RelationId[_callbacks.length];
            }
            System.arraycopy(_relSet, 0, prow._relSet, 0, _relSet.length);
            System.arraycopy(_callbacks, 0, prow._callbacks, 0,
                _callbacks.length);
        }
    }

    @Override
    public String toString() {
    	StringBuilder buf = new StringBuilder();
    	buf.append("PrimaryRow[");
    	switch (getAction()) {
	    	case ACTION_UPDATE: buf.append("UPDATE"); break;
	    	case ACTION_INSERT: buf.append("INSERT"); break;
	    	case ACTION_DELETE: buf.append("DELETE"); break;
	    	default: buf.append("UNKNOWN");
    	}
    	buf.append(" ").append(getTable().getName()).append("]: ");
    	buf.append(_pk);
    	return buf.toString();
    }
}
