/*
 * 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.tomcat.jdbc.pool.interceptor;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

/**
 * Implementation of <b>JdbcInterceptor</b> that proxies resultSets and statements.
 * @author Guillermo Fernandes
 */
public class StatementDecoratorInterceptor extends AbstractCreateStatementInterceptor {

    private static final Log logger = LogFactory.getLog(StatementDecoratorInterceptor.class);

    private static final String[] EXECUTE_QUERY_TYPES = { "executeQuery" };

    /**
     * the constructors that are used to create statement proxies
     */
    protected static final Constructor<?>[] constructors = new Constructor[AbstractCreateStatementInterceptor.STATEMENT_TYPE_COUNT];

    /**
     * the constructor to create the resultSet proxies
     */
    protected static Constructor<?> resultSetConstructor = null;

    @Override
    public void closeInvoked() {
        // nothing to do
    }

    /**
     * Creates a constructor for a proxy class, if one doesn't already exist
     *
     * @param idx
     *            - the index of the constructor
     * @param clazz
     *            - the interface that the proxy will implement
     * @return - returns a constructor used to create new instances
     * @throws NoSuchMethodException
     */
    protected Constructor<?> getConstructor(int idx, Class<?> clazz) throws NoSuchMethodException {
        if (constructors[idx] == null) {
            Class<?> proxyClass = Proxy.getProxyClass(StatementDecoratorInterceptor.class.getClassLoader(),
                    new Class[] { clazz });
            constructors[idx] = proxyClass.getConstructor(new Class[] { InvocationHandler.class });
        }
        return constructors[idx];
    }

    protected Constructor<?> getResultSetConstructor() throws NoSuchMethodException {
        if (resultSetConstructor == null) {
            Class<?> proxyClass = Proxy.getProxyClass(StatementDecoratorInterceptor.class.getClassLoader(),
                    new Class[] { ResultSet.class });
            resultSetConstructor = proxyClass.getConstructor(new Class[] { InvocationHandler.class });
        }
        return resultSetConstructor;
    }

    /**
     * Creates a statement interceptor to monitor query response times
     */
    @Override
    public Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time) {
        try {
            String name = method.getName();
            Constructor<?> constructor = null;
            String sql = null;
            if (compare(CREATE_STATEMENT, name)) {
                // createStatement
                constructor = getConstructor(CREATE_STATEMENT_IDX, Statement.class);
            } else if (compare(PREPARE_STATEMENT, name)) {
                // prepareStatement
                constructor = getConstructor(PREPARE_STATEMENT_IDX, PreparedStatement.class);
                sql = (String)args[0];
            } else if (compare(PREPARE_CALL, name)) {
                // prepareCall
                constructor = getConstructor(PREPARE_CALL_IDX, CallableStatement.class);
                sql = (String)args[0];
            } else {
                // do nothing, might be a future unsupported method
                // so we better bail out and let the system continue
                return statement;
            }
            return createDecorator(proxy, method, args, statement, constructor, sql);
        } catch (Exception x) {
            if (x instanceof InvocationTargetException) {
                Throwable cause = x.getCause();
                if (cause instanceof ThreadDeath) {
                    throw (ThreadDeath) cause;
                }
                if (cause instanceof VirtualMachineError) {
                    throw (VirtualMachineError) cause;
                }
            }
            logger.warn("Unable to create statement proxy for slow query report.", x);
        }
        return statement;
    }

    protected Object createDecorator(Object proxy, Method method, Object[] args,
                                     Object statement, Constructor<?> constructor, String sql)
    throws InstantiationException, IllegalAccessException, InvocationTargetException {
        Object result = null;
        StatementProxy<Statement> statementProxy =
                new StatementProxy<Statement>((Statement)statement,sql);
        result = constructor.newInstance(new Object[] { statementProxy });
        statementProxy.setActualProxy(result);
        statementProxy.setConnection(proxy);
        statementProxy.setConstructor(constructor);
        return result;
    }

    protected boolean isExecuteQuery(String methodName) {
        return EXECUTE_QUERY_TYPES[0].equals(methodName);
    }

    protected boolean isExecuteQuery(Method method) {
        return isExecuteQuery(method.getName());
    }

    /**
     * Class to measure query execute time
     *
     * @author fhanik
     *
     */
    protected class StatementProxy<T extends java.sql.Statement> implements InvocationHandler {

        protected boolean closed = false;
        protected T delegate;
        private Object actualProxy;
        private Object connection;
        private String sql;
        private Constructor<?> constructor;

        public StatementProxy(T delegate, String sql) {
            this.delegate = delegate;
            this.sql = sql;
        }
        public T getDelegate() {
            return this.delegate;
        }

        public String getSql() {
            return sql;
        }

        public void setConnection(Object proxy) {
            this.connection = proxy;
        }
        public Object getConnection() {
            return this.connection;
        }

        public void setActualProxy(Object proxy){
            this.actualProxy = proxy;
        }
        public Object getActualProxy() {
            return this.actualProxy;
        }


        public Constructor<?> getConstructor() {
            return constructor;
        }
        public void setConstructor(Constructor<?> constructor) {
            this.constructor = constructor;
        }
        public void closeInvoked() {
            if (getDelegate()!=null) {
                try {
                    getDelegate().close();
                }catch (SQLException ignore) {
                }
            }
            closed = true;
            delegate = null;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (compare(TOSTRING_VAL,method)) {
                return toString();
            }
            // was close invoked?
            boolean close = compare(CLOSE_VAL, method);
            // allow close to be called multiple times
            if (close && closed)
                return null;
            // are we calling isClosed?
            if (compare(ISCLOSED_VAL, method))
                return Boolean.valueOf(closed);
            // if we are calling anything else, bail out
            if (closed)
                throw new SQLException("Statement closed.");
            if (compare(GETCONNECTION_VAL,method)){
                return connection;
            }
            boolean process = isExecuteQuery(method);
            // check to see if we are about to execute a query
            // if we are executing, get the current time
            Object result = null;
            try {
                // perform close cleanup
                if (close) {
                    closeInvoked();
                } else {
                    // execute the query
                    result = method.invoke(delegate, args);
                }
            } catch (Throwable t) {
                if (t instanceof InvocationTargetException
                        && t.getCause() != null) {
                    throw t.getCause();
                } else {
                    throw t;
                }
            }
            if (process){
                Constructor<?> cons = getResultSetConstructor();
                result = cons.newInstance(new Object[]{new ResultSetProxy(actualProxy, result)});
            }
            return result;
        }

        @Override
        public String toString() {
            StringBuffer buf = new StringBuffer(StatementProxy.class.getName());
            buf.append("[Proxy=");
            buf.append(System.identityHashCode(this));
            buf.append("; Sql=");
            buf.append(getSql());
            buf.append("; Delegate=");
            buf.append(getDelegate());
            buf.append("; Connection=");
            buf.append(getConnection());
            buf.append("]");
            return buf.toString();
        }
    }

    protected class ResultSetProxy implements InvocationHandler {

        private Object st;
        private Object delegate;

        public ResultSetProxy(Object st, Object delegate) {
            this.st = st;
            this.delegate = delegate;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (method.getName().equals("getStatement")) {
                return this.st;
            } else {
                try {
                    return method.invoke(this.delegate, args);
                } catch (Throwable t) {
                    if (t instanceof InvocationTargetException
                            && t.getCause() != null) {
                        throw t.getCause();
                    } else {
                        throw t;
                    }
                }
            }
        }
    }
}
