package io.github.tramchamploo.bufferslayer;

import com.google.common.annotations.VisibleForTesting;
import io.github.tramchamploo.bufferslayer.Sql.Builder;
import io.github.tramchamploo.bufferslayer.internal.MessageFuture;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.CallableStatementCallback;
import org.springframework.jdbc.core.CallableStatementCreator;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter;
import org.springframework.jdbc.core.PreparedStatementCallback;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.core.SqlProvider;
import org.springframework.jdbc.core.StatementCallback;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.jdbc.support.SQLExceptionTranslator;
import org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor;
import org.springframework.jdbc.support.rowset.SqlRowSet;

/**
 * JdbcTemplate that transforms normal updates to batched ones.
 */
public class BatchJdbcTemplate {

  private final JdbcTemplate delegate;
  private final Reporter<Sql, Integer> reporter;

  @VisibleForTesting
  BatchJdbcTemplate(JdbcTemplate delegate, final Reporter<Sql, Integer> reporter) {
    this.delegate = delegate;
    this.reporter = reporter;
    // Close when shutdown
    Runtime.getRuntime().addShutdownHook(new Thread() {
      @Override
      public void run() {
        try {
          reporter.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    });
  }

  @SuppressWarnings("unchecked")
  public BatchJdbcTemplate(JdbcTemplate delegate, ReporterProperties properties) {
    this.delegate = delegate;
    this.reporter = properties.toBuilder(new JdbcTemplateSender(delegate)).build();
  }

  public void setNativeJdbcExtractor(NativeJdbcExtractor extractor) {
    delegate.setNativeJdbcExtractor(extractor);
  }

  public NativeJdbcExtractor getNativeJdbcExtractor() {
    return delegate.getNativeJdbcExtractor();
  }

  public void setIgnoreWarnings(boolean ignoreWarnings) {
    delegate.setIgnoreWarnings(ignoreWarnings);
  }

  public boolean isIgnoreWarnings() {
    return delegate.isIgnoreWarnings();
  }

  public void setFetchSize(int fetchSize) {
    delegate.setFetchSize(fetchSize);
  }

  public int getFetchSize() {
    return delegate.getFetchSize();
  }

  public void setMaxRows(int maxRows) {
    delegate.setMaxRows(maxRows);
  }

  public int getMaxRows() {
    return delegate.getMaxRows();
  }

  public void setQueryTimeout(int queryTimeout) {
    delegate.setQueryTimeout(queryTimeout);
  }

  public int getQueryTimeout() {
    return delegate.getQueryTimeout();
  }

  public void setSkipResultsProcessing(boolean skipResultsProcessing) {
    delegate.setSkipResultsProcessing(skipResultsProcessing);
  }

  public boolean isSkipResultsProcessing() {
    return delegate.isSkipResultsProcessing();
  }

  public void setSkipUndeclaredResults(boolean skipUndeclaredResults) {
    delegate.setSkipUndeclaredResults(skipUndeclaredResults);
  }

  public boolean isSkipUndeclaredResults() {
    return delegate.isSkipUndeclaredResults();
  }

  public void setResultsMapCaseInsensitive(boolean resultsMapCaseInsensitive) {
    delegate.setResultsMapCaseInsensitive(resultsMapCaseInsensitive);
  }

  public boolean isResultsMapCaseInsensitive() {
    return delegate.isResultsMapCaseInsensitive();
  }

  public <T> T execute(ConnectionCallback<T> action) throws DataAccessException {
    return delegate.execute(action);
  }

  public <T> T execute(StatementCallback<T> action) throws DataAccessException {
    return delegate.execute(action);
  }

  public void execute(String sql) throws DataAccessException {
    delegate.execute(sql);
  }

  public <T> T query(String sql, ResultSetExtractor<T> rse) throws DataAccessException {
    return delegate.query(sql, rse);
  }

  public void query(String sql, RowCallbackHandler rch) throws DataAccessException {
    delegate.query(sql, rch);
  }

  public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
    return delegate.query(sql, rowMapper);
  }

  public Map<String, Object> queryForMap(String sql) throws DataAccessException {
    return delegate.queryForMap(sql);
  }

  public <T> T queryForObject(String sql, RowMapper<T> rowMapper) throws DataAccessException {
    return delegate.queryForObject(sql, rowMapper);
  }

  public <T> T queryForObject(String sql, Class<T> requiredType) throws DataAccessException {
    return delegate.queryForObject(sql, requiredType);
  }

  public <T> List<T> queryForList(String sql, Class<T> elementType) throws DataAccessException {
    return delegate.queryForList(sql, elementType);
  }

  public List<Map<String, Object>> queryForList(String sql) throws DataAccessException {
    return delegate.queryForList(sql);
  }

  public SqlRowSet queryForRowSet(String sql) throws DataAccessException {
    return delegate.queryForRowSet(sql);
  }

  public MessageFuture<Integer> update(String sql) throws DataAccessException {
    return reporter.report(Sql.builder().sql(sql).build());
  }

  public int[] batchUpdate(String... sql) throws DataAccessException {
    return delegate.batchUpdate(sql);
  }

  public <T> T execute(PreparedStatementCreator psc,
      PreparedStatementCallback<T> action) throws DataAccessException {
    return delegate.execute(psc, action);
  }

  public <T> T execute(String sql, PreparedStatementCallback<T> action) throws DataAccessException {
    return delegate.execute(sql, action);
  }

  public <T> T query(PreparedStatementCreator psc,
      PreparedStatementSetter pss, ResultSetExtractor<T> rse) throws DataAccessException {
    return delegate.query(psc, pss, rse);
  }

  public <T> T query(PreparedStatementCreator psc,
      ResultSetExtractor<T> rse) throws DataAccessException {
    return delegate.query(psc, rse);
  }

  public <T> T query(String sql, PreparedStatementSetter pss,
      ResultSetExtractor<T> rse) throws DataAccessException {
    return delegate.query(sql, pss, rse);
  }

  public <T> T query(String sql, Object[] args, int[] argTypes,
      ResultSetExtractor<T> rse) throws DataAccessException {
    return delegate.query(sql, args, argTypes, rse);
  }

  public <T> T query(String sql, Object[] args,
      ResultSetExtractor<T> rse) throws DataAccessException {
    return delegate.query(sql, args, rse);
  }

  public <T> T query(String sql, ResultSetExtractor<T> rse, Object... args)
      throws DataAccessException {
    return delegate.query(sql, rse, args);
  }

  public void query(PreparedStatementCreator psc,
      RowCallbackHandler rch) throws DataAccessException {
    delegate.query(psc, rch);
  }

  public void query(String sql, PreparedStatementSetter pss,
      RowCallbackHandler rch) throws DataAccessException {
    delegate.query(sql, pss, rch);
  }

  public void query(String sql, Object[] args, int[] argTypes,
      RowCallbackHandler rch) throws DataAccessException {
    delegate.query(sql, args, argTypes, rch);
  }

  public void query(String sql, Object[] args, RowCallbackHandler rch) throws DataAccessException {
    delegate.query(sql, args, rch);
  }

  public void query(String sql, RowCallbackHandler rch, Object... args) throws DataAccessException {
    delegate.query(sql, rch, args);
  }

  public <T> List<T> query(PreparedStatementCreator psc,
      RowMapper<T> rowMapper) throws DataAccessException {
    return delegate.query(psc, rowMapper);
  }

  public <T> List<T> query(String sql, PreparedStatementSetter pss,
      RowMapper<T> rowMapper) throws DataAccessException {
    return delegate.query(sql, pss, rowMapper);
  }

  public <T> List<T> query(String sql, Object[] args, int[] argTypes,
      RowMapper<T> rowMapper) throws DataAccessException {
    return delegate.query(sql, args, argTypes, rowMapper);
  }

  public <T> List<T> query(String sql, Object[] args,
      RowMapper<T> rowMapper) throws DataAccessException {
    return delegate.query(sql, args, rowMapper);
  }

  public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args)
      throws DataAccessException {
    return delegate.query(sql, rowMapper, args);
  }

  public <T> T queryForObject(String sql, Object[] args, int[] argTypes,
      RowMapper<T> rowMapper) throws DataAccessException {
    return delegate.queryForObject(sql, args, argTypes, rowMapper);
  }

  public <T> T queryForObject(String sql, Object[] args,
      RowMapper<T> rowMapper) throws DataAccessException {
    return delegate.queryForObject(sql, args, rowMapper);
  }

  public <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args)
      throws DataAccessException {
    return delegate.queryForObject(sql, rowMapper, args);
  }

  public <T> T queryForObject(String sql, Object[] args, int[] argTypes,
      Class<T> requiredType) throws DataAccessException {
    return delegate.queryForObject(sql, args, argTypes, requiredType);
  }

  public <T> T queryForObject(String sql, Object[] args, Class<T> requiredType)
      throws DataAccessException {
    return delegate.queryForObject(sql, args, requiredType);
  }

  public <T> T queryForObject(String sql, Class<T> requiredType, Object... args)
      throws DataAccessException {
    return delegate.queryForObject(sql, requiredType, args);
  }

  public Map<String, Object> queryForMap(String sql, Object[] args, int[] argTypes)
      throws DataAccessException {
    return delegate.queryForMap(sql, args, argTypes);
  }

  public Map<String, Object> queryForMap(String sql, Object... args) throws DataAccessException {
    return delegate.queryForMap(sql, args);
  }

  public <T> List<T> queryForList(String sql, Object[] args, int[] argTypes,
      Class<T> elementType) throws DataAccessException {
    return delegate.queryForList(sql, args, argTypes, elementType);
  }

  public <T> List<T> queryForList(String sql, Object[] args, Class<T> elementType)
      throws DataAccessException {
    return delegate.queryForList(sql, args, elementType);
  }

  public <T> List<T> queryForList(String sql, Class<T> elementType, Object... args)
      throws DataAccessException {
    return delegate.queryForList(sql, elementType, args);
  }

  public List<Map<String, Object>> queryForList(String sql, Object[] args, int[] argTypes)
      throws DataAccessException {
    return delegate.queryForList(sql, args, argTypes);
  }

  public List<Map<String, Object>> queryForList(String sql, Object... args)
      throws DataAccessException {
    return delegate.queryForList(sql, args);
  }

  public SqlRowSet queryForRowSet(String sql, Object[] args, int[] argTypes)
      throws DataAccessException {
    return delegate.queryForRowSet(sql, args, argTypes);
  }

  public SqlRowSet queryForRowSet(String sql, Object... args) throws DataAccessException {
    return delegate.queryForRowSet(sql, args);
  }

  public MessageFuture<Integer> update(PreparedStatementCreator psc) throws DataAccessException {
    if (psc instanceof SqlProvider) {
      SqlProvider sqlProvider = (SqlProvider) psc;
      String sql = sqlProvider.getSql();

      Builder builder = Sql.builder().sql(sql);
      if (psc instanceof PreparedStatementSetter) {
        PreparedStatementSetter pss = (PreparedStatementSetter) psc;
        builder.preparedStatementSetter(pss);
      }
      return reporter.report(builder.build());
    }
    throw new UnsupportedOperationException("Must be instance of SqlProvider");
  }

  public int update(PreparedStatementCreator psc,
      KeyHolder generatedKeyHolder) throws DataAccessException { // not supported right now
    return delegate.update(psc, generatedKeyHolder);
  }

  public MessageFuture<Integer> update(String sql, PreparedStatementSetter pss) throws DataAccessException {
    return reporter.report(Sql.builder()
        .sql(sql)
        .preparedStatementSetter(pss)
        .build());
  }

  public MessageFuture<Integer> update(String sql, Object[] args, int[] argTypes) throws DataAccessException {
    return reporter.report(Sql.builder()
        .sql(sql)
        .args(args)
        .argTypes(argTypes)
        .build());
  }

  public MessageFuture<Integer> update(String sql, Object... args) throws DataAccessException {
    return reporter.report(Sql.builder()
        .sql(sql)
        .args(args)
        .build());
  }

  public int[] batchUpdate(String sql, BatchPreparedStatementSetter pss)
      throws DataAccessException {
    return delegate.batchUpdate(sql, pss);
  }

  public int[] batchUpdate(String sql, List<Object[]> batchArgs) throws DataAccessException {
    return delegate.batchUpdate(sql, batchArgs);
  }

  public int[] batchUpdate(String sql, List<Object[]> batchArgs, int[] argTypes)
      throws DataAccessException {
    return delegate.batchUpdate(sql, batchArgs, argTypes);
  }

  public <T> int[][] batchUpdate(String sql, Collection<T> batchArgs, int batchSize,
      ParameterizedPreparedStatementSetter<T> pss) throws DataAccessException {
    return delegate.batchUpdate(sql, batchArgs, batchSize, pss);
  }

  public <T> T execute(CallableStatementCreator csc,
      CallableStatementCallback<T> action) throws DataAccessException {
    return delegate.execute(csc, action);
  }

  public <T> T execute(String callString,
      CallableStatementCallback<T> action) throws DataAccessException {
    return delegate.execute(callString, action);
  }

  public Map<String, Object> call(CallableStatementCreator csc,
      List<SqlParameter> declaredParameters) throws DataAccessException {
    return delegate.call(csc, declaredParameters);
  }

  public void setDataSource(DataSource dataSource) {
    delegate.setDataSource(dataSource);
  }

  public DataSource getDataSource() {
    return delegate.getDataSource();
  }

  public void setDatabaseProductName(String dbName) {
    delegate.setDatabaseProductName(dbName);
  }

  public void setExceptionTranslator(
      SQLExceptionTranslator exceptionTranslator) {
    delegate.setExceptionTranslator(exceptionTranslator);
  }

  public SQLExceptionTranslator getExceptionTranslator() {
    return delegate.getExceptionTranslator();
  }

  public void setLazyInit(boolean lazyInit) {
    delegate.setLazyInit(lazyInit);
  }

  public boolean isLazyInit() {
    return delegate.isLazyInit();
  }

  public void afterPropertiesSet() {
    delegate.afterPropertiesSet();
  }
}