/* Copyright (c) 1995-2000, The Hypersonic SQL Group.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the Hypersonic SQL Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE HYPERSONIC SQL GROUP,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * on behalf of the Hypersonic SQL Group.
 *
 *
 * For work added by the HSQL Development Group:
 *
 * Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb;

import org.hsqldb.index.RowIterator;
import org.hsqldb.lib.ArrayUtil;
import org.hsqldb.lib.HashMappedList;

// fredt@users 20030813 - patch 1.7.2 - fix for column comparison within same table bugs #572075 and 722443
// fredt@users 20031012 - patch 1.7.2 - better OUTER JOIN implementation
// fredt@users 20031026 - patch 1.7.2 - more efficient findfirst - especially for multi-column equijoins
// implemented optimisations similart to patch 465542 by hjbush@users

/**
 * This class iterates over table rows to select the rows that fulfil join
 * or other conditions. It uses indexes if they are availabe.
 *
 * Extended in successive versions of HSQLDB.
 *
 * @author Thomas Mueller (Hypersonic SQL Group)
 * @version 1.8.0
 * @since Hypersonic SQL
 */
final class TableFilter {

    static final int CONDITION_NONE      = -1;     // not a condition expression
    static final int CONDITION_UNORDERED = 0;      // not candidate for eStart or eEnd
    static final int   CONDITION_START_END = 1;    // candidate for eStart and eEnd
    static final int   CONDITION_START     = 2;    // candidate for eStart
    static final int   CONDITION_END       = 3;    // candidate for eEnd
    static final int   CONDITION_OUTER     = 4;    // add to this
    Table              filterTable;
    private String     tableAlias;
    HashMappedList     columnAliases;
    Index              filterIndex;
    private Object[]   emptyData;
    boolean[]          usedColumns;
    private Expression eStart, eEnd;

    //
    Expression eAnd;

    //
    boolean      isOuterJoin;                      // table joined with OUTER JOIN
    boolean      isAssigned;                       // conditions have been assigned to this
    boolean      isMultiFindFirst;                 // findFirst() uses multi-column index
    Expression[] findFirstExpressions;             // expressions for column values

    //
    private RowIterator it;
    Object[]            currentData;
    Row                 currentRow;

    //
    Object[] currentJoinData;

    // addendum to the result of findFirst() and next() with isOuterJoin==true
    // when the result is false, it indicates if a non-join condition caused the failure
    boolean nonJoinIsNull;

    // indicates current data is empty data produced for an outer join
    boolean isCurrentOuter;

    /**
     * Constructor declaration
     *
     *
     * @param t
     * @param alias
     * @param outerjoin
     */
    TableFilter(Table t, String alias, HashMappedList columnList,
                boolean outerjoin) {

        filterTable   = t;
        tableAlias    = alias == null ? t.getName().name
                                      : alias;
        columnAliases = columnList;
        isOuterJoin   = outerjoin;
        emptyData     = filterTable.getEmptyRowData();
        usedColumns   = filterTable.getNewColumnCheckList();
    }

    /**
     * Returns the alias or the table name.
     * Never returns null;
     * @return
     */
    String getName() {
        return tableAlias;
    }

    /**
     * Retrieves this object's filter Table object.
     *
     * @return this object's filter Table object
     */
    Table getTable() {
        return filterTable;
    }

    /**
     * Retrieves a CONDITION_XXX code indicating how a condition
     * expression can be used for a TableFilter.
     *
     * @param exprType an expression type code
     * @return
     */
    static int getConditionType(Expression e) {

        int exprType = e.getType();

        switch (exprType) {

            case Expression.NOT_EQUAL :
            case Expression.LIKE :
                return CONDITION_UNORDERED;

            case Expression.IN : {
                return e.isQueryCorrelated ? CONDITION_NONE
                                           : CONDITION_UNORDERED;
            }
            case Expression.IS_NULL :
            case Expression.EQUAL : {
                return CONDITION_START_END;
            }
            case Expression.BIGGER :
            case Expression.BIGGER_EQUAL : {
                return CONDITION_START;
            }
            case Expression.SMALLER :
            case Expression.SMALLER_EQUAL : {
                return CONDITION_END;
            }
            default : {

                // not a condition so forget it
                return CONDITION_NONE;
            }
        }
    }

    // TODO: Optimize
    //
    // The current way always chooses eStart, eEnd conditions
    // using first encountered eligible index
    //
    // We should check if current index offers better selectivity/access
    // path than previously assigned iIndex.
    //
    // EXAMPLE 1:
    //
    // CREATE TABLE t (c1 int, c2 int primary key)
    // CREATE INDEX I1 ON t(c1)
    // SELECT
    //      *
    // FROM
    //      t
    // WHERE
    //     c1 = | < | <= | >= | > ...
    // AND
    //     c2 = | < | <= | >= | > ...
    //
    // currently always chooses iIndex / condition (c1/I1), over
    // index / condition (c2/pk), whereas index / condition (c2/pk)
    // may well be better, especially if condition on c2 is equality
    // (condition_start_end) and conditionon(s) on c1 involve range
    // (condition_start, condition_end, or some composite).
    //
    // Currently, the developer/client software must somehow know facts
    // both about the table, the query and the way HSQLDB forms its
    // plans and, based on this knowlege, perhaps decide to reverse
    // order by explicitly issuing instead:
    //
    // SELECT
    //      *
    // FROM
    //      t
    // WHERE
    //     c2 = | < | <= | >= | > ...
    // AND
    //     c1 = | < | <= | >= | > ...
    //
    // to get optimal index choice.
    //
    // The same thing applies to and is even worse for joins.
    //
    // Consider the following (highly artificial, but easy to
    // understand) case:
    //
    // CREATE TABLE T1(ID INTEGER PRIMARY KEY, C1 INTEGER)
    // CREATE INDEX I1 ON T1(C1)
    // CREATE TABLE T2(ID INTEGER PRIMARY KEY, C1 INTEGER)
    // CREATE INDEX I2 ON T2(C1)
    //
    // select * from t1, t2 where t1.c1 = t2.c1 and t1.id = t2.id
    //
    // Consider the worst value distribution where t1 and t2 are both
    // 10,000 rows, c1 selectivity is nil (all values are identical)
    // for both tables, and, say, id values span the range 0..9999
    // for both tables.
    //
    // Then time to completion on 500 MHz Athlon testbed using memory
    // tables is:
    //
    // 10000 row(s) in 309114 ms
    //
    // whereas for:
    //
    // select * from t1, t2 where t1.id = t2.id and t1.c1 = t2.c1
    //
    // time to completion is:
    //
    // 10000 row(s) in 471 ms
    //
    // Hence, the unoptimized query takes 656 times as long as the
    // optimized one!!!
    //
    // EXAMPLE 2:
    //
    // If there are, say, two non-unique candidate indexes,
    // and some range or equality predicates against
    // them, preference should be given to the one with
    // better selectivity (if the total row count of the
    // table is large, otherwise the overhead of making
    // the choice is probably large w.r.t. any possible
    // savings).  Might require maintaining some basic
    // statistics or performing appropriate index probes
    // at the time the plan is being generated.

    /**
     * Chooses certain query conditions and assigns a copy of them to this
     * filter. The original condition is set to Expression.TRUE once assigned.
     *
     * @param condition
     *
     * @throws HsqlException
     */
    void setConditions(Session session,
                       Expression condition) throws HsqlException {

        setCondition(session, condition);

        if (filterIndex == null) {
            filterIndex = filterTable.getPrimaryIndex();
        }

        if (filterIndex.getVisibleColumns() == 1 || eStart == null
                || eAnd == null || eStart.exprType != Expression.EQUAL) {
            return;
        }

        boolean[]    check    = filterTable.getNewColumnCheckList();
        Expression[] expr     = new Expression[check.length];
        int          colindex = eStart.getArg().getColumnNr();

        check[colindex] = true;
        expr[colindex]  = eStart.getArg2();

        eAnd.getEquiJoinColumns(this, check, expr);

        if (ArrayUtil.containsAllTrueElements(check, filterIndex.colCheck)) {
            isMultiFindFirst     = true;
            findFirstExpressions = expr;
        }
    }

    private void setCondition(Session session,
                              Expression e) throws HsqlException {

        int        type = e.getType();
        Expression e1   = e.getArg();
        Expression e2   = e.getArg2();

        isAssigned = true;

        if (type == Expression.AND) {
            setCondition(session, e1);
            setCondition(session, e2);

            return;
        }

        if (type == Expression.OR && isOuterJoin && e.isInJoin
                && e.outerFilter == this) {
            addAndCondition(e);
            e.setTrue();

            return;
        }

        int conditionType = getConditionType(e);

        if (conditionType == CONDITION_NONE) {

            // not a condition expression
            return;
        }

// fredt@users 20030813 - patch 1.7.2 - fix for column comparison within same table bugs #572075 and 722443
        if (e1.getFilter() == this && e2.getFilter() == this) {
            conditionType = CONDITION_UNORDERED;
        } else if (e1.getFilter() == this) {
            if (!e.isInJoin && isOuterJoin) {

                // do not use a where condition on the second table in outer joins
                return;
            }

            // ok include this
        } else if ((e2.getFilter() == this)
                   && (conditionType != CONDITION_UNORDERED)) {

            // swap and try again to allow index usage
            e.swapCondition();
            setCondition(session, e);

            return;
        } else if (e1.outerFilter == this) {

            // fredt - this test is last to allow swapping the terms above
            conditionType = CONDITION_OUTER;
        } else {

            // unrelated: don't include
            return;
        }

//        Trace.doAssert(e1.getFilter() == this, "setCondition");
        if (!e2.isResolved()) {
            return;
        }

        // fredt - condition defined in outer but not this one
        if (e1.outerFilter != null && e1.outerFilter != this) {
            return;
        }

        if (conditionType == CONDITION_UNORDERED) {
            addAndCondition(e);

            return;
        }

        if (conditionType == CONDITION_OUTER) {
            addAndCondition(e);

            return;
        }

        int   i     = e1.getColumnNr();
        Index index = filterTable.getIndexForColumn(session, i);

        if (index == null || (filterIndex != index && filterIndex != null)) {
            addAndCondition(e);

            return;
        }

        filterIndex = index;

        switch (conditionType) {

            case CONDITION_START_END : {

                // candidate for both start and end
                if ((eStart != null) || (eEnd != null)) {
                    addAndCondition(e);

                    return;
                }

                eStart = new Expression(e);
                eEnd   = eStart;

                break;
            }
            case CONDITION_START : {

                // candidate for start
                if (eStart != null) {
                    addAndCondition(e);

                    return;
                }

                eStart = new Expression(e);

                break;
            }
            case CONDITION_END : {

                // candidate for end
                if (eEnd != null) {
                    addAndCondition(e);

                    return;
                }

                eEnd = new Expression(e);

                break;
            }
        }

        e.setTrue();
    }

    /**
     * Finds the first row in the table (using an index if there is one) and
     * checks it against the eEnd (range) and eAnd (other conditions)
     * Expression objects. (fredt)
     *
     * @return true if first row was found, else false
     */
    boolean findFirst(Session session) throws HsqlException {

        nonJoinIsNull  = false;
        isCurrentOuter = false;

        if (filterIndex == null) {
            filterIndex = filterTable.getPrimaryIndex();
        }

        if (isMultiFindFirst) {
            boolean convertible = true;
            int[]   types       = filterTable.getColumnTypes();

            currentJoinData = filterTable.getEmptyRowData();

            for (int i = 0; i < findFirstExpressions.length; i++) {
                Expression e = findFirstExpressions[i];

                if (e != null) {
                    Object value = e.getValue(session);

                    if (Column.compareToTypeRange(value, types[i]) != 0) {
                        convertible = false;

                        break;
                    }

                    value = Column.convertObject(value, types[i]);
                    currentJoinData[i] = e.getValue(session, types[i]);
                }
            }

            it = convertible
                 ? filterIndex.findFirstRow(session, currentJoinData)
                 : filterIndex.emptyIterator();

            if (!it.hasNext()) {
                ArrayUtil.clearArray(ArrayUtil.CLASS_CODE_OBJECT,
                                     currentJoinData, 0,
                                     currentJoinData.length);
            }
        } else if (eStart == null) {
            it = eEnd == null ? filterIndex.firstRow(session)
                              : filterIndex.findFirstRowNotNull(session);
        } else {
            Object value      = eStart.getArg2().getValue(session);
            int    valuetype  = eStart.getArg2().getDataType();
            int    targettype = eStart.getArg().getDataType();

            it = getFirstIterator(session, eStart.getType(), value,
                                  valuetype, filterIndex, targettype);
        }

        while (true) {
            currentRow = it.next();

            if (currentRow == null) {
                break;
            }

            currentData = currentRow.getData();

            if (!(eEnd == null || eEnd.testCondition(session))) {
                break;
            }

            if (eAnd == null || eAnd.testCondition(session)) {
                return true;
            }
        }

        currentRow  = null;
        currentData = emptyData;

        return false;
    }

    static RowIterator getFirstIterator(Session session, int eType,
                                        Object value, int valueType,
                                        Index index,
                                        int targetType) throws HsqlException {

        RowIterator it;
        int         range = 0;

        if (targetType != valueType) {
            range = Column.compareToTypeRange(value, targetType);
        }

        if (range == 0) {
            value = Column.convertObject(value, targetType);
            it    = index.findFirstRow(session, value, eType);
        } else {
            switch (eType) {

                case Expression.BIGGER_EQUAL :
                case Expression.BIGGER :
                    if (range < 0) {
                        it = index.findFirstRowNotNull(session);

                        break;
                    }
                default :
                    it = index.emptyIterator();
            }
        }

        return it;
    }

    /**
     * Advances to the next available value. <p>
     *
     * @return true if a next value is available upon exit
     *
     * @throws HsqlException if a database access error occurs
     */
    boolean next(Session session) throws HsqlException {

        boolean result = false;

        nonJoinIsNull  = false;
        isCurrentOuter = false;

        while (true) {
            currentRow = it.next();

            if (currentRow == null) {
                break;
            }

            currentData = currentRow.getData();

            if (!(eEnd == null || eEnd.testCondition(session))) {
                break;
            }

            if (eAnd == null || eAnd.testCondition(session)) {
                result = true;

                break;
            }
        }

        if (result) {
            return true;
        }

        currentRow  = null;
        currentData = emptyData;

        return false;
    }

    boolean nextOuter(Session session) throws HsqlException {

        nonJoinIsNull  = false;
        isCurrentOuter = true;
        currentData    = emptyData;
        currentRow     = null;

        return eAnd == null || (eAnd.getFilter() != this && eAnd.isInJoin)
               || eAnd.testCondition(session);
    }

    /**
     * Forms a new conjunction using the given condition and this filter's
     * pre-existing AND condition, or sets the given condition as this filter's
     * AND condition when there is no such pre-exisiting object.
     *
     * @param e the condition to add
     */
    private void addAndCondition(Expression e) {

        Expression e2 = new Expression(e);

        if (eAnd == null) {
            eAnd = e2;
        } else {
            Expression and = new Expression(Expression.AND, eAnd, e2);

            eAnd = and;
        }

        e.setTrue();
    }

    /**
     * Removes reference to Index to avoid possible memory leaks after alter
     * table or drop index
     */
    void setAsCheckFilter() {
        filterIndex = null;
    }

// boucheb@users 20030415 - added for debugging support

    /**
     * Retreives a String representation of this obejct. <p>
     *
     * The returned String describes this object's table, alias
     * access mode, index, join mode, Start, End and And conditions.
     *
     * @return a String representation of this object
     */
    public String describe(Session session) {

        StringBuffer sb;
        String       temp;
        Index        index;
        Index        primaryIndex;
        int[]        primaryKey;
        boolean      hidden;
        boolean      fullScan;

        sb           = new StringBuffer();
        index        = filterIndex;
        primaryIndex = filterTable.getPrimaryIndex();
        primaryKey   = filterTable.getPrimaryKey();
        hidden       = false;
        fullScan     = (eStart == null && eEnd == null);

        if (index == null) {
            index = primaryIndex;
        }

        if (index == primaryIndex && primaryKey.length == 0) {
            hidden   = true;
            fullScan = true;
        }

        sb.append(super.toString()).append('\n');
        sb.append("table=[").append(filterTable.getName().name).append("]\n");
        sb.append("alias=[").append(tableAlias).append("]\n");
        sb.append("access=[").append(fullScan ? "FULL SCAN"
                                              : "INDEX PRED").append("]\n");
        sb.append("index=[");
        sb.append(index == null ? "NONE"
                                : index.getName() == null ? "UNNAMED"
                                                          : index.getName()
                                                          .name);
        sb.append(hidden ? "[HIDDEN]]\n"
                         : "]\n");
        sb.append("isOuterJoin=[").append(isOuterJoin).append("]\n");

        temp = eStart == null ? "null"
                              : eStart.describe(session);

        sb.append("eStart=[").append(temp).append("]\n");

        temp = eEnd == null ? "null"
                            : eEnd.describe(session);

        sb.append("eEnd=[").append(temp).append("]\n");

        temp = eAnd == null ? "null"
                            : eAnd.describe(session);

        sb.append("eAnd=[").append(temp).append("]");

        return sb.toString();
    }
}
