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

package org.postgresql.jdbc;

import org.postgresql.core.BaseConnection;
import org.postgresql.core.ServerVersion;
import org.postgresql.largeobject.LargeObject;
import org.postgresql.largeobject.LargeObjectManager;
import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;

import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Blob;
import java.sql.SQLException;
import java.util.ArrayList;

/**
 * This class holds all of the methods common to both Blobs and Clobs.
 *
 * @author <a href="mailto:mike@middlesoft.co.uk">Michael Barker</a>
 */
public abstract class AbstractBlobClob {
  protected BaseConnection conn;

  private LargeObject currentLo;
  private boolean currentLoIsWriteable;
  private boolean support64bit;

  /**
   * We create separate LargeObjects for methods that use streams so they won't interfere with each
   * other.
   */
  private ArrayList<LargeObject> subLOs;

  private final long oid;

  public AbstractBlobClob(BaseConnection conn, long oid) throws SQLException {
    this.conn = conn;
    this.oid = oid;
    this.currentLo = null;
    this.currentLoIsWriteable = false;

    support64bit = conn.haveMinimumServerVersion(90300);

    subLOs = new ArrayList<LargeObject>();
  }

  public synchronized void free() throws SQLException {
    if (currentLo != null) {
      currentLo.close();
      currentLo = null;
      currentLoIsWriteable = false;
    }
    for (LargeObject subLO : subLOs) {
      subLO.close();
    }
    subLOs = null;
  }

  /**
   * For Blobs this should be in bytes while for Clobs it should be in characters. Since we really
   * haven't figured out how to handle character sets for Clobs the current implementation uses
   * bytes for both Blobs and Clobs.
   *
   * @param len maximum length
   * @throws SQLException if operation fails
   */
  public synchronized void truncate(long len) throws SQLException {
    checkFreed();
    if (!conn.haveMinimumServerVersion(ServerVersion.v8_3)) {
      throw new PSQLException(
          GT.tr("Truncation of large objects is only implemented in 8.3 and later servers."),
          PSQLState.NOT_IMPLEMENTED);
    }

    if (len < 0) {
      throw new PSQLException(GT.tr("Cannot truncate LOB to a negative length."),
          PSQLState.INVALID_PARAMETER_VALUE);
    }
    if (len > Integer.MAX_VALUE) {
      if (support64bit) {
        getLo(true).truncate64(len);
      } else {
        throw new PSQLException(GT.tr("PostgreSQL LOBs can only index to: {0}", Integer.MAX_VALUE),
            PSQLState.INVALID_PARAMETER_VALUE);
      }
    } else {
      getLo(true).truncate((int) len);
    }
  }

  public synchronized long length() throws SQLException {
    checkFreed();
    if (support64bit) {
      return getLo(false).size64();
    } else {
      return getLo(false).size();
    }
  }

  public synchronized byte[] getBytes(long pos, int length) throws SQLException {
    assertPosition(pos);
    getLo(false).seek((int) (pos - 1), LargeObject.SEEK_SET);
    return getLo(false).read(length);
  }

  public synchronized InputStream getBinaryStream() throws SQLException {
    checkFreed();
    LargeObject subLO = getLo(false).copy();
    addSubLO(subLO);
    subLO.seek(0, LargeObject.SEEK_SET);
    return subLO.getInputStream();
  }

  public synchronized OutputStream setBinaryStream(long pos) throws SQLException {
    assertPosition(pos);
    LargeObject subLO = getLo(true).copy();
    addSubLO(subLO);
    subLO.seek((int) (pos - 1));
    return subLO.getOutputStream();
  }

  /**
   * Iterate over the buffer looking for the specified pattern.
   *
   * @param pattern A pattern of bytes to search the blob for
   * @param start The position to start reading from
   * @return position of the specified pattern
   * @throws SQLException if something wrong happens
   */
  public synchronized long position(byte[] pattern, long start) throws SQLException {
    assertPosition(start, pattern.length);

    int position = 1;
    int patternIdx = 0;
    long result = -1;
    int tmpPosition = 1;

    for (LOIterator i = new LOIterator(start - 1); i.hasNext(); position++) {
      byte b = i.next();
      if (b == pattern[patternIdx]) {
        if (patternIdx == 0) {
          tmpPosition = position;
        }
        patternIdx++;
        if (patternIdx == pattern.length) {
          result = tmpPosition;
          break;
        }
      } else {
        patternIdx = 0;
      }
    }

    return result;
  }

  /**
   * Iterates over a large object returning byte values. Will buffer the data from the large object.
   */
  private class LOIterator  {
    private static final int BUFFER_SIZE = 8096;
    private byte[] buffer = new byte[BUFFER_SIZE];
    private int idx = BUFFER_SIZE;
    private int numBytes = BUFFER_SIZE;

    LOIterator(long start) throws SQLException {
      getLo(false).seek((int) start);
    }

    public boolean hasNext() throws SQLException {
      boolean result;
      if (idx < numBytes) {
        result = true;
      } else {
        numBytes = getLo(false).read(buffer, 0, BUFFER_SIZE);
        idx = 0;
        result = (numBytes > 0);
      }
      return result;
    }

    private byte next() {
      return buffer[idx++];
    }
  }

  /**
   * This is simply passing the byte value of the pattern Blob.
   *
   * @param pattern search pattern
   * @param start start position
   * @return position of given pattern
   * @throws SQLException if something goes wrong
   */
  public synchronized long position(Blob pattern, long start) throws SQLException {
    return position(pattern.getBytes(1, (int) pattern.length()), start);
  }

  /**
   * Throws an exception if the pos value exceeds the max value by which the large object API can
   * index.
   *
   * @param pos Position to write at.
   * @throws SQLException if something goes wrong
   */
  protected void assertPosition(long pos) throws SQLException {
    assertPosition(pos, 0);
  }

  /**
   * Throws an exception if the pos value exceeds the max value by which the large object API can
   * index.
   *
   * @param pos Position to write at.
   * @param len number of bytes to write.
   * @throws SQLException if something goes wrong
   */
  protected void assertPosition(long pos, long len) throws SQLException {
    checkFreed();
    if (pos < 1) {
      throw new PSQLException(GT.tr("LOB positioning offsets start at 1."),
          PSQLState.INVALID_PARAMETER_VALUE);
    }
    if (pos + len - 1 > Integer.MAX_VALUE) {
      throw new PSQLException(GT.tr("PostgreSQL LOBs can only index to: {0}", Integer.MAX_VALUE),
          PSQLState.INVALID_PARAMETER_VALUE);
    }
  }

  /**
   * Checks that this LOB hasn't been free()d already.
   *
   * @throws SQLException if LOB has been freed.
   */
  protected void checkFreed() throws SQLException {
    if (subLOs == null) {
      throw new PSQLException(GT.tr("free() was called on this LOB previously"),
          PSQLState.OBJECT_NOT_IN_STATE);
    }
  }

  protected synchronized LargeObject getLo(boolean forWrite) throws SQLException {
    if (this.currentLo != null) {
      if (forWrite && !currentLoIsWriteable) {
        // Reopen the stream in read-write, at the same pos.
        int currentPos = this.currentLo.tell();

        LargeObjectManager lom = conn.getLargeObjectAPI();
        LargeObject newLo = lom.open(oid, LargeObjectManager.READWRITE);
        this.subLOs.add(this.currentLo);
        this.currentLo = newLo;

        if (currentPos != 0) {
          this.currentLo.seek(currentPos);
        }
      }

      return this.currentLo;
    }
    LargeObjectManager lom = conn.getLargeObjectAPI();
    currentLo = lom.open(oid, forWrite ? LargeObjectManager.READWRITE : LargeObjectManager.READ);
    currentLoIsWriteable = forWrite;
    return currentLo;
  }

  protected void addSubLO(LargeObject subLO) {
    subLOs.add(subLO);
  }
}
