/*
 * 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.arrow.driver.jdbc.accessor.impl.text;

import static java.nio.charset.StandardCharsets.US_ASCII;

import java.io.ByteArrayInputStream;
import java.io.CharArrayReader;
import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.sql.Date;
import java.sql.SQLException;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.function.IntSupplier;
import org.apache.arrow.driver.jdbc.accessor.ArrowFlightJdbcAccessor;
import org.apache.arrow.driver.jdbc.accessor.ArrowFlightJdbcAccessorFactory;
import org.apache.arrow.driver.jdbc.utils.DateTimeUtils;
import org.apache.arrow.vector.LargeVarCharVector;
import org.apache.arrow.vector.VarCharVector;
import org.apache.arrow.vector.util.Text;

/** Accessor for the Arrow types: {@link VarCharVector} and {@link LargeVarCharVector}. */
public class ArrowFlightJdbcVarCharVectorAccessor extends ArrowFlightJdbcAccessor {

  /** Functional interface to help integrating VarCharVector and LargeVarCharVector. */
  @FunctionalInterface
  interface Getter {
    byte[] get(int index);
  }

  private final Getter getter;

  public ArrowFlightJdbcVarCharVectorAccessor(
      VarCharVector vector,
      IntSupplier currentRowSupplier,
      ArrowFlightJdbcAccessorFactory.WasNullConsumer setCursorWasNull) {
    this(vector::get, currentRowSupplier, setCursorWasNull);
  }

  public ArrowFlightJdbcVarCharVectorAccessor(
      LargeVarCharVector vector,
      IntSupplier currentRowSupplier,
      ArrowFlightJdbcAccessorFactory.WasNullConsumer setCursorWasNull) {
    this(vector::get, currentRowSupplier, setCursorWasNull);
  }

  ArrowFlightJdbcVarCharVectorAccessor(
      Getter getter,
      IntSupplier currentRowSupplier,
      ArrowFlightJdbcAccessorFactory.WasNullConsumer setCursorWasNull) {
    super(currentRowSupplier, setCursorWasNull);
    this.getter = getter;
  }

  @Override
  public Class<?> getObjectClass() {
    return String.class;
  }

  @Override
  public String getObject() {
    final byte[] bytes = getBytes();
    return bytes == null ? null : new String(bytes, StandardCharsets.UTF_8);
  }

  @Override
  public String getString() {
    return getObject();
  }

  @Override
  public byte[] getBytes() {
    final byte[] bytes = this.getter.get(getCurrentRow());
    this.wasNull = bytes == null;
    this.wasNullConsumer.setWasNull(this.wasNull);
    return bytes;
  }

  @Override
  public boolean getBoolean() throws SQLException {
    String value = getString();
    if (value == null || value.equalsIgnoreCase("false") || value.equals("0")) {
      return false;
    } else if (value.equalsIgnoreCase("true") || value.equals("1")) {
      return true;
    } else {
      throw new SQLException("It is not possible to convert this value to boolean: " + value);
    }
  }

  @Override
  public byte getByte() throws SQLException {
    try {
      return Byte.parseByte(this.getString());
    } catch (Exception e) {
      throw new SQLException(e);
    }
  }

  @Override
  public short getShort() throws SQLException {
    try {
      return Short.parseShort(this.getString());
    } catch (Exception e) {
      throw new SQLException(e);
    }
  }

  @Override
  public int getInt() throws SQLException {
    try {
      return Integer.parseInt(this.getString());
    } catch (Exception e) {
      throw new SQLException(e);
    }
  }

  @Override
  public long getLong() throws SQLException {
    try {
      return Long.parseLong(this.getString());
    } catch (Exception e) {
      throw new SQLException(e);
    }
  }

  @Override
  public float getFloat() throws SQLException {
    try {
      return Float.parseFloat(this.getString());
    } catch (Exception e) {
      throw new SQLException(e);
    }
  }

  @Override
  public double getDouble() throws SQLException {
    try {
      return Double.parseDouble(this.getString());
    } catch (Exception e) {
      throw new SQLException(e);
    }
  }

  @Override
  public BigDecimal getBigDecimal() throws SQLException {
    try {
      return new BigDecimal(this.getString());
    } catch (Exception e) {
      throw new SQLException(e);
    }
  }

  @Override
  public BigDecimal getBigDecimal(int i) throws SQLException {
    try {
      return BigDecimal.valueOf(this.getLong(), i);
    } catch (Exception e) {
      throw new SQLException(e);
    }
  }

  @Override
  public InputStream getAsciiStream() {
    final String textValue = getString();
    if (textValue == null) {
      return null;
    }
    // Already in UTF-8
    return new ByteArrayInputStream(textValue.getBytes(US_ASCII));
  }

  @Override
  public InputStream getUnicodeStream() {
    final byte[] value = getBytes();
    if (value == null) {
      return null;
    }

    // Already in UTF-8
    final Text textValue = new Text(value);
    return new ByteArrayInputStream(textValue.getBytes(), 0, (int) textValue.getLength());
  }

  @Override
  public Reader getCharacterStream() {
    return new CharArrayReader(getString().toCharArray());
  }

  @Override
  public Date getDate(Calendar calendar) throws SQLException {
    try {
      Date date = Date.valueOf(getString());
      if (calendar == null) {
        return date;
      }

      // Use Calendar to apply time zone's offset
      long milliseconds = date.getTime();
      return new Date(DateTimeUtils.applyCalendarOffset(milliseconds, calendar));
    } catch (Exception e) {
      throw new SQLException(e);
    }
  }

  @Override
  public Time getTime(Calendar calendar) throws SQLException {
    try {
      Time time = Time.valueOf(getString());
      if (calendar == null) {
        return time;
      }

      // Use Calendar to apply time zone's offset
      long milliseconds = time.getTime();
      return new Time(DateTimeUtils.applyCalendarOffset(milliseconds, calendar));
    } catch (Exception e) {
      throw new SQLException(e);
    }
  }

  @Override
  public Timestamp getTimestamp(Calendar calendar) throws SQLException {
    try {
      Timestamp timestamp = Timestamp.valueOf(getString());
      if (calendar == null) {
        return timestamp;
      }

      // Use Calendar to apply time zone's offset
      long milliseconds = timestamp.getTime();
      return new Timestamp(DateTimeUtils.applyCalendarOffset(milliseconds, calendar));
    } catch (Exception e) {
      throw new SQLException(e);
    }
  }
}
