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

package org.postgresql.largeobject;

import org.postgresql.core.BaseConnection;
import org.postgresql.fastpath.Fastpath;
import org.postgresql.fastpath.FastpathArg;
import org.postgresql.util.ByteStreamWriter;
import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;

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

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.SQLException;

/**
 * This class provides the basic methods required to run the interface, plus a pair of methods that
 * provide InputStream and OutputStream classes for this object.
 *
 * <p>Normally, client code would use the getAsciiStream, getBinaryStream, or getUnicodeStream methods
 * in ResultSet, or setAsciiStream, setBinaryStream, or setUnicodeStream methods in
 * PreparedStatement to access Large Objects.</p>
 *
 * <p>However, sometimes lower level access to Large Objects are required, that are not supported by
 * the JDBC specification.</p>
 *
 * <p>Refer to org.postgresql.largeobject.LargeObjectManager on how to gain access to a Large Object,
 * or how to create one.</p>
 *
 * @see org.postgresql.largeobject.LargeObjectManager
 * @see java.sql.ResultSet#getAsciiStream
 * @see java.sql.ResultSet#getBinaryStream
 * @see java.sql.ResultSet#getUnicodeStream
 * @see java.sql.PreparedStatement#setAsciiStream
 * @see java.sql.PreparedStatement#setBinaryStream
 * @see java.sql.PreparedStatement#setUnicodeStream
 */
@SuppressWarnings("deprecation") // support for deprecated Fastpath API
public class LargeObject
    implements AutoCloseable {

  /**
   * Indicates a seek from the beginning of a file.
   */
  public static final int SEEK_SET = 0;

  /**
   * Indicates a seek from the current position.
   */
  public static final int SEEK_CUR = 1;

  /**
   * Indicates a seek from the end of a file.
   */
  public static final int SEEK_END = 2;

  private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];

  private final Fastpath fp; // Fastpath API to use
  private final long oid; // OID of this object
  private final int mode; // read/write mode of this object
  private final int fd; // the descriptor of the open large object

  private /* @Nullable */ BlobOutputStream os; // The current output stream

  private boolean closed; // true when we are closed

  private /* @Nullable */ BaseConnection conn; // Only initialized when open a LOB with CommitOnClose
  private final boolean commitOnClose; // Only initialized when open a LOB with CommitOnClose

  /**
   * Checks if this LargeObject is closed and throws an exception if it is.
   *
   * @throws SQLException if this LargeObject has been closed
   */
  private void checkClosed() throws SQLException {
    if (closed) {
      throw new PSQLException(GT.tr("This large object has been closed."),
          PSQLState.OBJECT_NOT_IN_STATE);
    }
  }

  /**
   * This opens a large object.
   *
   * <p>If the object does not exist, then an SQLException is thrown.</p>
   *
   * @param fp FastPath API for the connection to use
   * @param oid of the Large Object to open
   * @param mode Mode of opening the large object
   * @param conn the connection to the database used to access this LOB
   * @param commitOnClose commit the transaction when this LOB will be closed (defined in
   *        LargeObjectManager)
   * @throws SQLException if a database-access error occurs.
   * @see org.postgresql.largeobject.LargeObjectManager
   */
  protected LargeObject(Fastpath fp, long oid, int mode,
      /* @Nullable */ BaseConnection conn, boolean commitOnClose)
      throws SQLException {
    this.fp = fp;
    this.oid = oid;
    this.mode = mode;
    if (commitOnClose) {
      this.commitOnClose = true;
      this.conn = conn;
    } else {
      this.commitOnClose = false;
    }

    FastpathArg[] args = new FastpathArg[2];
    args[0] = Fastpath.createOIDArg(oid);
    args[1] = new FastpathArg(mode);
    this.fd = fp.getInteger("lo_open", args);
  }

  /**
   * This opens a large object.
   *
   * <p>If the object does not exist, then an SQLException is thrown.</p>
   *
   * @param fp FastPath API for the connection to use
   * @param oid of the Large Object to open
   * @param mode Mode of opening the large object (defined in LargeObjectManager)
   * @throws SQLException if a database-access error occurs.
   * @see org.postgresql.largeobject.LargeObjectManager
   */
  protected LargeObject(Fastpath fp, long oid, int mode) throws SQLException {
    this(fp, oid, mode, null, false);
  }

  public LargeObject copy() throws SQLException {
    checkClosed();
    return new LargeObject(fp, oid, mode);
  }

  /**
   * @return the OID of this LargeObject
   * @deprecated As of 8.3, replaced by {@link #getLongOID()}
   */
  @Deprecated
  public int getOID() {
    return (int) oid;
  }

  /**
   * @return the OID of this LargeObject
   */
  public long getLongOID() {
    return oid;
  }

  /**
   * This method closes the object. You must not call methods in this object after this is called.
   *
   * @throws SQLException if a database-access error occurs.
   */
  @Override
  public void close() throws SQLException {
    if (closed) {
      return;
    }
    // Flush while still open: BlobOutputStream.flush() calls back into LargeObject.write(),
    // which would fail checkClosed() if the object were already marked closed.
    SQLException error = null;
    if (os != null) {
      try {
        // we can't call os.close() otherwise we go into an infinite loop!
        os.flush();
      } catch (IOException ioe) {
        error = new PSQLException(GT.tr("Exception flushing output stream"),
            PSQLState.DATA_ERROR, ioe);
      } finally {
        os = null;
      }
    }

    closed = true;

    // Always release the server-side descriptor, but do not let lo_close mask a flush failure.
    try {
      FastpathArg[] args = new FastpathArg[1];
      args[0] = new FastpathArg(fd);
      fp.fastpath("lo_close", args);
    } catch (SQLException e) {
      if (error != null) {
        error.addSuppressed(e);
      } else {
        error = e;
      }
    }

    // Commit only when flush and lo_close both succeeded; never commit a half-written object.
    BaseConnection conn = this.conn;
    if (error == null && this.commitOnClose && conn != null) {
      conn.commit();
    }

    if (error != null) {
      throw error;
    }
  }

  /**
   * Reads some data from the object, and return as a byte[] array.
   *
   * @param len number of bytes to read
   * @return byte[] array containing data read
   * @throws SQLException if a database-access error occurs.
   */
  public byte[] read(int len) throws SQLException {
    checkClosed();
    // This is the original method, where the entire block (len bytes)
    // is retrieved in one go.
    FastpathArg[] args = new FastpathArg[2];
    args[0] = new FastpathArg(fd);
    args[1] = new FastpathArg(len);
    byte[] bytes = fp.getData("loread", args);
    if (bytes == null) {
      return EMPTY_BYTE_ARRAY;
    }
    return bytes;
  }

  /**
   * Reads some data from the object into an existing array.
   *
   * @param buf destination array
   * @param off offset within array
   * @param len number of bytes to read
   * @return the number of bytes actually read
   * @throws SQLException if a database-access error occurs.
   */
  public int read(byte[] buf, int off, int len) throws SQLException {
    checkClosed();
    byte[] b = read(len);
    if (b.length == 0) {
      return 0;
    }
    len = Math.min(len, b.length);
    System.arraycopy(b, 0, buf, off, len);
    return len;
  }

  /**
   * Writes an array to the object.
   *
   * @param buf array to write
   * @throws SQLException if a database-access error occurs.
   */
  public void write(byte[] buf) throws SQLException {
    checkClosed();
    FastpathArg[] args = new FastpathArg[2];
    args[0] = new FastpathArg(fd);
    args[1] = new FastpathArg(buf);
    fp.fastpath("lowrite", args);
  }

  /**
   * Writes some data from an array to the object.
   *
   * @param buf destination array
   * @param off offset within array
   * @param len number of bytes to write
   * @throws SQLException if a database-access error occurs.
   */
  public void write(byte[] buf, int off, int len) throws SQLException {
    checkClosed();
    FastpathArg[] args = new FastpathArg[2];
    args[0] = new FastpathArg(fd);
    args[1] = new FastpathArg(buf, off, len);
    fp.fastpath("lowrite", args);
  }

  /**
   * Writes some data from a given writer to the object.
   *
   * @param writer the source of the data to write
   * @throws SQLException if a database-access error occurs.
   */
  public void write(ByteStreamWriter writer) throws SQLException {
    checkClosed();
    FastpathArg[] args = new FastpathArg[2];
    args[0] = new FastpathArg(fd);
    args[1] = FastpathArg.of(writer);
    fp.fastpath("lowrite", args);
  }

  /**
   * Sets the current position within the object.
   *
   * <p>This is similar to the fseek() call in the standard C library. It allows you to have random
   * access to the large object.</p>
   *
   * @param pos position within object
   * @param ref Either SEEK_SET, SEEK_CUR or SEEK_END
   * @throws SQLException if a database-access error occurs.
   */
  public void seek(int pos, int ref) throws SQLException {
    checkClosed();
    FastpathArg[] args = new FastpathArg[3];
    args[0] = new FastpathArg(fd);
    args[1] = new FastpathArg(pos);
    args[2] = new FastpathArg(ref);
    fp.fastpath("lo_lseek", args);
  }

  /**
   * Sets the current position within the object using 64-bit value (9.3+).
   *
   * @param pos position within object
   * @param ref Either SEEK_SET, SEEK_CUR or SEEK_END
   * @throws SQLException if a database-access error occurs.
   */
  public void seek64(long pos, int ref) throws SQLException {
    checkClosed();
    FastpathArg[] args = new FastpathArg[3];
    args[0] = new FastpathArg(fd);
    args[1] = new FastpathArg(pos);
    args[2] = new FastpathArg(ref);
    fp.fastpath("lo_lseek64", args);
  }

  /**
   * Sets the current position within the object.
   *
   * <p>This is similar to the fseek() call in the standard C library. It allows you to have random
   * access to the large object.</p>
   *
   * @param pos position within object from beginning
   * @throws SQLException if a database-access error occurs.
   */
  public void seek(int pos) throws SQLException {
    checkClosed();
    seek(pos, SEEK_SET);
  }

  /**
   * @return the current position within the object
   * @throws SQLException if a database-access error occurs.
   */
  public int tell() throws SQLException {
    checkClosed();
    FastpathArg[] args = new FastpathArg[1];
    args[0] = new FastpathArg(fd);
    return fp.getInteger("lo_tell", args);
  }

  /**
   * @return the current position within the object
   * @throws SQLException if a database-access error occurs.
   */
  public long tell64() throws SQLException {
    checkClosed();
    FastpathArg[] args = new FastpathArg[1];
    args[0] = new FastpathArg(fd);
    return fp.getLong("lo_tell64", args);
  }

  /**
   * Reports whether the server supports the 64-bit large object functions {@code lo_tell64} and
   * {@code lo_lseek64}, added in PostgreSQL 9.3. Callers can use this to choose the 64-bit
   * functions by version rather than calling them and recovering from the failure.
   *
   * @return {@code true} if the server is 9.3 or newer
   */
  boolean supports64BitOffsets() {
    return fp.getServerVersionNum() >= 90300;
  }

  /**
   * This method is inefficient, as the only way to find out the size of the object is to seek to
   * the end, record the current position, then return to the original position.
   *
   * <p>A better method will be found in the future.</p>
   *
   * @return the size of the large object
   * @throws SQLException if a database-access error occurs.
   */
  public int size() throws SQLException {
    checkClosed();
    int cp = tell();
    seek(0, SEEK_END);
    int sz = tell();
    seek(cp, SEEK_SET);
    return sz;
  }

  /**
   * See #size() for information about efficiency.
   *
   * @return the size of the large object
   * @throws SQLException if a database-access error occurs.
   */
  public long size64() throws SQLException {
    checkClosed();
    long cp = tell64();
    seek64(0, SEEK_END);
    long sz = tell64();
    seek64(cp, SEEK_SET);
    return sz;
  }

  /**
   * Truncates the large object to the given length in bytes. If the number of bytes is larger than
   * the current large object length, the large object will be filled with zero bytes. This method
   * does not modify the current file offset.
   *
   * @param len given length in bytes
   * @throws SQLException if something goes wrong
   */
  public void truncate(int len) throws SQLException {
    checkClosed();
    FastpathArg[] args = new FastpathArg[2];
    args[0] = new FastpathArg(fd);
    args[1] = new FastpathArg(len);
    fp.getInteger("lo_truncate", args);
  }

  /**
   * Truncates the large object to the given length in bytes. If the number of bytes is larger than
   * the current large object length, the large object will be filled with zero bytes. This method
   * does not modify the current file offset.
   *
   * @param len given length in bytes
   * @throws SQLException if something goes wrong
   */
  public void truncate64(long len) throws SQLException {
    checkClosed();
    FastpathArg[] args = new FastpathArg[2];
    args[0] = new FastpathArg(fd);
    args[1] = new FastpathArg(len);
    fp.getInteger("lo_truncate64", args);
  }

  /**
   * Returns an {@link InputStream} from this object.
   *
   * <p>This {@link InputStream} can then be used in any method that requires an InputStream.</p>
   *
   * @return {@link InputStream} from this object
   * @throws SQLException if a database-access error occurs.
   */
  public InputStream getInputStream() throws SQLException {
    checkClosed();
    return new BlobInputStream(this);
  }

  /**
   * Returns an {@link InputStream} from this object, that will limit the amount of data that is
   * visible.
   *
   * @param limit maximum number of bytes the resulting stream will serve
   * @return {@link InputStream} from this object
   * @throws SQLException if a database-access error occurs.
   */
  public InputStream getInputStream(long limit) throws SQLException {
    checkClosed();
    return new BlobInputStream(this, BlobInputStream.DEFAULT_MAX_BUFFER_SIZE, limit);
  }

  /**
   * Returns an {@link InputStream} from this object, that will limit the amount of data that is
   * visible.
   * Added mostly for testing
   *
   * @param bufferSize buffer size for the stream
   * @param limit maximum number of bytes the resulting stream will serve
   * @return {@link InputStream} from this object
   * @throws SQLException if a database-access error occurs.
   */
  public InputStream getInputStream(int bufferSize, long limit) throws SQLException {
    checkClosed();
    return new BlobInputStream(this, bufferSize, limit);
  }

  /**
   * Returns an {@link OutputStream} to this object.
   *
   * <p>This OutputStream can then be used in any method that requires an OutputStream.</p>
   *
   * @return {@link OutputStream} from this object
   * @throws SQLException if a database-access error occurs.
   */
  public OutputStream getOutputStream() throws SQLException {
    checkClosed();
    if (os == null) {
      os = new BlobOutputStream(this);
    }
    return os;
  }
}
