/*
 * 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.ArrayList;
import java.util.List;
import java.util.stream.Collector;

/**
 * @author <a href="mailto:nscavell@redhat.com">Nick Scavelli</a>
 */
public class JDBCPreparedQuery<C, R> extends JDBCQueryAction<C, R> {

  private final ExtendedQueryCommand<R> query;
  private final Tuple params;
  private final List<Integer> outParams;

  private static List<Integer> countOut(Tuple tuple) {
    List<Integer> total = new ArrayList<>();
    if (tuple != null) {
      for (int i = 0; i < tuple.size(); i++) {
        if (tuple.getValue(i) instanceof SqlOutParam) {
          total.add(i + 1);
        }
      }
    }

    return total;
  }

  public JDBCPreparedQuery(JDBCStatementHelper helper, SQLOptions options, ExtendedQueryCommand<R> query, Collector<Row, C, R> collector, Tuple params) {
    super(helper, options, collector);
    this.query = query;
    this.params = params;
    this.outParams = countOut(params);
  }

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

    try (PreparedStatement ps = prepare(conn, returnAutoGeneratedKeys)) {
      fillStatement(ps, conn);
      return decode(ps, ps.execute(), returnAutoGeneratedKeys, outParams);
    }
  }

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

    final String sql = query.sql();

    if (outParams.size() > 0) {
      return conn.prepareCall(sql);
    } else {

      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) 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) {
        SqlOutParam outValue = (SqlOutParam) value;

        if (outValue.in()) {
          ps.setObject(i + 1, adaptType(conn, outValue.value()));
        }

        ((CallableStatement) ps)
          .registerOutParameter(i + 1, outValue.type());
      } 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 buffer = (Buffer) value;
      Blob blob = conn.createBlob();
      blob.setBytes(1, buffer.getBytes());
      return blob;
    }

    return value;
  }
}
