/*
 * Copyright (c) 2011-2014 The original author or authors
 * ------------------------------------------------------
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution.
 *
 *     The Eclipse Public License is available at
 *     http://www.eclipse.org/legal/epl-v10.html
 *
 *     The Apache License v2.0 is available at
 *     http://www.opensource.org/licenses/apache2.0.php
 *
 * You may elect to redistribute this code under either of these licenses.
 */

package io.vertx.jdbcclient.impl.actions;

import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;
import io.vertx.ext.jdbc.impl.actions.JDBCStatementHelper;
import io.vertx.ext.sql.SQLOptions;
import io.vertx.jdbcclient.SqlOutParam;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.Tuple;
import io.vertx.sqlclient.impl.command.ExtendedQueryCommand;

import java.sql.*;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import java.util.stream.Collector;

/**
 * @author <a href="mailto:pmlopes@gmail.com">Paulo Lopes</a>
 */
public class JDBCPreparedBatch<C, R> extends JDBCQueryAction<C, R> {

  private final ExtendedQueryCommand<R> query;
  private final List<Tuple> listParams;

  public JDBCPreparedBatch(JDBCStatementHelper helper, SQLOptions options, ExtendedQueryCommand<R> query, Collector<Row, C, R> collector, List<Tuple> listParams) {
    super(helper, options, collector);
    this.query = query;
    this.listParams = listParams;
  }

  @Override
  public JDBCResponse<R> execute(Connection conn) throws SQLException {
    boolean returnAutoGeneratedKeys = returnAutoGeneratedKeys(conn);

    try (PreparedStatement ps = prepare(conn, returnAutoGeneratedKeys)) {
      for (Tuple params : listParams) {
        fillStatement(ps, conn, params);
        ps.addBatch();
      }
      return decode(ps, ps.executeBatch(), returnAutoGeneratedKeys);
    }
  }

  private PreparedStatement prepare(Connection conn, boolean returnAutoGeneratedKeys) throws SQLException {

    final String sql = query.sql();

    boolean autoGeneratedIndexes = options != null && options.getAutoGeneratedKeysIndexes() != null && options.getAutoGeneratedKeysIndexes().size() > 0;

    if (returnAutoGeneratedKeys && !autoGeneratedIndexes) {
      return conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
    } else if (autoGeneratedIndexes) {
      // convert json array to int or string array
      JsonArray indexes = options.getAutoGeneratedKeysIndexes();
      try {
        if (indexes.getValue(0) instanceof Number) {
          int[] keys = new int[indexes.size()];
          for (int i = 0; i < keys.length; i++) {
            keys[i] = indexes.getInteger(i);
          }
          return conn.prepareStatement(sql, keys);
        } else if (indexes.getValue(0) instanceof String) {
          String[] keys = new String[indexes.size()];
          for (int i = 0; i < keys.length; i++) {
            keys[i] = indexes.getString(i);
          }
          return conn.prepareStatement(sql, keys);
        } else {
          throw new SQLException("Invalid type of index, only [int, String] allowed");
        }
      } catch (RuntimeException e) {
        // any exception due to type conversion
        throw new SQLException(e);
      }
    } else {
      return conn.prepareStatement(sql);
    }
  }

  private void fillStatement(PreparedStatement ps, Connection conn, Tuple params) throws SQLException {

    for (int i = 0; i < params.size(); i++) {
      // we must convert types (to comply to JDBC)
      Object value = adaptType(conn, params.getValue(i));

      if (value instanceof SqlOutParam) {
        throw new SQLException("{out} parameters are not supported in batch mode");
      } else {
        ps.setObject(i + 1, value);
      }
    }
  }

  private Object adaptType(Connection conn, Object value) throws SQLException {
    // we must convert types (to comply to JDBC)

    if (value instanceof LocalTime) {
      // -> java.sql.Time
      LocalTime time = (LocalTime) value;
      return Time.valueOf(time);
    } else if (value instanceof LocalDate) {
      // -> java.sql.Date
      LocalDate date = (LocalDate) value;
      return Date.valueOf(date);
    } else if (value instanceof Instant) {
      // -> java.sql.Timestamp
      Instant timestamp = (Instant) value;
      return Timestamp.from(timestamp);
    } else if (value instanceof Buffer) {
      // -> java.sql.Blob
      Buffer blob = (Buffer) value;
      return conn.createBlob().setBytes(0, blob.getBytes());
    }

    return value;
  }
}
