/**
 * Copyright 2013 Simeon Malchev
 *
 * Licensed 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.vibur.dbcp.proxy;

import org.vibur.dbcp.ViburConfig;
import org.vibur.dbcp.pool.Hook;
import org.vibur.dbcp.pool.HookHolder.InvocationHooksAccessor;
import org.vibur.dbcp.stcache.StatementCache;
import org.vibur.dbcp.stcache.StatementHolder;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

import static org.vibur.dbcp.proxy.Proxy.newProxyResultSet;
import static org.vibur.dbcp.util.JdbcUtils.quietClose;

/**
 * @author Simeon Malchev
 */
class StatementInvocationHandler extends ChildObjectInvocationHandler<Connection, Statement>
        implements Hook.StatementProceedingPoint {

    private final StatementHolder statement;
    private final StatementCache statementCache; // always "null" (i.e. turned off) for simple JDBC Statements
    private final ViburConfig config;
    private final Deque<ResultSet> currentResultSets = new ArrayDeque<>();

    private final Hook.StatementExecution[] executionHooks;
    private final Hook.StatementExecution firstHook;
    private int hookIdx = 0;

    private final boolean logSqlQueryParams;
    private final List<Object[]> sqlQueryParams;

    StatementInvocationHandler(StatementHolder statement, StatementCache statementCache, Connection connProxy,
                               ViburConfig config, ExceptionCollector exceptionCollector) {
        super(statement.rawStatement(), connProxy, "getConnection", config, exceptionCollector);
        this.statement = statement;
        this.statementCache = statementCache;
        this.config = config;

        InvocationHooksAccessor invocationHooksAccessor = (InvocationHooksAccessor) config.getInvocationHooks();
        this.executionHooks = invocationHooksAccessor.onStatementExecution();
        this.firstHook = executionHooks.length > 0 ? executionHooks[0] : this;

        this.logSqlQueryParams = config.isIncludeQueryParameters() &&
                (executionHooks.length > 0 || invocationHooksAccessor.onResultSetRetrieval().length > 0);
        this.sqlQueryParams = logSqlQueryParams ? new ArrayList<Object[]>() : null;
    }

    @Override
    Object unrestrictedInvoke(Statement proxy, Method method, Object[] args) throws SQLException {
        String methodName = method.getName();

        if (methodName == "close") {
            return processClose(method, args);
        }
        if (methodName == "isClosed") {
            return isClosed();
        }

        return super.unrestrictedInvoke(proxy, method, args);
    }

    @Override
    Object restrictedInvoke(Statement proxy, Method method, Object[] args) throws SQLException {
        String methodName = method.getName();

        if (methodName.startsWith("set")) { // this intercepts all "set..." JDBC Prepared/Callable Statement methods
            return processSet(method, args);
        }
        if (methodName.startsWith("execute")) { // this intercepts all "execute..." JDBC Statement methods
            return processExecute(proxy, method, args);
        }

        if (methodName == "getMoreResults") { // *2
            return processMoreResults(method, args);
        }

        // Methods which results have to be proxied so that when getStatement() is called
        // on their results the return value to be the current JDBC Statement proxy.
        if (methodName == "getResultSet" || methodName == "getGeneratedKeys") { // *2
            return newProxiedResultSet(proxy, method, args, statement.getSqlQuery());
        }

        if (methodName == "cancel") {
            return processCancel(method, args);
        }

        return super.restrictedInvoke(proxy, method, args);
    }

    private Object processClose(Method method, Object[] args) throws SQLException {
        if (!close()) {
            return null;
        }

        closeAllResultSets();

        if (statementCache != null && statementCache.restore(statement, config.isClearSQLWarnings())) {
            return null; // calls to close() are not passed when the statement is restored successfully in the cache
        }
        return targetInvoke(method, args);
    }

    private Object processCancel(Method method, Object[] args) throws SQLException {
        if (statementCache != null) {
            statementCache.remove(statement); // because cancelled Statements are not longer valid
        }
        return targetInvoke(method, args);
    }

    private Object processSet(Method method, Object[] args) throws SQLException {
        if (logSqlQueryParams && args != null && args.length >= 2) {
            addSqlQueryParams(method, args);
        }
        return targetInvoke(method, args); // the real "set..." call
    }

    private Object processExecute(Statement proxy, Method method, Object[] args) throws SQLException {
        closeAllResultSets();

        if (statement.getSqlQuery() == null && args != null && args.length >= 1) { // a simple Statement "execute..." call
            statement.setSqlQuery((String) args[0]);
        }

        try {
            return firstHook.on(proxy, method, args, statement.getSqlQuery(), sqlQueryParams, this); // see the SPP implementation below
        } finally {
            prepareForNextExecution();
        }
    }

    private Object processMoreResults(Method method, Object[] args) throws SQLException {
        int current = Statement.CLOSE_ALL_RESULTS;
        if (args != null && args.length == 1) {
            current = (Integer) args[0];
        }

        if (current == Statement.CLOSE_CURRENT_RESULT) {
            quietClose(currentResultSets.pollLast());
        }
        else if (current == Statement.CLOSE_ALL_RESULTS) {
            closeAllResultSets();
        }

        return targetInvoke(method, args);
    }

    private void prepareForNextExecution() {
        if (sqlQueryParams != null) {
            sqlQueryParams.clear();
        }
        hookIdx = 0;
    }

    private ResultSet newProxiedResultSet(Statement proxy, Method method, Object[] args, String sqlQuery) throws SQLException {
        ResultSet rawResultSet = (ResultSet) targetInvoke(method, args);
        return addResultSet(newProxyResultSet(rawResultSet, proxy, sqlQuery, sqlQueryParams, config, this));
    }

    private void addSqlQueryParams(Method method, Object[] args) {
        Object[] params = new Object[args.length + 1];
        params[0] = method.getName().substring(3); // "set".length() == 3
        System.arraycopy(args, 0, params, 1, args.length);
        sqlQueryParams.add(params);
    }

    private ResultSet addResultSet(ResultSet resultSet) {
        if (resultSet != null) {
            currentResultSets.addLast(resultSet);
        }
        return resultSet;
    }

    private void closeAllResultSets() {
        ResultSet next;
        while ((next = currentResultSets.pollFirst()) != null) {
            quietClose(next);
        }
    }

    //////// The StatementProceedingPoint implementation: ////////

    @Override
    public Object on(Statement proxy, Method method, Object[] args, String sqlQuery, List<Object[]> sqlQueryParams,
                     StatementProceedingPoint proceed) throws SQLException {

        if (++hookIdx < executionHooks.length) { // invoke the next statement execution hook, if any
            return executionHooks[hookIdx].on(proxy, method, args, sqlQuery, sqlQueryParams, this);
        }

        return doProcessExecute(proxy, method, args);
    }

    private Object doProcessExecute(Statement proxy, Method method, Object[] args) throws SQLException {
        // executeQuery result has to be proxied so that when getStatement() is called
        // on its result the return value to be the current JDBC Statement proxy.
        if (method.getName() == "executeQuery") { // *1
            return newProxiedResultSet(proxy, method, args, statement.getSqlQuery());
        }

        return targetInvoke(method, args); // the real "execute..." call
    }
}