/*
 * 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.iotdb.jdbc;

import org.apache.iotdb.service.rpc.thrift.IClientRPCService.Iface;

import org.apache.thrift.TException;
import org.apache.tsfile.common.conf.TSFileConfig;
import org.apache.tsfile.utils.Binary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.net.URL;
import java.nio.charset.Charset;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.NClob;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.Ref;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.RowId;
import java.sql.SQLException;
import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.text.DateFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

public class IoTDBPreparedStatement extends IoTDBStatement implements PreparedStatement {

  private String sql;
  private static final String METHOD_NOT_SUPPORTED_STRING = "Method not supported";
  private static final Logger logger = LoggerFactory.getLogger(IoTDBPreparedStatement.class);

  /** save the SQL parameters as (paramLoc,paramValue) pairs. */
  private final Map<Integer, String> parameters = new HashMap<>();

  IoTDBPreparedStatement(
      IoTDBConnection connection,
      Iface client,
      Long sessionId,
      String sql,
      ZoneId zoneId,
      Charset charset)
      throws SQLException {
    super(connection, client, sessionId, zoneId, charset);
    this.sql = sql;
  }

  // Only for tests
  IoTDBPreparedStatement(
      IoTDBConnection connection, Iface client, Long sessionId, String sql, ZoneId zoneId)
      throws SQLException {
    super(connection, client, sessionId, zoneId, TSFileConfig.STRING_CHARSET);
    this.sql = sql;
  }

  @Override
  public void addBatch() throws SQLException {
    super.addBatch(createCompleteSql(sql, parameters));
  }

  @Override
  public void clearParameters() {
    this.parameters.clear();
  }

  @Override
  public boolean execute() throws SQLException {
    return super.execute(createCompleteSql(sql, parameters));
  }

  @Override
  public ResultSet executeQuery() throws SQLException {
    return super.executeQuery(createCompleteSql(sql, parameters));
  }

  @Override
  public int executeUpdate() throws SQLException {
    return super.executeUpdate(createCompleteSql(sql, parameters));
  }

  @Override
  public ResultSetMetaData getMetaData() throws SQLException {
    return getResultSet().getMetaData();
  }

  @Override
  public ParameterMetaData getParameterMetaData() {
    return new ParameterMetaData() {
      @Override
      public int getParameterCount() {
        return parameters.size();
      }

      @Override
      public int isNullable(int param) {
        return ParameterMetaData.parameterNullableUnknown;
      }

      @Override
      public boolean isSigned(int param) {
        try {
          return Integer.parseInt(parameters.get(param)) < 0;
        } catch (Exception e) {
          return false;
        }
      }

      @Override
      public int getPrecision(int param) {
        return parameters.get(param).length();
      }

      @Override
      public int getScale(int param) {
        try {
          double d = Double.parseDouble(parameters.get(param));
          if (d >= 1) { // we only need the fraction digits
            d = d - (long) d;
          }
          if (d == 0) { // nothing to count
            return 0;
          }
          d *= 10; // shifts 1 digit to left
          int count = 1;
          while (d - (long) d != 0) { // keeps shifting until there are no more fractions
            d *= 10;
            count++;
          }
          return count;
        } catch (Exception e) {
          return 0;
        }
      }

      @Override
      public int getParameterType(int param) {
        return 0;
      }

      @Override
      public String getParameterTypeName(int param) {
        return null;
      }

      @Override
      public String getParameterClassName(int param) {
        return null;
      }

      @Override
      public int getParameterMode(int param) {
        return 0;
      }

      @Override
      public <T> T unwrap(Class<T> iface) {
        return null;
      }

      @Override
      public boolean isWrapperFor(Class<?> iface) {
        return false;
      }
    };
  }

  @Override
  public void setArray(int parameterIndex, Array x) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setBlob(int parameterIndex, Blob x) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setBlob(int parameterIndex, InputStream inputStream, long length)
      throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setBoolean(int parameterIndex, boolean x) {
    this.parameters.put(parameterIndex, Boolean.toString(x));
  }

  @Override
  public void setByte(int parameterIndex, byte x) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setBytes(int parameterIndex, byte[] x) throws SQLException {
    Binary binary = new Binary(x);
    this.parameters.put(parameterIndex, binary.getStringValue(TSFileConfig.STRING_CHARSET));
  }

  @Override
  public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setCharacterStream(int parameterIndex, Reader reader, int length)
      throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setCharacterStream(int parameterIndex, Reader reader, long length)
      throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setClob(int parameterIndex, Clob x) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setClob(int parameterIndex, Reader reader) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setClob(int parameterIndex, Reader reader, long length) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setDate(int parameterIndex, Date x) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setDouble(int parameterIndex, double x) {
    this.parameters.put(parameterIndex, Double.toString(x));
  }

  @Override
  public void setFloat(int parameterIndex, float x) {
    this.parameters.put(parameterIndex, Float.toString(x));
  }

  @Override
  public void setInt(int parameterIndex, int x) {
    this.parameters.put(parameterIndex, Integer.toString(x));
  }

  @Override
  public void setLong(int parameterIndex, long x) {
    this.parameters.put(parameterIndex, Long.toString(x));
  }

  @Override
  public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setNCharacterStream(int parameterIndex, Reader value, long length)
      throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setNClob(int parameterIndex, NClob value) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setNClob(int parameterIndex, Reader reader) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setNString(int parameterIndex, String value) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setNull(int parameterIndex, int sqlType) throws SQLException {
    throw new SQLException(Constant.PARAMETER_NOT_NULL);
  }

  @Override
  public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException {
    throw new SQLException(Constant.PARAMETER_NOT_NULL);
  }

  @Override
  public void setObject(int parameterIndex, Object x) throws SQLException {
    if (x instanceof String) {
      setString(parameterIndex, (String) x);
    } else if (x instanceof Integer) {
      setInt(parameterIndex, (Integer) x);
    } else if (x instanceof Long) {
      setLong(parameterIndex, (Long) x);
    } else if (x instanceof Float) {
      setFloat(parameterIndex, (Float) x);
    } else if (x instanceof Double) {
      setDouble(parameterIndex, (Double) x);
    } else if (x instanceof Boolean) {
      setBoolean(parameterIndex, (Boolean) x);
    } else if (x instanceof Timestamp) {
      setTimestamp(parameterIndex, (Timestamp) x);
    } else if (x instanceof Time) {
      setTime(parameterIndex, (Time) x);
    } else {
      // Can't infer a type.
      throw new SQLException(
          String.format(
              "Can''t infer the SQL type to use for an instance of %s. Use setObject() with"
                  + " an explicit Types value to specify the type to use.",
              x.getClass().getName()));
    }
  }

  @Override
  public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException {
    if (!(x instanceof BigDecimal)) {
      setObject(parameterIndex, x, targetSqlType, 0);
    } else {
      setObject(parameterIndex, x, targetSqlType, ((BigDecimal) x).scale());
    }
  }

  @SuppressWarnings({
    "squid:S3776",
    "squid:S6541"
  }) // ignore Cognitive Complexity of methods should not be too high
  // ignore Methods should not perform too many tasks (aka Brain method)
  @Override
  public void setObject(int parameterIndex, Object parameterObj, int targetSqlType, int scale)
      throws SQLException {
    if (parameterObj == null) {
      setNull(parameterIndex, java.sql.Types.OTHER);
    } else {
      try {
        switch (targetSqlType) {
          case Types.BOOLEAN:
            if (parameterObj instanceof Boolean) {
              setBoolean(parameterIndex, ((Boolean) parameterObj).booleanValue());
              break;
            } else if (parameterObj instanceof String) {
              if ("true".equalsIgnoreCase((String) parameterObj)
                  || "Y".equalsIgnoreCase((String) parameterObj)) {
                setBoolean(parameterIndex, true);
              } else if ("false".equalsIgnoreCase((String) parameterObj)
                  || "N".equalsIgnoreCase((String) parameterObj)) {
                setBoolean(parameterIndex, false);
              } else if (((String) parameterObj).matches("-?\\d+\\.?\\d*")) {
                setBoolean(parameterIndex, !((String) parameterObj).matches("-?[0]+[.]*[0]*"));
              } else {
                throw new SQLException(
                    "No conversion from " + parameterObj + " to Types.BOOLEAN possible.");
              }
              break;
            } else if (parameterObj instanceof Number) {
              int intValue = ((Number) parameterObj).intValue();

              setBoolean(parameterIndex, intValue != 0);

              break;
            } else {
              throw new SQLException(
                  "No conversion from " + parameterObj + " to Types.BOOLEAN possible.");
            }

          case Types.BIT:
          case Types.TINYINT:
          case Types.SMALLINT:
          case Types.INTEGER:
          case Types.BIGINT:
          case Types.REAL:
          case Types.FLOAT:
          case Types.DOUBLE:
          case Types.DECIMAL:
          case Types.NUMERIC:
            setNumericObject(parameterIndex, parameterObj, targetSqlType, scale);
            break;
          case Types.CHAR:
          case Types.VARCHAR:
          case Types.LONGVARCHAR:
            if (parameterObj instanceof BigDecimal) {
              setString(
                  parameterIndex,
                  StringUtils.fixDecimalExponent(
                      StringUtils.consistentToString((BigDecimal) parameterObj)));
            } else {
              setString(parameterIndex, parameterObj.toString());
            }

            break;

          case Types.CLOB:
            if (parameterObj instanceof java.sql.Clob) {
              setClob(parameterIndex, (java.sql.Clob) parameterObj);
            } else {
              setString(parameterIndex, parameterObj.toString());
            }

            break;

          case Types.BINARY:
          case Types.VARBINARY:
          case Types.LONGVARBINARY:
          case Types.BLOB:
            throw new SQLException(Constant.PARAMETER_SUPPORTED);
          case Types.DATE:
          case Types.TIMESTAMP:
            java.util.Date parameterAsDate;

            if (parameterObj instanceof String) {
              ParsePosition pp = new ParsePosition(0);
              DateFormat sdf =
                  new SimpleDateFormat(getDateTimePattern((String) parameterObj, false), Locale.US);
              parameterAsDate = sdf.parse((String) parameterObj, pp);
            } else {
              parameterAsDate = (Date) parameterObj;
            }

            switch (targetSqlType) {
              case Types.DATE:
                if (parameterAsDate instanceof java.sql.Date) {
                  setDate(parameterIndex, (java.sql.Date) parameterAsDate);
                } else {
                  setDate(parameterIndex, new java.sql.Date(parameterAsDate.getTime()));
                }

                break;

              case Types.TIMESTAMP:
                if (parameterAsDate instanceof java.sql.Timestamp) {
                  setTimestamp(parameterIndex, (java.sql.Timestamp) parameterAsDate);
                } else {
                  setTimestamp(parameterIndex, new java.sql.Timestamp(parameterAsDate.getTime()));
                }

                break;
              default:
                logger.error("No type was matched");
                break;
            }

            break;

          case Types.TIME:
            if (parameterObj instanceof String) {
              DateFormat sdf =
                  new SimpleDateFormat(getDateTimePattern((String) parameterObj, true), Locale.US);
              setTime(parameterIndex, new Time(sdf.parse((String) parameterObj).getTime()));
            } else if (parameterObj instanceof Timestamp) {
              Timestamp xT = (Timestamp) parameterObj;
              setTime(parameterIndex, new Time(xT.getTime()));
            } else {
              setTime(parameterIndex, (Time) parameterObj);
            }

            break;

          case Types.OTHER:
            throw new SQLException(Constant.PARAMETER_SUPPORTED); //
          default:
            throw new SQLException(Constant.PARAMETER_SUPPORTED); //
        }
      } catch (SQLException ex) {
        throw ex;
      } catch (Exception ex) {
        throw new SQLException(Constant.PARAMETER_SUPPORTED); //
      }
    }
  }

  @SuppressWarnings({
    "squid:S3776",
    "squid:S6541"
  }) // ignore Cognitive Complexity of methods should not be too high
  // ignore Methods should not perform too many tasks (aka Brain method)
  private final String getDateTimePattern(String dt, boolean toTime) throws Exception {
    //
    // Special case
    //
    int dtLength = (dt != null) ? dt.length() : 0;

    if ((dtLength >= 8) && (dtLength <= 10)) {
      int dashCount = 0;
      boolean isDateOnly = true;

      for (int i = 0; i < dtLength; i++) {
        char c = dt.charAt(i);

        if (!Character.isDigit(c) && (c != '-')) {
          isDateOnly = false;

          break;
        }

        if (c == '-') {
          dashCount++;
        }
      }

      if (isDateOnly && (dashCount == 2)) {
        return "yyyy-MM-dd";
      }
    }
    boolean colonsOnly = true;

    for (int i = 0; i < dtLength; i++) {
      char c = dt.charAt(i);

      if (!Character.isDigit(c) && (c != ':')) {
        colonsOnly = false;

        break;
      }
    }

    if (colonsOnly) {
      return "HH:mm:ss";
    }

    int n;
    int z;
    int count;
    int maxvecs;
    char c;
    char separator;
    StringReader reader = new StringReader(dt + " ");
    ArrayList<Object[]> vec = new ArrayList<>();
    ArrayList<Object[]> vecRemovelist = new ArrayList<>();
    Object[] nv = new Object[3];
    Object[] v;
    nv[0] = Character.valueOf('y');
    nv[1] = new StringBuilder();
    nv[2] = Integer.valueOf(0);
    vec.add(nv);

    if (toTime) {
      nv = new Object[3];
      nv[0] = Character.valueOf('h');
      nv[1] = new StringBuilder();
      nv[2] = Integer.valueOf(0);
      vec.add(nv);
    }

    while ((z = reader.read()) != -1) {
      separator = (char) z;
      maxvecs = vec.size();

      for (count = 0; count < maxvecs; count++) {
        v = vec.get(count);
        n = ((Integer) v[2]).intValue();
        c = getSuccessor(((Character) v[0]).charValue(), n);

        if (!Character.isLetterOrDigit(separator)) {
          if ((c == ((Character) v[0]).charValue()) && (c != 'S')) {
            vecRemovelist.add(v);
          } else {
            ((StringBuilder) v[1]).append(separator);

            if ((c == 'X') || (c == 'Y')) {
              v[2] = Integer.valueOf(4);
            }
          }
        } else {
          if (c == 'X') {
            c = 'y';
            nv = new Object[3];
            nv[1] = (new StringBuilder(((StringBuilder) v[1]).toString())).append('M');
            nv[0] = Character.valueOf('M');
            nv[2] = Integer.valueOf(1);
            vec.add(nv);
          } else if (c == 'Y') {
            c = 'M';
            nv = new Object[3];
            nv[1] = (new StringBuilder(((StringBuilder) v[1]).toString())).append('d');
            nv[0] = Character.valueOf('d');
            nv[2] = Integer.valueOf(1);
            vec.add(nv);
          }

          ((StringBuilder) v[1]).append(c);

          if (c == ((Character) v[0]).charValue()) {
            v[2] = Integer.valueOf(n + 1);
          } else {
            v[0] = Character.valueOf(c);
            v[2] = Integer.valueOf(1);
          }
        }
      }

      int size = vecRemovelist.size();

      for (int i = 0; i < size; i++) {
        v = vecRemovelist.get(i);
        vec.remove(v);
      }

      vecRemovelist.clear();
    }

    int size = vec.size();

    for (int i = 0; i < size; i++) {
      v = vec.get(i);
      c = ((Character) v[0]).charValue();
      n = ((Integer) v[2]).intValue();

      boolean bk = getSuccessor(c, n) != c;
      boolean atEnd = (((c == 's') || (c == 'm') || ((c == 'h') && toTime)) && bk);
      boolean finishesAtDate = (bk && (c == 'd') && !toTime);
      boolean containsEnd = (((StringBuilder) v[1]).toString().indexOf('W') != -1);

      if ((!atEnd && !finishesAtDate) || (containsEnd)) {
        vecRemovelist.add(v);
      }
    }

    size = vecRemovelist.size();

    for (int i = 0; i < size; i++) {
      vec.remove(vecRemovelist.get(i));
    }

    vecRemovelist.clear();
    v = vec.get(0); // might throw exception

    StringBuilder format = (StringBuilder) v[1];
    format.setLength(format.length() - 1);

    return format.toString();
  }

  @SuppressWarnings({"squid:S3776", "squid:S3358"}) // ignore Ternary operators should not be nested
  // ignore Cognitive Complexity of methods should not be too high
  private final char getSuccessor(char c, int n) {
    return ((c == 'y') && (n == 2))
        ? 'X'
        : (((c == 'y') && (n < 4))
            ? 'y'
            : ((c == 'y')
                ? 'M'
                : (((c == 'M') && (n == 2))
                    ? 'Y'
                    : (((c == 'M') && (n < 3))
                        ? 'M'
                        : ((c == 'M')
                            ? 'd'
                            : (((c == 'd') && (n < 2))
                                ? 'd'
                                : ((c == 'd')
                                    ? 'H'
                                    : (((c == 'H') && (n < 2))
                                        ? 'H'
                                        : ((c == 'H')
                                            ? 'm'
                                            : (((c == 'm') && (n < 2))
                                                ? 'm'
                                                : ((c == 'm')
                                                    ? 's'
                                                    : (((c == 's') && (n < 2))
                                                        ? 's'
                                                        : 'W'))))))))))));
  }

  @SuppressWarnings({
    "squid:S3776",
    "squid:S6541"
  }) // ignore Cognitive Complexity of methods should not be too high
  // ignore Methods should not perform too many tasks (aka Brain method)
  private void setNumericObject(
      int parameterIndex, Object parameterObj, int targetSqlType, int scale) throws SQLException {
    Number parameterAsNum;

    if (parameterObj instanceof Boolean) {
      parameterAsNum =
          ((Boolean) parameterObj).booleanValue() ? Integer.valueOf(1) : Integer.valueOf(0);
    } else if (parameterObj instanceof String) {
      switch (targetSqlType) {
        case Types.BIT:
          if ("1".equals(parameterObj) || "0".equals(parameterObj)) {
            parameterAsNum = Integer.valueOf((String) parameterObj);
          } else {
            boolean parameterAsBoolean = "true".equalsIgnoreCase((String) parameterObj);

            parameterAsNum = parameterAsBoolean ? Integer.valueOf(1) : Integer.valueOf(0);
          }

          break;

        case Types.TINYINT:
        case Types.SMALLINT:
        case Types.INTEGER:
          parameterAsNum = Integer.valueOf((String) parameterObj);

          break;

        case Types.BIGINT:
          parameterAsNum = Long.valueOf((String) parameterObj);

          break;

        case Types.REAL:
          parameterAsNum = Float.valueOf((String) parameterObj);

          break;

        case Types.FLOAT:
        case Types.DOUBLE:
          parameterAsNum = Double.valueOf((String) parameterObj);

          break;

        case Types.DECIMAL:
        case Types.NUMERIC:
        default:
          parameterAsNum = new java.math.BigDecimal((String) parameterObj);
      }
    } else {
      parameterAsNum = (Number) parameterObj;
    }

    switch (targetSqlType) {
      case Types.BIT:
      case Types.TINYINT:
      case Types.SMALLINT:
      case Types.INTEGER:
        setInt(parameterIndex, parameterAsNum.intValue());
        break;

      case Types.BIGINT:
        setLong(parameterIndex, parameterAsNum.longValue());
        break;

      case Types.REAL:
        setFloat(parameterIndex, parameterAsNum.floatValue());
        break;

      case Types.FLOAT:
        setFloat(parameterIndex, parameterAsNum.floatValue());
        break;
      case Types.DOUBLE:
        setDouble(parameterIndex, parameterAsNum.doubleValue());

        break;

      case Types.DECIMAL:
      case Types.NUMERIC:
        if (parameterAsNum instanceof java.math.BigDecimal) {
          BigDecimal scaledBigDecimal = null;

          try {
            scaledBigDecimal = ((java.math.BigDecimal) parameterAsNum).setScale(scale);
          } catch (ArithmeticException ex) {
            try {
              scaledBigDecimal =
                  ((java.math.BigDecimal) parameterAsNum).setScale(scale, BigDecimal.ROUND_HALF_UP);
            } catch (ArithmeticException arEx) {
              throw new SQLException(
                  "Can't set scale of '"
                      + scale
                      + "' for DECIMAL argument '"
                      + parameterAsNum
                      + "'");
            }
          }

          setBigDecimal(parameterIndex, scaledBigDecimal);
        } else if (parameterAsNum instanceof java.math.BigInteger) {
          setBigDecimal(
              parameterIndex,
              new java.math.BigDecimal((java.math.BigInteger) parameterAsNum, scale));
        } else {
          setBigDecimal(parameterIndex, BigDecimal.valueOf(parameterAsNum.doubleValue()));
        }

        break;
      default:
    }
  }

  @Override
  public void setRef(int parameterIndex, Ref x) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setRowId(int parameterIndex, RowId x) throws SQLException {
    throw new SQLException(METHOD_NOT_SUPPORTED_STRING);
  }

  @Override
  public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException {
    throw new SQLException(METHOD_NOT_SUPPORTED_STRING);
  }

  @Override
  public void setShort(int parameterIndex, short x) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setString(int parameterIndex, String x) {
    // if the sql is insert and the value is not a string literal, add double quotes
    if (sql.trim().toUpperCase().startsWith("INSERT") && !x.startsWith("\"") && !x.endsWith("'")) {
      this.parameters.put(parameterIndex, "\"" + x + "\"");
    } else {
      this.parameters.put(parameterIndex, x);
    }
  }

  @Override
  public void setTime(int parameterIndex, Time x) throws SQLException {
    try {
      long time = x.getTime();
      String timeprecision = client.getProperties().getTimestampPrecision();
      switch (timeprecision.toLowerCase()) {
        case "ms":
          break;
        case "us":
          time = time * 1000;
          break;
        case "ns":
          time = time * 1000000;
          break;
        default:
          break;
      }
      setLong(parameterIndex, time);
    } catch (TException e) {
      logger.error(
          String.format("set time error when iotdb prepared statement :%s ", e.getMessage()));
    }
  }

  @Override
  public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException {
    try {
      ZonedDateTime zonedDateTime = null;
      long time = x.getTime();
      String timeprecision = client.getProperties().getTimestampPrecision();
      switch (timeprecision.toLowerCase()) {
        case "ms":
          break;
        case "us":
          time = time * 1000;
          break;
        case "ns":
          time = time * 1000000;
          break;
        default:
          break;
      }
      if (cal != null) {
        zonedDateTime =
            ZonedDateTime.ofInstant(
                Instant.ofEpochMilli(time), ZoneId.of(cal.getTimeZone().getID()));
      } else {
        zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(time), super.zoneId);
      }
      this.parameters.put(
          parameterIndex, zonedDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
    } catch (TException e) {
      logger.error(
          String.format("set time error when iotdb prepared statement :%s ", e.getMessage()));
    }
  }

  @Override
  public void setTimestamp(int parameterIndex, Timestamp x) {
    ZonedDateTime zonedDateTime =
        ZonedDateTime.ofInstant(Instant.ofEpochMilli(x.getTime()), super.zoneId);
    this.parameters.put(
        parameterIndex, zonedDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
  }

  @Override
  public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException {
    ZonedDateTime zonedDateTime = null;
    if (cal != null) {
      zonedDateTime =
          ZonedDateTime.ofInstant(
              Instant.ofEpochMilli(x.getTime()), ZoneId.of(cal.getTimeZone().getID()));
    } else {
      zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(x.getTime()), super.zoneId);
    }
    this.parameters.put(
        parameterIndex, zonedDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
  }

  @Override
  public void setURL(int parameterIndex, URL x) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  @Override
  public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException {
    throw new SQLException(Constant.PARAMETER_SUPPORTED);
  }

  private String createCompleteSql(final String sql, Map<Integer, String> parameters)
      throws SQLException {
    List<String> parts = splitSqlStatement(sql);

    StringBuilder newSql = new StringBuilder(parts.get(0));
    for (int i = 1; i < parts.size(); i++) {
      if (logger.isDebugEnabled()) {
        logger.debug("SQL {}", sql);
        logger.debug("parameters {}", parameters.size());
      }
      if (!parameters.containsKey(i)) {
        throw new SQLException("Parameter #" + i + " is unset");
      }
      newSql.append(parameters.get(i));
      newSql.append(parts.get(i));
    }
    return newSql.toString();
  }

  private List<String> splitSqlStatement(final String sql) {
    List<String> parts = new ArrayList<>();
    int apCount = 0;
    int off = 0;
    boolean skip = false;

    for (int i = 0; i < sql.length(); i++) {
      char c = sql.charAt(i);
      if (skip) {
        skip = false;
        continue;
      }
      switch (c) {
        case '\'':
          // skip something like 'xxxxx'
          apCount++;
          break;
        case '\\':
          // skip something like \r\n
          skip = true;
          break;
        case '?':
          // for input like: select a from 'bc' where d, 'bc' will be skipped
          if ((apCount & 1) == 0) {
            parts.add(sql.substring(off, i));
            off = i + 1;
          }
          break;
        default:
          break;
      }
    }
    parts.add(sql.substring(off));
    return parts;
  }
}
