// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2012-2014 Monty Program Ab
// Copyright (c) 2015-2025 MariaDB Corporation Ab
package org.mariadb.jdbc.client.result;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.mariadb.jdbc.Statement;
import org.mariadb.jdbc.client.ColumnDecoder;
import org.mariadb.jdbc.client.Context;
import org.mariadb.jdbc.client.DataType;
import org.mariadb.jdbc.client.socket.Reader;
import org.mariadb.jdbc.client.util.ClosableLock;

/** Result-set that will retrieve all rows immediately before returning the result-set. */
public class CompleteResult extends Result {

  /** before first row position = initial position */
  protected static final int BEFORE_FIRST_POS = -1;

  private boolean bulkResult;
  private boolean mightBeBulkResult;

  /**
   * Constructor from exchanges
   *
   * @param stmt current statement
   * @param binaryProtocol does exchanges uses binary protocol
   * @param maxRows maximum number of rows
   * @param metadataList metadata
   * @param reader packet reader
   * @param context connection context
   * @param resultSetType result set type
   * @param closeOnCompletion close statement on completion
   * @param traceEnable network trace exchange possible
   * @param mightBeBulkResult might be a bulk unitary result
   * @throws IOException if Socket error occurs
   * @throws SQLException for all other kind of errors
   */
  @SuppressWarnings({"this-escape"})
  public CompleteResult(
      Statement stmt,
      boolean binaryProtocol,
      long maxRows,
      ColumnDecoder[] metadataList,
      Reader reader,
      Context context,
      int resultSetType,
      boolean closeOnCompletion,
      boolean traceEnable,
      boolean mightBeBulkResult)
      throws IOException, SQLException {
    super(
        stmt,
        binaryProtocol,
        maxRows,
        metadataList,
        reader,
        context,
        resultSetType,
        closeOnCompletion,
        traceEnable,
        false,
        0);
    this.mightBeBulkResult = mightBeBulkResult;

    if (maxRows > 0) {
      this.data = new byte[10][];
      do {
        readNext(reader.readPacket(traceEnable));
      } while (!this.loaded && this.dataSize < maxRows);
      if (!this.loaded) skipRemaining();
    } else {
      // avoiding creating array of array since
      byte[] buf = reader.readPacket(traceEnable);
      if (buf[0] == (byte) 0xFF || (buf[0] == (byte) 0xFE && buf.length < 0xFFFFFF)) {
        // no rows
        this.data = new byte[0][];
        readNext(buf);
      } else {
        byte[] buf2 = reader.readPacket(traceEnable);
        if (buf2[0] == (byte) 0xFF || (buf2[0] == (byte) 0xFE && buf2.length < 0xFFFFFF)) {
          // one row
          this.data = new byte[1][];
          this.data[0] = buf;
          this.dataSize = 1;
          readNext(buf2);
        } else {
          // multiple rows
          this.data = new byte[10][];
          this.data[0] = buf;
          this.data[1] = buf2;
          this.dataSize = 2;
          do {
            readNext(reader.readPacket(traceEnable));
          } while (!this.loaded);
        }
      }
    }
  }

  public void setBulkResult() {
    if (mightBeBulkResult) bulkResult = true;
  }

  private CompleteResult(ColumnDecoder[] metadataList, CompleteResult prev) {
    super(metadataList, prev);
  }

  /**
   * Specific constructor for internal build result-set, empty resultset, or generated key
   * result-set.
   *
   * @param metadataList metadata
   * @param data result-set data
   * @param context connection context
   * @param resultSetType result set type
   */
  public CompleteResult(
      ColumnDecoder[] metadataList, byte[][] data, Context context, int resultSetType) {
    super(metadataList, data, context, resultSetType);
  }

  /**
   * Specific constructor for generating generated key result-set.
   *
   * @param columnName column key
   * @param columnType column key type
   * @param data values
   * @param context connection context
   * @param flags column flags
   * @param resultSetType result set type
   * @return result-set
   */
  public static ResultSet createResultSet(
      String columnName,
      DataType columnType,
      String[][] data,
      Context context,
      int flags,
      int resultSetType) {
    return createResultSet(
        new String[] {columnName},
        new DataType[] {columnType},
        data,
        context,
        flags,
        resultSetType);
  }

  /**
   * Create a result set from given data. Useful for creating "fake" resultSets for
   * DatabaseMetaData, (one example is MariaDbDatabaseMetaData.getTypeInfo())
   *
   * @param columnNames - string array of column names
   * @param columnTypes - column types
   * @param data - each element of this array represents a complete row in the ResultSet. Each value
   *     is given in its string representation, as in MariaDB text protocol, except boolean (BIT(1))
   *     values that are represented as "1" or "0" strings
   * @param context connection context
   * @param flags column flags
   * @param resultSetType result set type
   * @return resultset
   */
  public static ResultSet createResultSet(
      String[] columnNames,
      DataType[] columnTypes,
      String[][] data,
      Context context,
      int flags,
      int resultSetType) {

    int columnNameLength = columnNames.length;
    ColumnDecoder[] columns = new ColumnDecoder[columnNameLength];

    for (int i = 0; i < columnNameLength; i++) {
      columns[i] =
          ColumnDecoder.create(context.getDatabase(), columnNames[i], columnTypes[i], flags);
    }

    int estimatedRowSize = columnNameLength * 20; // estimate ~20 bytes per column
    List<byte[]> rows = new ArrayList<>(data.length);
    for (String[] rowData : data) {
      ByteArrayOutputStream baos = new ByteArrayOutputStream(estimatedRowSize);

      for (String rowDatum : rowData) {

        if (rowDatum != null) {
          byte[] bb = rowDatum.getBytes(StandardCharsets.UTF_8);
          int len = bb.length;
          if (len < 251) {
            baos.write((byte) len);
          } else if (len < 65536) {
            baos.write((byte) 0xfc);
            baos.write((byte) len);
            baos.write((byte) (len >>> 8));
          } else if (len < 0xFFFFFF) {
            baos.write((byte) 0xfd);
            baos.write((byte) len);
            baos.write((byte) (len >>> 8));
            baos.write((byte) (len >>> 16));
          } else {
            baos.write((byte) 0xfe);
            baos.write((byte) len);
            baos.write((byte) (len >>> 8));
            baos.write((byte) (len >>> 16));
            baos.write((byte) (len >>> 24));
            baos.write((byte) ((long) len >>> 32));
            baos.write((byte) ((long) len >>> 40));
            baos.write((byte) ((long) len >>> 48));
            baos.write((byte) ((long) len >>> 56));
          }
          baos.write(bb, 0, len);
        } else {
          baos.write((byte) 0xfb);
        }
      }
      byte[] bb = baos.toByteArray();
      rows.add(bb);
    }
    return new CompleteResult(columns, rows.toArray(new byte[0][0]), context, resultSetType);
  }

  /**
   * Recreate Result-set with metadata using alias as name.
   *
   * @return Result-set with metadata using alias as name
   */
  public CompleteResult newResultsetWithUseAliasAsName() {
    int length = metadataList.length;
    ColumnDecoder[] newMeta = new ColumnDecoder[length];
    for (int i = 0; i < length; i++) {
      newMeta[i] = metadataList[i].useAliasAsName();
    }
    return new CompleteResult(newMeta, this);
  }

  @Override
  public boolean next() throws SQLException {
    if (rowPointer < dataSize - 1) {
      setRow(data[++rowPointer]);
      return true;
    } else {
      // all data are reads and pointer is after last
      setNullRowBuf();
      rowPointer = dataSize;
      return false;
    }
  }

  @Override
  public boolean streaming() {
    return false;
  }

  @Override
  public boolean isBulkResult() {
    return bulkResult;
  }

  @Override
  public void fetchRemaining() {}

  @Override
  public void closeFromStmtClose(ClosableLock lock) {
    this.closed = true;
  }

  @Override
  public boolean isAfterLast() throws SQLException {
    checkClose();
    if (rowPointer < dataSize) {
      // has remaining results
      return false;
    } else {

      // has read all data and pointer is after last result
      // so result would have to always be true,
      // but when result contain no row at all jdbc say that must return false
      return dataSize > 0;
    }
  }

  @Override
  public boolean isFirst() throws SQLException {
    checkClose();
    return rowPointer == 0 && dataSize > 0;
  }

  @Override
  public boolean isLast() throws SQLException {
    checkClose();
    return rowPointer == dataSize - 1 && dataSize > 0;
  }

  @Override
  public void beforeFirst() throws SQLException {
    checkClose();
    rowPointer = BEFORE_FIRST_POS;
    setNullRowBuf();
  }

  @Override
  public void afterLast() throws SQLException {
    checkClose();
    setNullRowBuf();
    rowPointer = dataSize;
  }

  @Override
  public boolean first() throws SQLException {
    checkClose();
    rowPointer = 0;
    if (dataSize == 0) {
      setNullRowBuf();
      return false;
    }
    setRow(data[rowPointer]);
    return true;
  }

  @Override
  public boolean last() throws SQLException {
    checkClose();
    rowPointer = dataSize - 1;
    if (rowPointer == BEFORE_FIRST_POS) {
      setNullRowBuf();
      return false;
    }
    setRow(data[rowPointer]);
    return true;
  }

  @Override
  public int getRow() throws SQLException {
    checkClose();
    return rowPointer == dataSize ? 0 : rowPointer + 1;
  }

  @Override
  public boolean absolute(int idx) throws SQLException {
    checkClose();
    if (idx == 0 || idx > dataSize) {
      rowPointer = idx == 0 ? BEFORE_FIRST_POS : dataSize;
      setNullRowBuf();
      return false;
    }

    if (idx > 0) {
      rowPointer = idx - 1;
      setRow(data[rowPointer]);
      return true;
    } else {
      if (dataSize + idx >= 0) {
        // absolute position reverse from ending resultSet
        rowPointer = dataSize + idx;
        setRow(data[rowPointer]);
        return true;
      }
      rowPointer = BEFORE_FIRST_POS;
      setNullRowBuf();
      return false;
    }
  }

  @Override
  public boolean relative(int rows) throws SQLException {
    checkClose();
    int newPos = rowPointer + rows;
    if (newPos <= -1) {
      rowPointer = BEFORE_FIRST_POS;
      setNullRowBuf();
      return false;
    } else if (newPos >= dataSize) {
      rowPointer = dataSize;
      setNullRowBuf();
      return false;
    } else {
      rowPointer = newPos;
      setRow(data[rowPointer]);
      return true;
    }
  }

  @Override
  public boolean previous() throws SQLException {
    checkClose();
    if (rowPointer > BEFORE_FIRST_POS) {
      rowPointer--;
      if (rowPointer != BEFORE_FIRST_POS) {
        setRow(data[rowPointer]);
        return true;
      }
    }
    setNullRowBuf();
    return false;
  }

  @Override
  public int getFetchSize() throws SQLException {
    checkClose();
    return super.getFetchSize();
  }

  @Override
  public void setFetchSize(int rows) throws SQLException {
    checkClose();
    super.setFetchSize(rows);
  }
}
