/*
 * Copyright 2014 NAVER Corp.
 *
 * 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 com.navercorp.pinpoint.web.dao.ibatis;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Properties;

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

/**
 * Plugin for printing out the bind variables of {@link java.sql.PreparedStatement} and {@link java.sql.CallableStatement} with Query string.
 * format of Query string can be changed with {@link BindLogFormatter}.
 * base implementation is {@link com.navercorp.pinpoint.web.dao.ibatis.DefaultBindingLogFormatter}.
 * removeWhitespace option is supported
 *
 * @author emeroad
 */
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}
)
public class BindingLogPlugin32 implements Interceptor {

    private static final Log pLogger = LogFactory.getLog(PreparedStatement.class);
    private static final Log internalLogger = LogFactory.getLog(BindingLogPlugin32.class);

    private BindLogFormatter bindLogFormatter;

    public BindingLogPlugin32() {
        this.bindLogFormatter = new DefaultBindingLogFormatter();
    }

    public BindingLogPlugin32(BindLogFormatter bindLogFormatter) {
        this.bindLogFormatter = Objects.requireNonNull(bindLogFormatter, "bindLogFormatter");
    }


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (internalLogger.isTraceEnabled()) {
            internalLogger.trace("target:" + invocation.getTarget()
                    + " method:" + invocation.getMethod() + " args:" + Arrays.toString(invocation.getArgs()));
        }
        try {
            return invocation.proceed();
        } finally {
            bindingLog(invocation);
        }
    }

    private void bindingLog(Invocation invocation) throws SQLException {

        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameterObject = args[1];
        StatementType statementType = ms.getStatementType();
        if (StatementType.PREPARED == statementType || StatementType.CALLABLE == statementType) {
            Log statementLog = ms.getStatementLog();
            if (isDebugEnable(statementLog)) {
                BoundSql boundSql = ms.getBoundSql(parameterObject);

                String sql = boundSql.getSql();
                List<String> parameterList = getParameters(ms, parameterObject, boundSql);
                debug(statementLog, "==> BindingLog: " + bindLogFormatter.format(sql, parameterList));
            }
        }
    }

    public boolean isDebugEnable(Log statementLogger) {
        return statementLogger.isDebugEnabled() || pLogger.isDebugEnabled();
    }

    public void debug(Log statementLogger, String msg) {
        if (statementLogger.isDebugEnabled()) {
            statementLogger.debug(msg);
        } else {
            pLogger.debug(msg);
        }
    }

    private List<String> getParameters(MappedStatement ms, Object parameterObject, BoundSql boundSql) throws SQLException {
        // DefaultParameterHandler is the only implementation of parameterHandler interface currently. it may be changed later.
        // need additional codes to find a appropriate implementation in that case.
        ParameterHandler parameterHandler = new DefaultParameterHandler(ms, parameterObject, boundSql);
        PreparedStatementParameterLogger parameterLogger = new PreparedStatementParameterLogger();
        parameterHandler.setParameters(parameterLogger);
        return parameterLogger.getParameters();
    }


    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * {@link com.navercorp.pinpoint.web.dao.ibatis.DefaultBindingLogFormatter} is the implementation of {@link BindLogFormatter} supports removeWhitespace option.
     * removeWhitespace : setting for printing out query format in a row.
     *
     * @param properties option properties
     */
    @Override
    public void setProperties(Properties properties) {
        bindLogFormatter.setProperties(properties);
    }

    public void setBindLogFormatter(BindLogFormatter bindLogFormatter) {
        this.bindLogFormatter = Objects.requireNonNull(bindLogFormatter, "bindLogFormatter");
    }
}