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

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;

import org.apache.openjpa.jdbc.identifier.DBIdentifier;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringDistance;
import org.apache.openjpa.util.InvalidStateException;

/**
 * Represents a database foreign key; may be a logical key with no
 * database representation. This class can also represent a partial key,
 * aligning with {@link DatabaseMetaData}.
 *
 * @author Abe White
 */
public class ForeignKey extends Constraint {
    private static final long serialVersionUID = 1L;

    /**
     * Logical foreign key; links columns, but does not perform any action
     * when the joined primary key columns are modified.
     */
    public static final int ACTION_NONE = 1;

    /**
     * Throw an exception if joined primary key columns are modified.
     */
    public static final int ACTION_RESTRICT = 2;

    /**
     * Cascade any modification of the joined primary key columns to
     * this table. If the joined primary key row is deleted, the row in this
     * table will also be deleted.
     */
    public static final int ACTION_CASCADE = 3;

    /**
     * Null the local columns if the joined primary key columns are modified.
     */
    public static final int ACTION_NULL = 4;

    /**
     * Set the local columns to their default values if the primary key
     * columns are modified.
     */
    public static final int ACTION_DEFAULT = 5;

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

    private DBIdentifier _pkTableName = DBIdentifier.NULL;
    private DBIdentifier _pkSchemaName = DBIdentifier.NULL;
    private DBIdentifier _pkColumnName = DBIdentifier.NULL;
    private int _seq = 0;

    private LinkedHashMap _joins = null;
    private LinkedHashMap _joinsPK = null;
    private LinkedHashMap _consts = null;
    private LinkedHashMap _constsPK = null;
    private int _delAction = ACTION_NONE;
    private int _upAction = ACTION_NONE;
    private int _index = 0;

    // cached items
    private Column[] _locals = null;
    private Column[] _pks = null;
    private Object[] _constVals = null;
    private Column[] _constCols = null;
    private Object[] _constValsPK = null;
    private Column[] _constColsPK = null;
    private Table _pkTable = null;
    private Boolean _autoAssign = null;

    /**
     * Return the foreign key action constant for the given action name.
     */
    public static int getAction(String name) {
        if (name == null || "none".equalsIgnoreCase(name))
            return ACTION_NONE;
        if ("cascade".equalsIgnoreCase(name))
            return ACTION_CASCADE;
        if ("default".equalsIgnoreCase(name))
            return ACTION_DEFAULT;
        if ("restrict".equalsIgnoreCase(name)
            || "exception".equalsIgnoreCase(name))
            return ACTION_RESTRICT;
        if ("null".equalsIgnoreCase(name))
            return ACTION_NULL;

        // not a recognized action; check for typo
        List recognized = Arrays.asList(new String[]{ "none", "exception",
            "restrict", "cascade", "null", "default", });
        String closest = StringDistance.getClosestLevenshteinDistance(name,
            recognized, .5F);

        String msg;
        if (closest != null)
            msg = _loc.get("bad-fk-action-hint", name, closest, recognized)
                .getMessage();
        else
            msg = _loc.get("bad-fk-action", name, recognized).getMessage();
        throw new IllegalArgumentException(msg);
    }

    /**
     * Return the foreign key action name for the given action constant.
     */
    public static String getActionName(int action) {
        switch (action) {
            case ACTION_NONE:
                return "none";
            case ACTION_RESTRICT:
                return "restrict";
            case ACTION_CASCADE:
                return "cascade";
            case ACTION_DEFAULT:
                return "default";
            case ACTION_NULL:
                return "null";
            default:
                throw new IllegalArgumentException(String.valueOf(action));
        }
    }

    /**
     * Default constructor.
     */
    public ForeignKey() {
    }

    /**
     * Constructor.
     *
     * @param name the foreign key name, if any
     * @param table the local table of the foreign key
     * @deprecated
     */
    @Deprecated
    public ForeignKey(String name, Table table) {
        super(name, table);
    }

    public ForeignKey(DBIdentifier name, Table table) {
        super(name, table);
    }

    @Override
    public boolean isLogical() {
        return _delAction == ACTION_NONE;
    }

    /**
     * Whether the primary key columns of this key are auto-incrementing, or
     * whether they themselves are members of a foreign key who's primary key
     * is auto-incrementing (recursing to arbitrary depth).
     */
    public boolean isPrimaryKeyAutoAssigned() {
        if (_autoAssign != null)
            return _autoAssign;
        return isPrimaryKeyAutoAssigned(new ArrayList(3));
    }

    /**
     * Helper to calculate whether this foreign key depends on auto-assigned
     * columns.  Recurses appropriately if the primary key columns this key
     * joins to are themselves members of a foreign key that is dependent on
     * auto-assigned columns.  Caches calculated auto-assign value as a side
     * effect.
     *
     * @param seen track seen foreign keys to prevent infinite recursion in
     * the case of foreign key cycles
     */
    private boolean isPrimaryKeyAutoAssigned(List seen) {
        if (_autoAssign != null)
            return _autoAssign;

        Column[] cols = getPrimaryKeyColumns();
        if (cols.length == 0) {
            _autoAssign = Boolean.FALSE;
            return false;
        }

        for (Column column : cols) {
            if (column.isAutoAssigned()) {
                _autoAssign = Boolean.TRUE;
                return true;
            }
        }

        ForeignKey[] fks = _pkTable.getForeignKeys();
        seen.add(this);
        for (Column col : cols) {
            for (ForeignKey fk : fks) {
                if (!fk.containsColumn(col))
                    continue;
                if (!seen.contains(fk)
                        && fk.isPrimaryKeyAutoAssigned(seen)) {
                    _autoAssign = Boolean.TRUE;
                    return true;
                }
            }
        }

        _autoAssign = Boolean.FALSE;
        return false;
    }

    /**
     * The name of the primary key table.
     * @deprecated
     */
    @Deprecated
    public String getPrimaryKeyTableName() {
        return getPrimaryKeyTableIdentifier().getName();
    }

    public DBIdentifier getPrimaryKeyTableIdentifier() {
        Table table = getPrimaryKeyTable();
        if (table != null)
            return table.getIdentifier();
        return _pkTableName == null ? DBIdentifier.NULL : _pkTableName;
    }

    /**
     * The name of the primary key table. You can only set the primary
     * key table name on foreign keys that have not already been joined.
     * @deprecated
     */
    @Deprecated
    public void setPrimaryKeyTableName(String pkTableName) {
        setPrimaryKeyTableIdentifier(DBIdentifier.newTable(pkTableName));
    }

    public void setPrimaryKeyTableIdentifier(DBIdentifier pkTableName) {
        if (getPrimaryKeyTable() != null)
            throw new IllegalStateException();
        _pkTableName = pkTableName;
    }

    /**
     * The name of the primary key table's schema.
     * @deprecated
     */
    @Deprecated
    public String getPrimaryKeySchemaName() {
        return getPrimaryKeySchemaIdentifier().getName();
    }

    public DBIdentifier getPrimaryKeySchemaIdentifier() {
        Table table = getPrimaryKeyTable();
        if (table != null)
            return table.getSchemaIdentifier();
        return _pkSchemaName;
    }

    /**
     * The name of the primary key table's schema. You can only set the
     * primary key schema name on foreign keys that have not already been
     * joined.
     * @deprecated
     */
    @Deprecated
    public void setPrimaryKeySchemaName(String pkSchemaName) {
        setPrimaryKeySchemaIdentifier(DBIdentifier.newSchema(pkSchemaName));
    }

    public void setPrimaryKeySchemaIdentifier(DBIdentifier pkSchemaName) {
        if (getPrimaryKeyTable() != null)
            throw new IllegalStateException();
        _pkSchemaName = pkSchemaName;
    }

    /**
     * The name of the primary key column.
     * @deprecated
     */
    @Deprecated
    public String getPrimaryKeyColumnName() {
        return getPrimaryKeyColumnIdentifier().getName();
    }

    public DBIdentifier getPrimaryKeyColumnIdentifier() {
        return _pkColumnName == null ? DBIdentifier.NULL : _pkColumnName;
    }

    /**
     * The name of the primary key column. You can only set the
     * primary key column name on foreign keys that have not already been
     * joined.
     * @deprecated
     */
    @Deprecated
    public void setPrimaryKeyColumnName(String pkColumnName) {
        setPrimaryKeyColumnIdentifier(DBIdentifier.newColumn(pkColumnName));
    }

    public void setPrimaryKeyColumnIdentifier(DBIdentifier pkColumnName) {
        if (getPrimaryKeyTable() != null)
            throw new IllegalStateException();
        _pkColumnName = pkColumnName;
    }

    /**
     * The sequence of this join in the foreign key.
     */
    public int getKeySequence() {
        return _seq;
    }

    /**
     * The sequence of this join in the foreign key.
     */
    public void setKeySequence(int seq) {
        _seq = seq;
    }

    /**
     * Return the delete action for the key. Will be one of:
     * {@link #ACTION_NONE}, {@link #ACTION_RESTRICT},
     * {@link #ACTION_CASCADE}, {@link #ACTION_NULL}, {@link #ACTION_DEFAULT}.
     */
    public int getDeleteAction() {
        return _delAction;
    }

    /**
     * Set the delete action for the key. Must be one of:
     * {@link #ACTION_NONE}, {@link #ACTION_RESTRICT},
     * {@link #ACTION_CASCADE}, {@link #ACTION_NULL}, {@link #ACTION_DEFAULT}.
     */
    public void setDeleteAction(int action) {
        _delAction = action;
        if (action == ACTION_NONE)
            _upAction = ACTION_NONE;
        else if (_upAction == ACTION_NONE)
            _upAction = ACTION_RESTRICT;
    }

    /**
     * Return the update action for the key. Will be one of:
     * {@link #ACTION_NONE}, {@link #ACTION_RESTRICT},
     * {@link #ACTION_CASCADE}, {@link #ACTION_NULL}, {@link #ACTION_DEFAULT}.
     */
    public int getUpdateAction() {
        return _upAction;
    }

    /**
     * Set the update action for the key. Must be one of:
     * {@link #ACTION_NONE}, {@link #ACTION_RESTRICT},
     * {@link #ACTION_CASCADE}, {@link #ACTION_NULL}, {@link #ACTION_DEFAULT}.
     */
    public void setUpdateAction(int action) {
        _upAction = action;
        if (action == ACTION_NONE)
            _delAction = ACTION_NONE;
        else if (_delAction == ACTION_NONE)
            _delAction = ACTION_RESTRICT;
    }

    /**
     * Return the foreign key's 0-based index in the owning table.
     */
    public int getIndex() {
        Table table = getTable();
        if (table != null)
            table.indexForeignKeys();
        return _index;
    }

    /**
     * Set the foreign key's 0-based index in the owning table.
     */
    void setIndex(int index) {
        _index = index;
    }

    /**
     * Return the primary key column joined to the given local column.
     */
    public Column getPrimaryKeyColumn(Column local) {
        return (_joins == null) ? null : (Column) _joins.get(local);
    }

    /**
     * Return the local column joined to the given primary key column.
     */
    public Column getColumn(Column pk) {
        return (_joinsPK == null) ? null : (Column) _joinsPK.get(pk);
    }

    /**
     * Return the constant value assigned to the given local column.
     */
    public Object getConstant(Column local) {
        return (_consts == null) ? null : _consts.get(local);
    }

    /**
     * Return the constant value assigned to the given primary key column.
     */
    public Object getPrimaryKeyConstant(Column pk) {
        return (_constsPK == null) ? null : _constsPK.get(pk);
    }

    /**
     * Return the local columns in the foreign key local table order.
     */
    public Column[] getColumns() {
        if (_locals == null)
            _locals = (_joins == null) ? Schemas.EMPTY_COLUMNS : (Column[])
                _joins.keySet().toArray(new Column[_joins.size()]);
        return _locals;
    }

    /**
     * Return the constant values assigned to the local columns
     * returned by {@link #getConstantColumns}.
     */
    public Object[] getConstants() {
        if (_constVals == null)
            _constVals = (_consts == null) ? Schemas.EMPTY_VALUES
                : _consts.values().toArray();
        return _constVals;
    }

    /**
     * Return the constant values assigned to the primary key columns
     * returned by {@link #getConstantPrimaryKeyColumns}.
     */
    public Object[] getPrimaryKeyConstants() {
        if (_constValsPK == null)
            _constValsPK = (_constsPK == null) ? Schemas.EMPTY_VALUES
                : _constsPK.values().toArray();
        return _constValsPK;
    }

    /**
     * Return true if the fk includes the given local column.
     */
    public boolean containsColumn(Column col) {
        return _joins != null && _joins.containsKey(col);
    }

    /**
     * Return true if the fk includes the given primary key column.
     */
    public boolean containsPrimaryKeyColumn(Column col) {
        return _joinsPK != null && _joinsPK.containsKey(col);
    }

    /**
     * Return true if the fk includes the given local column.
     */
    public boolean containsConstantColumn(Column col) {
        return _consts != null && _consts.containsKey(col);
    }

    /**
     * Return true if the fk includes the given primary key column.
     */
    public boolean containsConstantPrimaryKeyColumn(Column col) {
        return _constsPK != null && _constsPK.containsKey(col);
    }

    /**
     * Return the foreign columns in the foreign key, in join-order with
     * the result of {@link #getColumns}.
     */
    public Column[] getPrimaryKeyColumns() {
        if (_pks == null)
            _pks = (_joins == null) ? Schemas.EMPTY_COLUMNS : (Column[])
                _joins.values().toArray(new Column[_joins.size()]);
        return _pks;
    }

    /**
     * Return the local columns that we link to using constant values.
     */
    public Column[] getConstantColumns() {
        if (_constCols == null)
            _constCols = (_consts == null) ? Schemas.EMPTY_COLUMNS : (Column[])
                _consts.keySet().toArray(new Column[_consts.size()]);
        return _constCols;
    }

    /**
     * Return the primary key columns that we link to using constant values.
     */
    public Column[] getConstantPrimaryKeyColumns() {
        if (_constColsPK == null)
            _constColsPK = (_constsPK == null) ? Schemas.EMPTY_COLUMNS :
                (Column[]) _constsPK.keySet().toArray
                    (new Column[_constsPK.size()]);
        return _constColsPK;
    }

    /**
     * Set the foreign key's joins.
     */
    public void setJoins(Column[] cols, Column[] pkCols) {
        Column[] cur = getColumns();
        for (Column column : cur) {
            removeJoin(column);
        }

        if (cols != null)
            for (int i = 0; i < cols.length; i++)
                join(cols[i], pkCols[i]);
    }

    /**
     * Set the foreign key's constant joins.
     */
    public void setConstantJoins(Object[] consts, Column[] pkCols) {
        Column[] cur = getConstantPrimaryKeyColumns();
        for (Column column : cur) {
            removeJoin(column);
        }

        if (consts != null)
            for (int i = 0; i < consts.length; i++)
                joinConstant(consts[i], pkCols[i]);
    }

    /**
     * Set the foreign key's constant joins.
     */
    public void setConstantJoins(Column[] cols, Object[] consts) {
        Column[] cur = getConstantColumns();
        for (Column column : cur) {
            removeJoin(column);
        }

        if (consts != null)
            for (int i = 0; i < consts.length; i++)
                joinConstant(cols[i], consts[i]);
    }

    /**
     * Join a local column to a primary key column of another table.
     */
    public void join(Column local, Column toPK) {
        if (!Objects.equals(local.getTable(), getTable()))
            throw new InvalidStateException(_loc.get("table-mismatch",
                local.getTable(), getTable()));

        Table pkTable = toPK.getTable();
        if (_pkTable != null && !_pkTable.equals(pkTable))
            throw new InvalidStateException(_loc.get("fk-mismatch",
                pkTable, _pkTable));

        _pkTable = pkTable;
        if (_joins == null)
            _joins = new LinkedHashMap();
        _joins.put(local, toPK);
        local.addConstraint(this);
        if (_joinsPK == null)
            _joinsPK = new LinkedHashMap();
        _joinsPK.put(toPK, local);

        // force re-cache
        _locals = null;
        _pks = null;
        if (Boolean.FALSE.equals(_autoAssign))
            _autoAssign = null;
    }

    /**
     * Join a constant value to a primary key column of another table. The
     * constant must be either a string or a number.
     */
    public void joinConstant(Object val, Column toPK) {
        Table pkTable = toPK.getTable();
        if (_pkTable != null && !_pkTable.equals(pkTable))
            throw new InvalidStateException(_loc.get("fk-mismatch",
                pkTable, _pkTable));

        _pkTable = pkTable;
        if (_constsPK == null)
            _constsPK = new LinkedHashMap();
        _constsPK.put(toPK, val);

        // force re-cache
        _constValsPK = null;
        _constColsPK = null;
    }

    /**
     * Join a constant value to a local column of this table. The
     * constant must be either a string or a number.
     */
    public void joinConstant(Column col, Object val) {
        if (_consts == null)
            _consts = new LinkedHashMap();
        _consts.put(col, val);

        // force re-cache
        _constVals = null;
        _constCols = null;
    }

    /**
     * Remove any joins inolving the given column.
     *
     * @return true if the join was removed, false if not part of the key
     */
    public boolean removeJoin(Column col) {
        boolean remd = false;
        Object rem;

        if (_joins != null) {
            rem = _joins.remove(col);
            col.removeConstraint(this);
            if (rem != null) {
                _locals = null;
                _pks = null;
                _joinsPK.remove(rem);
                remd = true;
            }
        }

        if (_joinsPK != null) {
            rem = _joinsPK.remove(col);
            if (rem != null) {
                _locals = null;
                _pks = null;
                _joins.remove(rem);
                remd = true;
            }
        }

        if (_consts != null) {
            if (_consts.remove(col) != null) {
                _constVals = null;
                _constCols = null;
                remd = true;
            }
        }

        if (_constsPK != null) {
            if (_constsPK.containsKey(col)) {
                _constsPK.remove(col);
                _constValsPK = null;
                _constColsPK = null;
                remd = true;
            }
        }

        if ((_joins == null || _joins.isEmpty())
            && (_constsPK == null || _constsPK.isEmpty()))
            _pkTable = null;
        if (remd && Boolean.TRUE.equals(_autoAssign))
            _autoAssign = null;
        return remd;
    }

    /**
     * Returns the table this foreign key is linking to, if it is known yet.
     */
    public Table getPrimaryKeyTable() {
        return _pkTable;
    }

    /**
     * Ref all columns in this key.
     */
    public void refColumns() {
        Column[] cols = getColumns();
        for (Column column : cols) {
            column.ref();
        }
        cols = getConstantColumns();
        for (Column col : cols) {
            col.ref();
        }
    }

    /**
     * Deref all columns in this key.
     */
    public void derefColumns() {
        Column[] cols = getColumns();
        for (Column column : cols) {
            column.deref();
        }
        cols = getConstantColumns();
        for (Column col : cols) {
            col.deref();
        }
    }

    /**
     * Foreign keys are equal if the satisfy the equality constraints of
     * {@link Constraint} and they have the same local and primary key
     * columns and action.
     */
    public boolean equalsForeignKey(ForeignKey fk) {
        if (fk == this)
            return true;
        if (fk == null)
            return false;

        if (getDeleteAction() != fk.getDeleteAction())
            return false;
        if (isDeferred() != fk.isDeferred())
            return false;

        if (!columnsMatch(fk.getColumns(), fk.getPrimaryKeyColumns()))
            return false;
        if (!match(getConstantColumns(), fk.getConstantColumns()))
            return false;
        if (!match(getConstants(), fk.getConstants()))
            return false;
        if (!match(getConstantPrimaryKeyColumns(),
            fk.getConstantPrimaryKeyColumns()))
            return false;
        if (!match(getPrimaryKeyConstants(), fk.getPrimaryKeyConstants()))
            return false;
        return true;
    }

    /**
     * Return true if the given local and foreign columns match those
     * on this key. This can be used to find foreign keys given only
     * column linking information.
     */
    public boolean columnsMatch(Column[] fkCols, Column[] fkPKCols) {
        return match(getColumns(), fkCols)
            && match(getPrimaryKeyColumns(), fkPKCols);
    }

    /**
     * Checks for non-nullable local columns.
     */
    public boolean hasNotNullColumns() {
      Column[] columns = getColumns();
        for (Column column : columns) {
            if (column.isNotNull()) {
                return true;
            }
        }
      return false;
    }

    private static boolean match(Column[] cols, Column[] fkCols) {
        if (cols.length != fkCols.length)
            return false;
        for (Column fkCol : fkCols)
            if (!hasColumn(cols, fkCol))
                return false;
        return true;
    }

    private static boolean hasColumn(Column[] cols, Column col) {
        for (Column column : cols)
            if (column.getQualifiedPath().equals(col.getQualifiedPath()))
                return true;
        return false;
    }

    private static boolean match(Object[] vals, Object[] fkVals) {
        if (vals.length != fkVals.length)
            return false;
        for (int i = 0; i < vals.length; i++)
            if (!Objects.equals(vals[i], fkVals[i]))
                return false;
        return true;
    }

    /**
     * Return the name of the foreignkey constraint as defined in the database.
     * @deprecated
     */
    @Deprecated
    public String loadNameFromDB(DBDictionary dbdict, Connection conn) {
        return loadIdentifierFromDB(dbdict, conn).getName();
    }

    public DBIdentifier loadIdentifierFromDB(DBDictionary dbdict, Connection conn) {
        if( isLogical() || getTable() == null)
            return DBIdentifier.NULL;
        DBIdentifier retVal = DBIdentifier.NULL;
        try{
            Schema schema = getTable().getSchema();
            ForeignKey[] fks = dbdict.getImportedKeys(conn.getMetaData(),
                DBIdentifier.newCatalog(conn.getCatalog()), schema.getIdentifier(),
                getTable().getIdentifier(), conn, false);
            for (ForeignKey fk : fks) {
                Table localtable = schema.getTable(fk.getTableIdentifier());
                Table pkTable = schema.getTable(
                        fk.getPrimaryKeyTableIdentifier());
                boolean addFK = false;
                ForeignKey fkTemp = localtable.getForeignKey(
                        fk.getIdentifier());
                if (fkTemp == null) {
                    addFK = true;
                    fkTemp = localtable.addForeignKey(
                            fk.getIdentifier());
                    fkTemp.setDeferred(fk.isDeferred());
                    fkTemp.setDeleteAction(fk.getDeleteAction());
                }
                if (fk.getColumns() == null || fk.getColumns().length == 0) {
                    // Singular column foreign key
                    if (!fkTemp.containsColumn(
                            localtable.getColumn(fk.getColumnIdentifier())))
                        fkTemp.join(localtable.getColumn(fk.getColumnIdentifier()),
                                pkTable.getColumn(fk.getPrimaryKeyColumnIdentifier()));
                }
                else {
                    // Add the multi-column foreign key, joining local and pk columns in
                    // the temporary key
                    Column[] locCols = fk.getColumns();
                    Column[] pkCols = fk.getPrimaryKeyColumns();
                    // Column counts must match
                    if (locCols != null && pkCols != null &&
                            locCols.length != pkCols.length) {
                        Log log = dbdict.getLog();
                        if (log.isTraceEnabled()) {
                            log.trace(_loc.get("fk-column-mismatch"));
                        }
                    }
                    for (int j = 0; j < locCols.length; j++) {
                        if (!fkTemp.containsColumn(
                                localtable.getColumn(locCols[j].getIdentifier()))) {
                            fkTemp.join(localtable.getColumn(locCols[j].getIdentifier()),
                                    pkTable.getColumn(pkCols[j].getIdentifier()));
                        }
                    }
                }
                if (equalsForeignKey(fkTemp)) {
                    if (addFK)
                        localtable.removeForeignKey(fkTemp);
                    retVal = fk.getIdentifier();
                    break;
                }
                if (addFK)
                    localtable.removeForeignKey(fkTemp);
            }
        } catch(Exception ex){
            Log log = dbdict.getLog();
            if (log.isTraceEnabled()) {
                log.trace(_loc.get("except-read-fk-name"), ex);
            }
        }
        return retVal;
    }

    /**
     * Joins the column of a single column FK to this FK.
     * @param fk
     */
    public void addColumn(ForeignKey fk) {
        // Convert simple name based fk to a multi-column FK if necessary.
        if (getColumns() == null || getColumns().length == 0) {
            // If this FK is single column key, covert to a multi-column key
            Column[] keyCols = createKeyColumns(this);
            if (keyCols[0] != null && keyCols[1] != null) {
                setPrimaryKeyColumnIdentifier(DBIdentifier.NULL);
                setColumnIdentifier(DBIdentifier.NULL);
                join(keyCols[0], keyCols[1]);
            }
        }
        // Create the local and primary key columns from the fk and add them
        // to this fk.
        Column[] keyCols = createKeyColumns(fk);
        if (keyCols[0] != null && keyCols[1] != null) {
            join(keyCols[0], keyCols[1]);
        }
    }

    /*
     * Creates the local and primary key columns for a name-based fk.
     * @return Column[] element 0 is local column
     *                  element 1 is the primary key in another table.
     */
    private static Column[] createKeyColumns(ForeignKey fk) {
        Column fkCol = null;
        if (!DBIdentifier.isEmpty(fk.getColumnIdentifier())) {
            fkCol = new Column();
            fkCol.setIdentifier(fk.getColumnIdentifier());
            fkCol.setTableIdentifier(fk.getTableIdentifier());
            fkCol.setSchemaIdentifier(fk.getSchemaIdentifier());
        }

        Column pkCol = null;
        if (!DBIdentifier.isEmpty(fk.getPrimaryKeyColumnIdentifier())) {
            pkCol = new Column();
            pkCol.setIdentifier(fk.getPrimaryKeyColumnIdentifier());
            pkCol.setTableIdentifier(fk.getPrimaryKeyTableIdentifier());
            pkCol.setSchemaIdentifier(fk.getPrimaryKeySchemaIdentifier());
        }
        return new Column[] { fkCol, pkCol };
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        if (!super.equals(o)) return false;

        ForeignKey that = (ForeignKey) o;

        if (_seq != that._seq) return false;
        if (_delAction != that._delAction) return false;
        if (_upAction != that._upAction) return false;
        if (_index != that._index) return false;
        if (_pkTableName != null ? !_pkTableName.equals(that._pkTableName) : that._pkTableName != null) return false;
        if (_pkSchemaName != null ? !_pkSchemaName.equals(that._pkSchemaName) : that._pkSchemaName != null) return false;
        if (_pkColumnName != null ? !_pkColumnName.equals(that._pkColumnName) : that._pkColumnName != null) return false;
        if (_joins != null ? !_joins.equals(that._joins) : that._joins != null) return false;
        if (_joinsPK != null ? !_joinsPK.equals(that._joinsPK) : that._joinsPK != null) return false;
        if (_consts != null ? !_consts.equals(that._consts) : that._consts != null) return false;
        if (_constsPK != null ? !_constsPK.equals(that._constsPK) : that._constsPK != null) return false;
        // Probably incorrect - comparing Object[] arrays with Arrays.equals
        if (!Arrays.equals(_locals, that._locals)) return false;
        // Probably incorrect - comparing Object[] arrays with Arrays.equals
        if (!Arrays.equals(_pks, that._pks)) return false;
        // Probably incorrect - comparing Object[] arrays with Arrays.equals
        if (!Arrays.equals(_constVals, that._constVals)) return false;
        // Probably incorrect - comparing Object[] arrays with Arrays.equals
        if (!Arrays.equals(_constCols, that._constCols)) return false;
        // Probably incorrect - comparing Object[] arrays with Arrays.equals
        if (!Arrays.equals(_constValsPK, that._constValsPK)) return false;
        // Probably incorrect - comparing Object[] arrays with Arrays.equals
        if (!Arrays.equals(_constColsPK, that._constColsPK)) return false;
        if (_pkTable != null ? !_pkTable.equals(that._pkTable) : that._pkTable != null) return false;
        return _autoAssign != null ? _autoAssign.equals(that._autoAssign) : that._autoAssign == null;
    }

    @Override
    public int hashCode() {
        int result = super.hashCode();
        result = 31 * result + (_pkTableName != null ? _pkTableName.hashCode() : 0);
        result = 31 * result + (_pkSchemaName != null ? _pkSchemaName.hashCode() : 0);
        result = 31 * result + (_pkColumnName != null ? _pkColumnName.hashCode() : 0);
        result = 31 * result + _seq;
        result = 31 * result + (_joins != null ? _joins.hashCode() : 0);
        result = 31 * result + (_joinsPK != null ? _joinsPK.hashCode() : 0);
        result = 31 * result + (_consts != null ? _consts.hashCode() : 0);
        result = 31 * result + (_constsPK != null ? _constsPK.hashCode() : 0);
        result = 31 * result + _delAction;
        result = 31 * result + _upAction;
        result = 31 * result + _index;
        result = 31 * result + Arrays.hashCode(_locals);
        result = 31 * result + Arrays.hashCode(_pks);
        result = 31 * result + Arrays.hashCode(_constVals);
        result = 31 * result + Arrays.hashCode(_constCols);
        result = 31 * result + Arrays.hashCode(_constValsPK);
        result = 31 * result + Arrays.hashCode(_constColsPK);
        result = 31 * result + (_pkTable != null ? _pkTable.hashCode() : 0);
        result = 31 * result + (_autoAssign != null ? _autoAssign.hashCode() : 0);
        return result;
    }

    /*
     * ForeignKey utility class which determines equality based upon the
     * non-column state of the keys.
     */
    public static class FKMapKey {

        private ForeignKey _fk;

        public FKMapKey(ForeignKey fk) {
            _fk = fk;
        }
        public ForeignKey getFk() {
            return _fk;
        }

        @Override
        public int hashCode() {
            return getFk().getIdentifier() != null ? getFk().getIdentifier().hashCode() : getFk().hashCode();
        }

        @Override
        public boolean equals(Object fkObj) {
            if (fkObj == this) {
                return true;
            }
            if (fkObj == null || !(fkObj instanceof FKMapKey)) {
                return false;
            }
            ForeignKey fk = ((FKMapKey)fkObj).getFk();
            if (getFk().getDeleteAction() != fk.getDeleteAction())
                return false;
            if (getFk().isDeferred() != fk.isDeferred())
                return false;
            if (!getFk().getIdentifier().equals(fk.getIdentifier())) {
                return false;
            }
            // Assert PK table name and schema
            if (!getFk().getPrimaryKeySchemaIdentifier().equals(fk.getPrimaryKeySchemaIdentifier()) ||
                !getFk().getPrimaryKeyTableIdentifier().equals(fk.getPrimaryKeyTableIdentifier()) ||
                !getFk().getSchemaIdentifier().equals(fk.getSchemaIdentifier()) ||
                !getFk().getTableIdentifier().equals(fk.getTableIdentifier())) {
                return false;
            }
            return true;
        }
    }
}
