/*
 * Copyright (c) 2003, PostgreSQL Global Development Group
 * See the LICENSE file in the project root for more information.
 */

package org.postgresql.fastpath;

import static org.postgresql.util.internal.Nullness.castNonNull;

import org.postgresql.core.BaseConnection;
import org.postgresql.core.ParameterList;
import org.postgresql.core.QueryExecutor;
import org.postgresql.util.ByteConverter;
import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;

// import org.checkerframework.checker.nullness.qual.Nullable;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;

/**
 * This class implements the Fastpath api.
 *
 * <p>This is a means of executing functions embedded in the backend from within a java application.</p>
 *
 * <p>It is based around the file src/interfaces/libpq/fe-exec.c</p>
 */
public class Fastpath {
  // Java passes oids around as longs, but in the backend
  // it's an unsigned int, so we use this to make the conversion
  // of long -> signed int which the backend interprets as unsigned.
  private static final long NUM_OIDS = 4294967296L; // 2^32

  // This maps the functions names to their id's (possible unique just
  // to a connection).
  private final Map<String, Integer> func = new HashMap<>();
  private final QueryExecutor executor;
  private final BaseConnection connection;

  /**
   * Initialises the fastpath system.
   *
   * @param conn BaseConnection to attach to
   */
  public Fastpath(BaseConnection conn) {
    this.connection = conn;
    this.executor = conn.getQueryExecutor();
  }

  /**
   * Send a function call to the PostgreSQL backend.
   *
   * @param fnId Function id
   * @param resultType True if the result is a numeric (Integer or Long)
   * @param args FastpathArguments to pass to fastpath
   * @return null if no data, Integer if an integer result, Long if a long result, or byte[]
   *         otherwise
   * @throws SQLException if a database-access error occurs.
   * @deprecated please use {@link #fastpath(int, FastpathArg[])}
   */
  @Deprecated
  public /* @Nullable */ Object fastpath(int fnId, boolean resultType, FastpathArg[] args)
      throws SQLException {
    // Run it.
    byte[] returnValue = fastpath(fnId, args);

    // Interpret results.
    if (!resultType || returnValue == null) {
      return returnValue;
    }

    if (returnValue.length == 4) {
      return ByteConverter.int4(returnValue, 0);
    } else if (returnValue.length == 8) {
      return ByteConverter.int8(returnValue, 0);
    } else {
      throw new PSQLException(
          GT.tr("Fastpath call {0} - No result was returned and we expected a numeric.", fnId),
          PSQLState.NO_DATA);
    }
  }

  /**
   * Send a function call to the PostgreSQL backend.
   *
   * @param fnId Function id
   * @param args FastpathArguments to pass to fastpath
   * @return null if no data, byte[] otherwise
   * @throws SQLException if a database-access error occurs.
   */
  public byte /* @Nullable */ [] fastpath(int fnId, FastpathArg[] args) throws SQLException {
    // Turn fastpath array into a parameter list.
    @SuppressWarnings("deprecation")
    ParameterList params = executor.createFastpathParameters(args.length);
    for (int i = 0; i < args.length; i++) {
      args[i].populateParameter(params, i + 1);
    }

    // Run it.
    @SuppressWarnings("deprecation")
    byte[] result = executor.fastpathCall(fnId, params, connection.getAutoCommit());
    return result;
  }

  /**
   * @param name Function name
   * @param resulttype True if the result is a numeric (Integer or Long)
   * @param args FastpathArguments to pass to fastpath
   * @return null if no data, Integer if an integer result, Long if a long result, or byte[]
   *         otherwise
   * @throws SQLException if something goes wrong
   * @see #fastpath(int, FastpathArg[])
   * @see #fastpath(String, FastpathArg[])
   * @deprecated Use {@link #getData(String, FastpathArg[])} if you expect a binary result, or one
   *             of {@link #getInteger(String, FastpathArg[])} or
   *             {@link #getLong(String, FastpathArg[])} if you expect a numeric one
   */
  @Deprecated
  public /* @Nullable */ Object fastpath(String name, boolean resulttype, FastpathArg[] args)
      throws SQLException {
    connection.getLogger().log(Level.FINEST, "Fastpath: calling {0}", name);
    return fastpath(getID(name), resulttype, args);
  }

  /**
   * Send a function call to the PostgreSQL backend by name.
   *
   * <p>Note: the mapping for the procedure name to function id needs to exist, usually to an earlier
   * call to addfunction().</p>
   *
   * <p>This is the preferred method to call, as function id's can/may change between versions of the
   * backend.</p>
   *
   * <p>For an example of how this works, refer to org.postgresql.largeobject.LargeObject</p>
   *
   * @param name Function name
   * @param args FastpathArguments to pass to fastpath
   * @return null if no data, byte[] otherwise
   * @throws SQLException if name is unknown or if a database-access error occurs.
   * @see org.postgresql.largeobject.LargeObject
   */
  public byte /* @Nullable */ [] fastpath(String name, FastpathArg[] args) throws SQLException {
    connection.getLogger().log(Level.FINEST, "Fastpath: calling {0}", name);
    return fastpath(getID(name), args);
  }

  /**
   * This convenience method assumes that the return value is an integer.
   *
   * @param name Function name
   * @param args Function arguments
   * @return integer result
   * @throws SQLException if a database-access error occurs or no result
   */
  public int getInteger(String name, FastpathArg[] args) throws SQLException {
    byte[] returnValue = fastpath(name, args);
    if (returnValue == null) {
      throw new PSQLException(
          GT.tr("Fastpath call {0} - No result was returned and we expected an integer.", name),
          PSQLState.NO_DATA);
    }

    if (returnValue.length == 4) {
      return ByteConverter.int4(returnValue, 0);
    } else {
      throw new PSQLException(GT.tr(
          "Fastpath call {0} - No result was returned or wrong size while expecting an integer.",
          name), PSQLState.NO_DATA);
    }
  }

  /**
   * This convenience method assumes that the return value is a long (bigint).
   *
   * @param name Function name
   * @param args Function arguments
   * @return long result
   * @throws SQLException if a database-access error occurs or no result
   */
  public long getLong(String name, FastpathArg[] args) throws SQLException {
    byte[] returnValue = fastpath(name, args);
    if (returnValue == null) {
      throw new PSQLException(
          GT.tr("Fastpath call {0} - No result was returned and we expected a long.", name),
          PSQLState.NO_DATA);
    }
    if (returnValue.length == 8) {
      return ByteConverter.int8(returnValue, 0);

    } else {
      throw new PSQLException(
          GT.tr("Fastpath call {0} - No result was returned or wrong size while expecting a long.",
              name),
          PSQLState.NO_DATA);
    }
  }

  /**
   * This convenience method assumes that the return value is an oid.
   *
   * @param name Function name
   * @param args Function arguments
   * @return oid of the given call
   * @throws SQLException if a database-access error occurs or no result
   */
  public long getOID(String name, FastpathArg[] args) throws SQLException {
    long oid = getInteger(name, args);
    if (oid < 0) {
      oid += NUM_OIDS;
    }
    return oid;
  }

  /**
   * This convenience method assumes that the return value is not an Integer.
   *
   * @param name Function name
   * @param args Function arguments
   * @return byte[] array containing result
   * @throws SQLException if a database-access error occurs or no result
   */
  public byte /* @Nullable */ [] getData(String name, FastpathArg[] args) throws SQLException {
    return fastpath(name, args);
  }

  /**
   * This adds a function to our lookup table.
   *
   * <p>User code should use the addFunctions method, which is based upon a query, rather than hard
   * coding the oid. The oid for a function is not guaranteed to remain static, even on different
   * servers of the same version.</p>
   *
   * @param name Function name
   * @param fnid Function id
   */
  public void addFunction(String name, int fnid) {
    func.put(name, fnid);
  }

  /**
   * This takes a ResultSet containing two columns. Column 1 contains the function name, Column 2
   * the oid.
   *
   * <p>It reads the entire ResultSet, loading the values into the function table.</p>
   *
   * <p><b>REMEMBER</b> to close() the resultset after calling this!!</p>
   *
   * <p><b><em>Implementation note about function name lookups:</em></b></p>
   *
   * <p>PostgreSQL stores the function id's and their corresponding names in the pg_proc table. To
   * speed things up locally, instead of querying each function from that table when required, a
   * HashMap is used. Also, only the function's required are entered into this table, keeping
   * connection times as fast as possible.</p>
   *
   * <p>The org.postgresql.largeobject.LargeObject class performs a query upon it's startup, and passes
   * the returned ResultSet to the addFunctions() method here.</p>
   *
   * <p>Once this has been done, the LargeObject api refers to the functions by name.</p>
   *
   * <p>Don't think that manually converting them to the oid's will work. Ok, they will for now, but
   * they can change during development (there was some discussion about this for V7.0), so this is
   * implemented to prevent any unwarranted headaches in the future.</p>
   *
   * @param rs ResultSet
   * @throws SQLException if a database-access error occurs.
   * @see org.postgresql.largeobject.LargeObjectManager
   */
  public void addFunctions(ResultSet rs) throws SQLException {
    while (rs.next()) {
      func.put(castNonNull(rs.getString(1)), rs.getInt(2));
    }
  }

  /**
   * This returns the function id associated by its name.
   *
   * <p>If addFunction() or addFunctions() have not been called for this name, then an SQLException is
   * thrown.</p>
   *
   * @param name Function name to lookup
   * @return Function ID for fastpath call
   * @throws SQLException is function is unknown.
   */
  public int getID(String name) throws SQLException {
    Integer id = func.get(name);

    // may be we could add a lookup to the database here, and store the result
    // in our lookup table, throwing the exception if that fails.
    // We must, however, ensure that if we do, any existing ResultSet is
    // unaffected, otherwise we could break user code.
    //
    // so, until we know we can do this (needs testing, on the TODO list)
    // for now, we throw the exception and do no lookups.
    if (id == null) {
      throw new PSQLException(GT.tr("The fastpath function {0} is unknown.", name),
          PSQLState.UNEXPECTED_ERROR);
    }

    return id;
  }

  /**
   * Creates a FastpathArg with an oid parameter. This is here instead of a constructor of
   * FastpathArg because the constructor can't tell the difference between an long that's really
   * int8 and a long thats an oid.
   *
   * @param oid input oid
   * @return FastpathArg with an oid parameter
   */
  public static FastpathArg createOIDArg(long oid) {
    if (oid > Integer.MAX_VALUE) {
      oid -= NUM_OIDS;
    }
    return new FastpathArg((int) oid);
  }

}
