/*
 *  Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; version 2 of the License.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
 */

package com.mysql.clusterj.jdbc;

import com.mysql.clusterj.ClusterJHelper;
import com.mysql.clusterj.ClusterJUserException;
import com.mysql.clusterj.SessionFactory;
import com.mysql.clusterj.core.query.QueryDomainTypeImpl;
import com.mysql.clusterj.core.spi.SessionSPI;
import com.mysql.clusterj.core.store.Dictionary;
import com.mysql.clusterj.core.util.I18NHelper;
import com.mysql.clusterj.core.util.Logger;
import com.mysql.clusterj.core.util.LoggerFactoryService;
import com.mysql.jdbc.Connection;
import com.mysql.jdbc.ResultSetInternalMethods;
import com.mysql.jdbc.Statement;
import com.mysql.clusterj.jdbc.antlr.ANTLRNoCaseStringStream;
import com.mysql.clusterj.jdbc.antlr.MySQL51Parser;
import com.mysql.clusterj.jdbc.antlr.MySQL51Lexer;
import com.mysql.clusterj.jdbc.antlr.QueuingErrorListener;
import com.mysql.clusterj.jdbc.antlr.node.Node;
import com.mysql.clusterj.jdbc.antlr.node.PlaceholderNode;
import com.mysql.clusterj.jdbc.antlr.node.SelectNode;
import com.mysql.clusterj.jdbc.antlr.node.WhereNode;
import com.mysql.clusterj.query.Predicate;

import com.mysql.clusterj.jdbc.SQLExecutor.Executor;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.Token;
import org.antlr.runtime.TokenStream;
import org.antlr.runtime.tree.CommonErrorNode;
import org.antlr.runtime.tree.CommonTree;
import org.antlr.runtime.tree.CommonTreeAdaptor;
import org.antlr.runtime.tree.TreeAdaptor;

/** This class implements the behavior associated with connection callbacks for statement execution
 * and connection lifecycle. There is a clusterj session associated with the interceptor that
 * is used to interact with the cluster. There is exactly one statement interceptor and one
 * connection lifecycle interceptor associated with the interceptor.
 * All of the SQL post-parsing behavior is contained here, and uses classes in the org.antlr.runtime and
 * com.mysql.clusterj.jdbc.antlr packages to perform the parsing of the SQL statement. Analysis
 * of the parsed SQL statement occurs here, and clusterj artifacts are constructed for use in
 * other classes, in particular SQLExecutor and its command-specific subclasses.
 */
public class InterceptorImpl {

    /** Register logger for JDBC stuff. */
    static {
        LoggerFactoryService.getFactory().registerLogger("com.mysql.clusterj.jdbc");
    }

    /** My message translator */
    static final I18NHelper local = I18NHelper.getInstance(InterceptorImpl.class);

    /** My logger */
    static final Logger logger = LoggerFactoryService.getFactory().getInstance(InterceptorImpl.class);

    static Map<String, Executor> parsedSqlMap = new IdentityHashMap<String, Executor>();

    /** The map of connection to interceptor */
    private static Map<Connection, InterceptorImpl> interceptorImplMap =
            new IdentityHashMap<Connection, InterceptorImpl>();

    /** The connection properties */
    private Properties properties;

    /** The connection being intercepted */
    private Connection connection;

    /** The session factory for this connection */
    SessionFactory sessionFactory;

    /** The current session (null if no session) */
    private SessionSPI session;

    /** The statement interceptor (only used during initialization) */
    private StatementInterceptor statementInterceptor;

    /** The connection lifecycle interceptor (only used during initialization) */
    private ConnectionLifecycleInterceptor connectionLifecycleInterceptor;

    /** The interceptor is ready (both interceptors are registered) */
    private boolean ready = false;

    private boolean autocommit;

    private static String LOTSOBLANKS = "                                                                          "; 

    /** Create the interceptor.
     * 
     * @param connection the connection being intercepted
     * @param properties the connection properties
     */
    public InterceptorImpl(Connection connection, Properties properties) {
        if (logger.isDebugEnabled()) logger.debug("constructed with properties: " + properties);
        this.properties = properties;
        this.connection = connection;
        // if database name is not specified, translate DBNAME to the required clusterj property
        String dbname = properties.getProperty("com.mysql.clusterj.database",
                properties.getProperty("DBNAME"));
        properties.put("com.mysql.clusterj.database", dbname);
    }

    /** Return the interceptor for the connection lifecycle callbacks.
     * 
     * @param connectionLifecycleInterceptor the connection lifecycle interceptor
     * @param connection the connection
     * @param properties the connection properties
     * @return the interceptor delegate
     */
    public static InterceptorImpl getInterceptorImpl(
            ConnectionLifecycleInterceptor connectionLifecycleInterceptor,
            Connection connection, Properties properties) {
        InterceptorImpl result = getInterceptorImpl(connection, properties);
        if (result.connectionLifecycleInterceptor != null) {
            if (result.connectionLifecycleInterceptor != connectionLifecycleInterceptor) {
                throw new ClusterJUserException(
                        local.message("ERR_Duplicate_Connection_Lifecycle_Interceptor"));
            }
        } else {
            result.connectionLifecycleInterceptor = connectionLifecycleInterceptor;
        }
        if (result.statementInterceptor != null) {
            result.ready = true;
        }
        return result;
    }

    /** Return the interceptor for the statement interceptor callbacks.
     * 
     * @param statementInterceptor the statement interceptor
     * @param connection the connection
     * @param properties the connection properties
     * @return the interceptor delegate
     */
    public static InterceptorImpl getInterceptorImpl(
            StatementInterceptor statementInterceptor, Connection connection,
            Properties properties) {
        InterceptorImpl result = getInterceptorImpl(connection, properties);
        if (result.statementInterceptor != null) {
            throw new ClusterJUserException(
                    local.message("ERR_Duplicate_Statement_Interceptor"));
        }
        result.statementInterceptor = statementInterceptor;
        if (result.connectionLifecycleInterceptor != null) {
            result.ready = true;
        }
        return result;
    }

    /** Create the interceptor to handle both connection lifecycle and statement interceptors.
     * 
     * @param connection the connection
     * @param properties the connection properties
     * @return
     */
    public static InterceptorImpl getInterceptorImpl(Connection connection, Properties properties) {
        InterceptorImpl result;
        synchronized(interceptorImplMap) {
            result = interceptorImplMap.get(connection);
            if (result == null) {
                result = new InterceptorImpl(connection, properties);
                interceptorImplMap.put(connection, result);
            }
        }
        return result;
    }

    /** Return the interceptor assigned to the connection. If there is no interceptor, return null.
     * 
     * @param connection the connection
     * @return the interceptor for this connection or null if there is no interceptor
     */
    public static InterceptorImpl getInterceptorImpl(java.sql.Connection connection) {
        synchronized (interceptorImplMap) {
            return interceptorImplMap.get(connection);
        }
    }

    @Override
    public String toString() {
        return "InterceptorImpl "
//        + " properties: "+ properties.toString()
        ;
    }

    void destroy() {
        if (sessionFactory != null) {
            if (session != null) {
                session.close();
            }
            sessionFactory.close();
            sessionFactory = null;
            synchronized(interceptorImplMap) {
                interceptorImplMap.remove(connection);
            }
        }
    }

    public SessionSPI getSession() {
        if (session == null) {
            session = (SessionSPI)sessionFactory.getSession();
        }
        return session;
    }

    public boolean executeTopLevelOnly() {
//        assertReady();
        boolean result = true;
        return result;
    }

    public ResultSetInternalMethods postProcess(String sql, Statement statement,
            ResultSetInternalMethods result, Connection connection, int arg4,
            boolean arg5, boolean arg6, SQLException sqlException) throws SQLException {
        assertReady();
        return null;
    }

    public ResultSetInternalMethods preProcess(String sql, Statement statement,
            Connection connection) throws SQLException {
        assertReady();
        if (statement instanceof com.mysql.jdbc.PreparedStatement) {
            com.mysql.jdbc.PreparedStatement preparedStatement =
                (com.mysql.jdbc.PreparedStatement)statement;
            // key must be interned because we are using IdentityHashMap
            String preparedSql = preparedStatement.getPreparedSql().intern();
            // see if we have a parsed version of this query
            Executor sQLExecutor = null;
            synchronized(parsedSqlMap) {
                sQLExecutor = parsedSqlMap.get(preparedSql);
            }
            // if no cached SQLExecutor, create it, which might take some time
            if (sQLExecutor == null) {
                sQLExecutor = createSQLExecutor(preparedSql);
                if (sQLExecutor != null) {
                    // multiple thread might have created a SQLExecutor but it's ok
                    synchronized(parsedSqlMap) {
                        parsedSqlMap.put(preparedSql, sQLExecutor);
                    }
                }
            }
            return sQLExecutor.execute(this, preparedStatement.getParameterBindings());
        }
        return null;
    }

    /**
     * @param preparedSql
     */
    private Executor createSQLExecutor(String preparedSql) {
        if (logger.isDetailEnabled()) logger.detail(preparedSql);
        Executor result = null;
        // parse the sql
        CommonTree root = parse(preparedSql);
        // get the root of the tree
        int tokenType = root.getType();
        // perform command-specific actions
        String tableName = "";
        CommonTree tableNode;
        WhereNode whereNode;
        List<String> columnNames = new ArrayList<String>();
        Dictionary dictionary;
        DomainTypeHandlerImpl<?> domainTypeHandler;
        QueryDomainTypeImpl<?> queryDomainType = null;
        switch (tokenType) {
            case MySQL51Parser.INSERT:
                tableNode = (CommonTree)root.getFirstChildWithType(MySQL51Parser.TABLE);
                tableName = getTableName(tableNode);
                getSession();
                dictionary = session.getDictionary();
                domainTypeHandler = getDomainTypeHandler(tableName, dictionary);
                CommonTree insertValuesNode = (CommonTree)root.getFirstChildWithType(MySQL51Parser.INSERT_VALUES);
                CommonTree columnsNode = (CommonTree)insertValuesNode.getFirstChildWithType(MySQL51Parser.COLUMNS);
                List<CommonTree> fields = columnsNode.getChildren();
                for (CommonTree field: fields) {
                    columnNames.add(getColumnName(field));
                }
                if (logger.isDetailEnabled()) logger.detail(
                        "StatementInterceptorImpl.preProcess parse result INSERT INTO " + tableName
                        + " COLUMNS " + columnNames);
                result = new SQLExecutor.Insert(domainTypeHandler, columnNames);
                break;
            case MySQL51Parser.SELECT:
                CommonTree fromNode = (CommonTree)root.getFirstChildWithType(MySQL51Parser.FROM);
                if (fromNode == null) {
                    // no from clause; cannot handle this case so return a do-nothing ParsedSQL
                    result = new SQLExecutor.Noop();
                    break;
                }
                try {
                    // this currently handles only FROM clauses with a single table
                    tableNode = (CommonTree) fromNode.getFirstChildWithType(MySQL51Parser.TABLE);
                    tableName = getTableName(tableNode);
                } catch (Exception e) {
                    // trouble with the FROM clause; log the SQL statement and the parser output
                    logger.info("Problem with FROM clause in SQL statement: " + preparedSql);
                    logger.info(walk(root));
                    result = new SQLExecutor.Noop();
                    break;
                }
                getSession();
                dictionary = session.getDictionary();
                domainTypeHandler = getDomainTypeHandler(tableName, dictionary);
                columnsNode = (CommonTree)root.getFirstChildWithType(MySQL51Parser.COLUMNS);
                List<CommonTree> selectExprNodes = columnsNode.getChildren();
                for (CommonTree selectExprNode: selectExprNodes) {
                    columnNames.add(getColumnName(getFieldNode(selectExprNode)));
                }
                String whereType = "empty";
                if (logger.isDetailEnabled()) logger.detail(
                        "SELECT FROM " + tableName
                        + " COLUMNS " + columnNames);
                // we need to distinguish three cases:
                // - no where clause (select all rows)
                // - where clause that cannot be executed by clusterj
                // - where clause that can be executed by clusterj
                whereNode = ((SelectNode)root).getWhereNode();
                queryDomainType = (QueryDomainTypeImpl<?>) session.createQueryDomainType(domainTypeHandler);
                if (whereNode == null) {
                    // no where clause (select all rows)
                    result = new SQLExecutor.Select(domainTypeHandler, columnNames, queryDomainType);
                } else {
                    // create a predicate from the tree
                    Predicate predicate = whereNode.getPredicate(queryDomainType);
                    if (predicate != null) {
                        // where clause that can be executed by clusterj
                        queryDomainType.where(predicate);
                        result = new SQLExecutor.Select(domainTypeHandler, columnNames, queryDomainType);
                        whereType = "clusterj";
                    } else {
                        // where clause that cannot be executed by clusterj
                        result = new SQLExecutor.Noop();
                        whereType = "non-clusterj";
                    }
                    if (logger.isDetailEnabled()) logger.detail(walk(root));
                }
                if (logger.isDetailEnabled()) {
                    logger.detail(
                        "SELECT FROM " + tableName
                        + " COLUMNS " + columnNames + " whereType " + whereType);
                    logger.detail(walk(root));
                }
                break;
            case MySQL51Parser.DELETE:
                tableNode = (CommonTree)root.getFirstChildWithType(MySQL51Parser.TABLE);
                tableName = getTableName(tableNode);
                getSession();
                dictionary = session.getDictionary();
                domainTypeHandler = getDomainTypeHandler(tableName, dictionary);
                whereNode = ((WhereNode)root.getFirstChildWithType(MySQL51Parser.WHERE));
                int numberOfParameters = 0;
                if (whereNode == null) {
                    // no where clause (delete all rows)
                    result = new SQLExecutor.Delete(domainTypeHandler);
                    whereType = "empty";
                } else {
                    // create a predicate from the tree
                    queryDomainType = (QueryDomainTypeImpl<?>) session.createQueryDomainType(domainTypeHandler);
                    Predicate predicate = whereNode.getPredicate(queryDomainType);
                    if (predicate != null) {
                        // where clause that can be executed by clusterj
                        queryDomainType.where(predicate);
                        numberOfParameters = whereNode.getNumberOfParameters();
                        result = new SQLExecutor.Delete(domainTypeHandler, queryDomainType, numberOfParameters);
                        whereType = "clusterj";
                    } else {
                        // where clause that cannot be executed by clusterj
                        result = new SQLExecutor.Noop();
                        whereType = "non-clusterj";
                    }
                    if (logger.isDetailEnabled()) logger.detail(walk(root));
                }
                if (logger.isDetailEnabled()) logger.detail(
                        "DELETE FROM " + tableName
                        + " whereType " + whereType
                        + " number of parameters " + numberOfParameters);
                break;
            default:
                // return a do-nothing ParsedSQL
                if (logger.isDetailEnabled()) logger.detail("ClusterJ cannot process this SQL statement: unsupported statement type.");
                result = new SQLExecutor.Noop();
        }
        return result;
    }

    private String getPrimaryKeyFieldName(CommonTree whereNode) {
        String result = null;
        CommonTree operation = (CommonTree) whereNode.getChild(0);
        if (MySQL51Parser.EQUALS == operation.getType()) {
            result = operation.getChild(0).getChild(0).getText();
        } else {
            throw new ClusterJUserException("Cannot find primary key in WHERE clause.");
        }
        return result;
    }

    private String walk(CommonTree tree) {
        StringBuilder buffer = new StringBuilder();
        walk(tree, buffer, 0);
        return buffer.toString();
    }

    @SuppressWarnings("unchecked") // tree.getChildren()
    private void walk(CommonTree tree, StringBuilder buffer, int level) {
            String indent = LOTSOBLANKS.substring(0, level);
            Token token = tree.token;
            int tokenType = token.getType();
            String tokenText = token.getText();
            int childCount = tree.getChildCount();
            int childIndex = tree.getChildIndex();
            buffer.append('\n');
            buffer.append(indent);
            buffer.append(tokenText);
            buffer.append(" class: ");
            buffer.append(tree.getClass().getName());
            buffer.append(" tokenType ");
            buffer.append(tokenType);
            buffer.append(" child count ");
            buffer.append(childCount);
            buffer.append(" child index ");
            buffer.append(childIndex);
            List<CommonTree> children = tree.getChildren();
            if (children == null) {
                return;
            }
            for (CommonTree child: children) {
                walk(child, buffer, level + 2);
            }
    }

    private CommonTree parse(String preparedSql) {
        CommonTree result = null;
        ANTLRNoCaseStringStream inputStream = new ANTLRNoCaseStringStream(preparedSql);
        MySQL51Lexer lexer = new MySQL51Lexer(inputStream);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        lexer.setErrorListener(new QueuingErrorListener(lexer));
        tokens.getTokens();
        if (lexer.getErrorListener().hasErrors()) {
            logger.warn(local.message("ERR_Lexing_SQ",preparedSql));
            return result;
        }
        PlaceholderNode.resetId();
        MySQL51Parser parser = new MySQL51Parser(tokens);
        parser.setTreeAdaptor(mySQLTreeAdaptor);
        parser.setErrorListener(new QueuingErrorListener(parser));
        try {
            CommonTree stmtTree = (CommonTree) parser.statement().getTree();
            result = stmtTree;
        } catch (RecognitionException e) {
            logger.warn(local.message("ERR_Parsing_SQL", preparedSql));
        }
        if (parser.getErrorListener().hasErrors()) {
            logger.warn(local.message("ERR_Parsing_SQL", preparedSql));
        }
        return result;
    }

    private TreeAdaptor mySQLTreeAdaptor = new CommonTreeAdaptor() {
        public Object create(Token token) { return new Node(token); }
        public Object dupNode(Object t) {
            if ( t==null ) return null;
            return create(((Node)t).token);
        }
    };

    private String getTableName(CommonTree tableNode) {
        return tableNode.getChild(0).getText();
    }

    private String getColumnName(CommonTree fieldNode) {
        return fieldNode.getChild(0).getText();
    }

    private CommonTree getFieldNode(CommonTree selectExprNode) {
        return (CommonTree)selectExprNode.getChild(0);
    }

    public void destroy(StatementInterceptor statementInterceptor) {
    }

    public void destroy(
            ConnectionLifecycleInterceptor connectionLifecycleInterceptor) {
    }

    private void assertReady() {
        if (!ready) {
            if (statementInterceptor == null) {
                throw new ClusterJUserException(local.message("ERR_No_Statement_Interceptor"));
            }
            if (connectionLifecycleInterceptor == null) {
                throw new ClusterJUserException(local.message("ERR_No_Connection_Lifecycle_Interceptor"));
            }
        } else {
            if (sessionFactory == null) {
                sessionFactory = ClusterJHelper.getSessionFactory(properties);
            }
        }
    }

    /** TODO This needs to be rewritten with a proper state machine. */
    public boolean setAutoCommit(boolean autocommit) throws SQLException {
        assertReady();
        logStatus("setAutoCommit(" + autocommit + ")");
        this.autocommit = autocommit;
        getSession();
        if (!autocommit) {
            // start a transaction
            if (!session.currentTransaction().isActive()) {
                session.begin();
            }
        } else {
            // roll back the previous transaction if active
            if (session.currentTransaction().isActive()) {
                session.rollback();
            }
        }
        return true; // let the driver perform its own autocommit behavior
    }

    public void close() {
    }

    public boolean commit() throws SQLException {
        logStatus("commit");
        if (session.currentTransaction().isActive()) {
            session.commit();
        } else {
            System.out.println("WARNING: commit called when session.transaction is not active");
        }
        session.begin();
        return true;
    }

    public boolean rollback() throws SQLException {
        logStatus("rollback");
        session.rollback();
        session.begin();
        return true;
    }

    public boolean rollback(Savepoint savepoint) throws SQLException {
        logStatus("rollback(Savepoint)");
        return true;
    }

    public boolean setCatalog(String catalog) throws SQLException {
        if (logger.isDebugEnabled()) logger.debug("catalog: " + catalog);
        return true;
    }

    public boolean transactionCompleted() throws SQLException {
        logStatus("transactionCompleted");
        return true;
    }

    public boolean transactionBegun() throws SQLException {
        logStatus("transactionBegun");
        return true;
    }

    private DomainTypeHandlerImpl<?> getDomainTypeHandler(String tableName, Dictionary dictionary) {
        DomainTypeHandlerImpl<?> domainTypeHandler = 
            DomainTypeHandlerImpl.getDomainTypeHandler(tableName, dictionary);
        return domainTypeHandler;
    }

    private void logStatus(String s) throws SQLException {
        if (logger.isDetailEnabled()) {
            StringBuilder builder = new StringBuilder("In ");
            builder.append(s);
            builder.append(" with");
            if (connection != null) {
                builder.append(" connection.getAutocommit: " + connection.getAutoCommit());
            }
            if (session != null) {
                builder.append(" session.isActive: " + session.currentTransaction().isActive());
            }
            builder.append('\n');
            String message = builder.toString();
            logger.detail(message);
        }
    }

}
