/**
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. 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.
 *
 * 3. The name "Exolab" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Intalio, Inc.  For written permission,
 *    please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab"
 *    nor may "Exolab" appear in their names without prior written
 *    permission of Intalio, Inc. Exolab is a registered
 *    trademark of Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project
 *    (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED 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
 * INTALIO, INC. OR ITS 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.
 *
 * Copyright 1999 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id: JDBCQueryExpression.java 8459 2009-11-09 04:27:56Z rjoachim $
 */
package org.castor.cpa.persistence.sql.driver;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.castor.core.util.Messages;
import org.exolab.castor.jdo.DbMetaInfo;
import org.exolab.castor.jdo.engine.JDBCSyntax;
import org.exolab.castor.jdo.oql.SyntaxNotSupportedException;
import org.exolab.castor.persist.spi.PersistenceFactory;
import org.exolab.castor.persist.spi.QueryExpression;

/**
 *
 * @author <a href="arkin@intalio.com">Assaf Arkin</a>
 * @version $Revision: 8459 $ $Date: 2006-04-10 16:39:24 -0600 (Mon, 10 Apr 2006) $
 */
public class JDBCQueryExpression implements QueryExpression {
    /** The <a href="http://jakarta.apache.org/commons/logging/">Jakarta
     *  Commons Logging</a> instance used for all logging. */
    private static Log _log = LogFactory.getFactory().getInstance(JDBCQueryExpression.class);

    protected Hashtable<String, String> _tables = new Hashtable<String, String>();

    private Vector<String> _cols = new Vector<String>();

    private Vector<String> _conds = new Vector<String>();

    protected Vector<Join> _joins = new Vector<Join>();

    protected String _select;

    private String _where;

    protected String _order;

    protected String _limit;
    
    protected String _offset;

    protected boolean _distinct = false;

    protected PersistenceFactory _factory;

    /** MetaInfo as acquired from the RDBMS. */
    protected DbMetaInfo _dbInfo;

    public JDBCQueryExpression(final PersistenceFactory factory) {
        _factory = factory;
    }

    /**
     * Store database meta information.
     *
     * @param dbInfo DbMetaInfo instance.
     */    
    public final void setDbMetaInfo(final DbMetaInfo dbInfo) {
        _dbInfo = dbInfo;
    }

    public final void setDistinct(final boolean distinct) {
        _distinct = distinct;
    }

    public final void addColumn(final String tableName, final String columnName) {
        _tables.put(tableName, tableName);
        _cols.addElement(_factory.quoteName(
                tableName + JDBCSyntax.TABLE_COLUMN_SEPARATOR + columnName));
    }

    public final void addTable(final String tableName, final String tableAlias) {
        _tables.put(tableAlias, tableName);
    }

    public final void addParameter(final String tableName, final String columnName, final String condOp) {
        addCondition(tableName, columnName, condOp, JDBCSyntax.PARAMETER);
    }

    public final void addCondition(final String tableName, final String columnName, final String condOp,
            final String value) {
        _tables.put(tableName, tableName);
        _conds.addElement(_factory.quoteName(
                tableName + JDBCSyntax.TABLE_COLUMN_SEPARATOR + columnName) + condOp + value);
    }

    public final String encodeColumn(final String tableName, final String columnName) {
        return _factory.quoteName(tableName + JDBCSyntax.TABLE_COLUMN_SEPARATOR + columnName);
    }

    public final void addInnerJoin(final String leftTable, final String[] leftColumn,
            final String leftTableAlias, final String rightTable, final String[] rightColumn,
            final String rightTableAlias) {
        int index;
        Join join;

        _tables.put(leftTableAlias, leftTable);
        _tables.put(rightTableAlias, rightTable);
        join = new Join(leftTableAlias, leftColumn, rightTableAlias, rightColumn, false);
        index = _joins.indexOf(join);
        if (index < 0) {
            _joins.add(join);
        } else {
            // inner join overrides outer joins
            _joins.set(index, join);
        }
    }

    public final void addOuterJoin(final String leftTable, final String[] leftColumn,
            final String rightTable, final String[] rightColumn, final String rightTableAlias) {
        int index;
        Join join;

        _tables.put(leftTable, leftTable);
        _tables.put(rightTableAlias, rightTable);
        join = new Join(leftTable, leftColumn, rightTableAlias, rightColumn, true);
        index = _joins.indexOf(join);
        if (index < 0) {
            _joins.add(join);
        }
    }

    public final void addSelect(final String selectClause) {
        _select = selectClause;
    }

    public final void addWhereClause(final String where) {
        _where = where;
    }

    public final void addOrderClause(final String order) {
        _order = order;
    }

    public final void addLimitClause(final String limit) throws SyntaxNotSupportedException {
        if (isLimitClauseSupported()) {
            _limit = limit;
        } else {
            throw new SyntaxNotSupportedException (Messages.format(
                    "query.limitClauseNotSupported", _factory.getFactoryName()));
        }
    }

    public final void addOffsetClause(final String offset) throws SyntaxNotSupportedException {
        if (isOffsetClauseSupported()) {
            _offset = offset;
        } else {
            throw new SyntaxNotSupportedException(Messages.format(
                    "query.offsetClauseNotSupported", _factory.getFactoryName()));
        }
    }

    protected final String getColumnList() {
        StringBuffer sql;

        if ((_cols.size() == 0)  && (_select == null)) {
            return "1";
        }

        sql = new StringBuffer();
        int i = 0;
        for (i = 0; i < _cols.size(); ++i) {
            if (i > 0) {
                sql.append(JDBCSyntax.COLUMN_SEPARATOR);
            }
            sql.append(_cols.elementAt(i));
        }

        if (_select != null) {
            if (i > 0) {
                sql.append(JDBCSyntax.COLUMN_SEPARATOR).append(_select);
            } else {
                sql.append(_select);
            }
        }

        return sql.toString();
    }

    protected final boolean addWhereClause(final StringBuffer sql, final boolean first) {
        boolean internalFirst = first;
        if (_conds.size() > 0) {
            if (internalFirst) {
                sql.append(JDBCSyntax.WHERE);
                internalFirst = false;
            } else {
                sql.append(JDBCSyntax.AND);
            }
            for (int i = 0; i < _conds.size(); ++i) {
                if (i > 0) {
                    sql.append(JDBCSyntax.AND);
                }
                sql.append(_conds.elementAt(i));
            }
        }
        if (_where != null) {
            if (internalFirst) {
                sql.append(JDBCSyntax.WHERE);
                internalFirst = false;
            } else {
                sql.append(JDBCSyntax.AND);
            }
            sql.append('(');
            sql.append(_where);
            sql.append(')');
        }
        return internalFirst;
    }
    
    /**
     * Creates a SQL statement.
     * In general, for a RDBMS/JDBC driver with a full support of the SQL standard/JDBC 
     * specification, this will return a valid SQL statement. For some features,
     * a particular RDBMS might indicate that it does not support this feature by 
     * throwing a {@link SyntaxNotSupportedException}.
     * @throws SyntaxNotSupportedException If the RDBMS does not support a particular feature.
     */
    public String getStatement(final boolean lock) throws SyntaxNotSupportedException {
        return getStandardStatement(lock, true).toString();
    }

    /**
     * Helper method. Can be used in two cases:
     * 1) for JDBC drivers which support "{oj ...OUTER JOIN ...}" notation (in accordance with
     * JDBC specification);
     * 2) for the databases which support "... OUTER JOIN ..." notation (in accordance with
     * SQL-92 standard); .
     * @param lock whether to lock selected tables
     * @param oj true in the first case above, false in the second case.
     **/
    protected final StringBuffer getStandardStatement(final boolean lock, final boolean oj) {
        StringBuffer sql = new StringBuffer();
        sql.append(JDBCSyntax.SELECT);
        if (_distinct) {
            sql.append(JDBCSyntax.DISTINCT);
        }

        if (_select == null) {
            sql.append(getColumnList());
        } else {
            sql.append(_select).append(" ");
        }

        sql.append(JDBCSyntax.FROM);

        // Use outer join syntax for all outer joins. Inner joins come later.
        Hashtable<String, String> tables = new Hashtable<String, String>(_tables);
        Vector<String> done = new Vector<String>();
        boolean first = true;
        // gather all outer joins with the same left part
        for (int i = 0; i < _joins.size(); ++i) {
            Join join = _joins.elementAt(i);

            if (!join._outer || done.contains(join._leftTable)) {
                continue;
            }
            if (first) {
                first = false;
            } else {
                sql.append(JDBCSyntax.TABLE_SEPARATOR);
            }
            if (oj) {
                sql.append("{oj ");
            }
            sql.append(_factory.quoteName(join._leftTable));
            sql.append(JDBCSyntax.LEFT_JOIN);
            String tableName = tables.get(join._rightTable);
            if (join._rightTable.equals(tableName)) {
                sql.append(_factory.quoteName(tableName));
            } else {
                sql.append(_factory.quoteName(tableName) + " "
                        + _factory.quoteName(join._rightTable));
            }
            sql.append(JDBCSyntax.ON);
            for (int j = 0; j < join._leftColumns.length; ++j) {
                if (j > 0) {
                    sql.append(JDBCSyntax.AND);
                }
                sql.append(_factory.quoteName(join._leftTable
                        + JDBCSyntax.TABLE_COLUMN_SEPARATOR
                        + join._leftColumns[j])).append(OP_EQUALS);
                sql.append(_factory.quoteName(join._rightTable
                        + JDBCSyntax.TABLE_COLUMN_SEPARATOR
                        + join._rightColumns[j]));
            }

            tables.remove(join._leftTable);
            tables.remove(join._rightTable);
            for (int k = i + 1; k < _joins.size(); ++k) {
                Join join2;

                join2 = _joins.elementAt(k);
                if (!join2._outer || !join._leftTable.equals(join2._leftTable)) {
                    continue;
                }
                sql.append(JDBCSyntax.LEFT_JOIN);
                tableName = tables.get(join2._rightTable);

                if (join2._rightTable.equals(tableName)) {
                    sql.append(_factory.quoteName(tableName));
                } else {
                    sql.append(_factory.quoteName(tableName) + " "
                            + _factory.quoteName(join2._rightTable));
                }
                sql.append(JDBCSyntax.ON);
                for (int j = 0; j < join2._leftColumns.length; ++j) {
                    if (j > 0) {
                        sql.append(JDBCSyntax.AND);
                    }
                    sql.append(_factory.quoteName(join2._leftTable
                            + JDBCSyntax.TABLE_COLUMN_SEPARATOR
                            + join2._leftColumns[j])).append(OP_EQUALS);
                    sql.append(_factory.quoteName(join2._rightTable
                            + JDBCSyntax.TABLE_COLUMN_SEPARATOR
                            + join2._rightColumns[j]));
                }
                tables.remove(join2._rightTable);
            }
            if (oj) {
                sql.append("}");
            }
            done.addElement(join._leftTable);
        }
        Enumeration<String> enumeration = tables.keys();
        while (enumeration.hasMoreElements()) {
            if (first) {
                first = false;
            } else {
                sql.append(JDBCSyntax.TABLE_SEPARATOR);
            }
            String tableAlias = enumeration.nextElement();
            String tableName = tables.get(tableAlias);
            if (tableAlias.equals(tableName)) {
                sql.append(_factory.quoteName(tableName));
            } else {
                sql.append(_factory.quoteName(tableName) + " "
                        + _factory.quoteName(tableAlias));
            }
        }

        // Use standard join syntax for all inner joins
        first = true;
        for (int i = 0; i < _joins.size(); ++i) {
            Join join;

            join = _joins.elementAt(i);
            if (!join._outer) {
                if (first) {
                    sql.append(JDBCSyntax.WHERE);
                    first = false;
                } else {
                    sql.append(JDBCSyntax.AND);
                }
                for (int j = 0; j < join._leftColumns.length; ++j) {
                    if (j > 0) {
                        sql.append(JDBCSyntax.AND);
                    }
                    sql.append(_factory.quoteName(join._leftTable
                            + JDBCSyntax.TABLE_COLUMN_SEPARATOR
                            + join._leftColumns[j])).append(OP_EQUALS);
                    sql.append(_factory.quoteName(join._rightTable
                            + JDBCSyntax.TABLE_COLUMN_SEPARATOR
                            + join._rightColumns[j]));
                }
            }
        }
        first = addWhereClause(sql, first);

        if (_order != null) {
            sql.append(JDBCSyntax.ORDER_BY).append(_order);
        }

        // There is no standard way to lock selected tables.
        return sql;
    }

    public final String toString() {
        StringBuffer buffer = new StringBuffer ();
        try {
            buffer.append ("<").append(getStatement(false)).append(">");
        } catch (SyntaxNotSupportedException e) {
            _log.error ("Problem turning this to a String", e);
        }
        return buffer.toString();
    }


    public final Object clone() {
        JDBCQueryExpression clone;

        try {
            clone = getClass().
                getConstructor(new Class[] {PersistenceFactory.class}).
                newInstance(new Object[] {_factory});
        } catch (Exception except) {
            // This should never happen
            throw new RuntimeException(except.toString());
        }
        clone._tables = new Hashtable<String, String>(_tables);
        clone._conds = new Vector<String>(_conds);
        clone._cols = new Vector<String>(_cols);
        clone._joins = new Vector<Join>(_joins);
        return clone;
    }


    static class Join {
        protected final String   _leftTable;

        protected final String[] _leftColumns;

        protected final String   _rightTable;

        protected final String[] _rightColumns;

        protected final boolean  _outer;

        Join(final String leftTable, final String leftColumn, final String rightTable,
                final String rightColumn, final boolean outer) {
            _leftTable = leftTable;
            _leftColumns = new String[] {leftColumn};
            _rightTable = rightTable;
            _rightColumns = new String[] {rightColumn};
            _outer = outer;
        }

        Join(final String leftTable, final String[] leftColumns, final String rightTable,
                final String[] rightColumns, final boolean outer) {
            _leftTable = leftTable;
            _leftColumns = leftColumns.clone();
            _rightTable = rightTable;
            _rightColumns = rightColumns.clone();
            _outer = outer;
        }

        public int hashCode() {
            return _leftTable.hashCode() ^ _rightTable.hashCode();
        }

        public boolean equals(final Object obj) {
            Join join = (Join) obj;

            if (!_leftTable.equals(join._leftTable) || !_rightTable.equals(join._rightTable)
                    || (_leftColumns.length != join._leftColumns.length)
                    || (_rightColumns.length != join._rightColumns.length)) {
                return false;
            }
            for (int i = 0; i < _leftColumns.length; i++) {
                if (!_leftColumns[i].equals(join._leftColumns[i])) {
                    return false;
                }
            }
            for (int i = 0; i < _rightColumns.length; i++) {
                if (!_rightColumns[i].equals(join._rightColumns[i])) {
                    return false;
                }
            }
            // [oleg] Important: Don't compare "outer" field!
            // We need this to make sure that inner join overrides outer joins,
            // we use Vector.indexOf to solve this problem.
            return true;
        }

        public String toString() {
            return _leftTable + "." + _leftColumns[0] + (_outer ? "*=" : "=")
                 + _rightTable + "." + _rightColumns[0];

        }
        
        /**
         * Indicates whether the join in question is an outer join.
         * 
         * @return True if it is an outer join.
         */
        public boolean isOuter() {
            return this._outer;
        }
    }

    /** 
     * Provides a default implementation of {@link QueryExpression#isLimitClauseSupported()}.
     * 
     * @return false to indicate that this feature is not supported by default. 
     * @see org.exolab.castor.persist.spi.QueryExpression#isLimitClauseSupported()
     */
    public boolean isLimitClauseSupported() {
        return false;
    }

    /** 
     * Provides a default implementation of {@link QueryExpression#isOffsetClauseSupported()}.
     * 
     * @return false to indicate that this feature is not supported by default. 
     * @see org.exolab.castor.persist.spi.QueryExpression#isOffsetClauseSupported()
     */
    public boolean isOffsetClauseSupported() {
        return false;
    }
}
