/*
 * Copyright (c) 1998-2006 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Rodrigo Westrupp
 */

package com.caucho.quercus.lib.db;

import com.caucho.quercus.env.BinaryBuilderValue;
import com.caucho.quercus.env.Env;
import com.caucho.quercus.env.LongValue;
import com.caucho.quercus.env.StringBuilderValue;
import com.caucho.quercus.env.Value;

import com.caucho.quercus.module.NotNull;
import com.caucho.quercus.module.Optional;
import com.caucho.quercus.module.ReturnNullAsFalse;

import com.caucho.util.L10N;

import com.caucho.vfs.Path;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.WriteStream;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;

import java.lang.reflect.Method;

import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;

import java.util.logging.Level;
import java.util.logging.Logger;


/**
 * Quercus Oracle OCI-Lob object oriented API.
 */
public class OracleOciLob {
  private static final Logger log = Logger.getLogger(OracleOciLob.class.getName());
  private static final L10N L = new L10N(OracleOciLob.class);

  // The large object
  private Object _lob;

  // Cache the connection. See writeTemporary()
  private Oracle _conn;

  // Current position (seek)
  private long _currentPointer;

  // OCI Lob type: OCI_D_FILE, OCI_D_LOB or OCI_D_ROWID
  private int _type;

  // This could be refactored in two classes: OracleOciBlob and OracleOciClob
  private OutputStream _outputStream;
  private Writer _writer;

  // Cache classes and methods for oracle.sql.BLOB and oracle.sql.CLOB
  private static Class classOracleBLOB;
  private static Class classOracleCLOB;
  private static Method createTemporaryBLOB;
  private static Method createTemporaryCLOB;
  private static int BLOB_DURATION_CALL;
  private static int BLOB_DURATION_SESSION;
  private static int CLOB_DURATION_CALL;
  private static int CLOB_DURATION_SESSION;

  static {
    try {
      classOracleBLOB = Class.forName("oracle.sql.BLOB");
      classOracleCLOB = Class.forName("oracle.sql.CLOB");
      createTemporaryBLOB = classOracleBLOB.getDeclaredMethod("createTemporary",
                                                              new Class[] {Connection.class,
                                                                           Boolean.TYPE,
                                                                           Integer.TYPE});
      createTemporaryCLOB = classOracleCLOB.getDeclaredMethod("createTemporary",
                                                              new Class[] {Connection.class,
                                                                           Boolean.TYPE,
                                                                           Integer.TYPE});
      BLOB_DURATION_CALL = classOracleBLOB.getDeclaredField("DURATION_CALL").getInt(null);
      BLOB_DURATION_SESSION = classOracleBLOB.getDeclaredField("DURATION_SESSION").getInt(null);
      CLOB_DURATION_CALL = classOracleCLOB.getDeclaredField("DURATION_CALL").getInt(null);
      CLOB_DURATION_SESSION = classOracleCLOB.getDeclaredField("DURATION_SESSION").getInt(null);
    } catch (Exception e) {
      L.l("Unable to load LOB classes or methods for oracle.sql.BLOB and oracle.sql.CLOB.");
    }
  }


  /**
   * Constructor for OracleOciLob
   *
   * @param type one of the following types:
   *
   * OCI_D_FILE - a FILE descriptor
   *
   * OCI_D_LOB - a LOB descriptor
   *
   * OCI_D_ROWID - a ROWID descriptor
   */
  OracleOciLob(Oracle conn, int type)
  {
    _conn = conn;
    _lob = null;
    _currentPointer = 0;
    _type = type;
    _outputStream = null;
    _writer = null;
  }

  /**
   * Appends data from the large object to another large object
   */
  public boolean append(Env env,
                        OracleOciLob lobFrom)
  {
    try {

      switch (_type) {
      case OracleModule.OCI_D_FILE:
        break;
      case OracleModule.OCI_D_LOB:
        if (_lob instanceof Blob) {
          Blob blob = (Blob) _lob;
          return appendInternalBlob(env, lobFrom);
        } else if (_lob instanceof Clob) {
          Clob clob = (Clob) _lob;
          return appendInternalClob(env, lobFrom);
        }
        break;
      case OracleModule.OCI_D_ROWID:
        break;
      }

      return true;

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return false;
    }
  }

  /**
   * Closes LOB descriptor
   */
  public boolean close(Env env)
  {
    try {

      _currentPointer = 0;

      if (_outputStream != null) {
        _outputStream.close();
        _outputStream = null;
      }

      if (_writer != null) {
        _writer.close();
        _writer = null;
      }

      return true;

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return false;
    }
  }

  /**
   * Tests for end-of-file on a large object's descriptor
   */
  public boolean eof(Env env)
  {
    try {

      long length = -1;

      switch (_type) {
      case OracleModule.OCI_D_FILE:
        break;
      case OracleModule.OCI_D_LOB:
        if (_lob instanceof Blob) {
          Blob blob = (Blob) _lob;
          length = blob.length();
        } else if (_lob instanceof Clob) {
          Clob clob = (Clob) _lob;
          length = clob.length();
        }
        break;
      case OracleModule.OCI_D_ROWID:
        break;
      }

      if (_currentPointer == length) {
        return true;
      }

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
    }

    return false;
  }

  /**
   * Erases a specified portion of the internal LOB data
   *
   * @return the actual number of characters/bytes erased or
   * FALSE in case of error.
   */
  @ReturnNullAsFalse
  public LongValue erase(Env env,
                         @Optional("0") long offset,
                         @Optional("-1") long length)
  {
    try {

      if (offset < 0) {
        offset = 0;
      }

      switch (_type) {
      case OracleModule.OCI_D_FILE:
        break;
      case OracleModule.OCI_D_LOB:
        if (_lob instanceof Blob) {
          Blob blob = (Blob) _lob;
          if (_outputStream != null) {
            _outputStream.close();
          }
          _outputStream = blob.setBinaryStream(offset);
          long blobLength = blob.length();
          if ((length < 0) || (offset + length > blobLength)) {
            length = blobLength - offset;
          }
          long remaining = length;
          byte zeroBuffer[] = new byte[128];
          while (remaining >= 128) {
            _outputStream.write(zeroBuffer, 0, 128);
            remaining -= 128;
          }
          if (remaining > 0) {
            _outputStream.write(zeroBuffer, 0, (int) remaining);
          }
        } else if (_lob instanceof Clob) {
          Clob clob = (Clob) _lob;
          if (_writer != null) {
            _writer.close();
          }
          _writer = clob.setCharacterStream(offset);
          long clobLength = clob.length();
          if ((length < 0) || (offset + length > clobLength)) {
            length = clobLength - offset;
          }
          long remaining = length;
          char spaceBuffer[] = new char[128];
          while (remaining >= 128) {
            _writer.write(spaceBuffer, 0, 128);
            remaining -= 128;
          }
          if (remaining > 0) {
            _writer.write(spaceBuffer, 0, (int) remaining);
          }
        }
        _currentPointer = offset + length;
        break;
      case OracleModule.OCI_D_ROWID:
        break;
      }

      if (length > 0) {
        return LongValue.create(length);
      }

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
    }

    return null;
  }

  /**
   * Exports LOB's contents to a file
   */
  public boolean export(Env env,
                        Path file,
                        @Optional("0") long start,
                        @Optional("-1") long length)
  {
    try {

      WriteStream writeStream = file.openWrite();

      if (_lob instanceof Blob) {
        Blob blob = (Blob) _lob;
        InputStream is = blob.getBinaryStream();
        is.skip(start);
        writeStream.writeStream(is);
        is.close();
      } else if (_lob instanceof Clob) {
        Clob clob = (Clob) _lob;
        Reader reader = clob.getCharacterStream();
        reader.skip(start);
        writeStream.writeStream(reader);
        reader.close();
      } else {
        writeStream.close();
        return false;
      }

      writeStream.close();
      return true;

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return false;
    }
  }

  /**
   * Flushes/writes buffer of the LOB to the server
   */
  public boolean flush(Env env,
                       @Optional("-1") int flag)
  {
    try {

      if (_outputStream != null) {
        _outputStream.flush();
      }

      if (_writer != null) {
        _writer.flush();
      }

      if (flag == OracleModule.OCI_LOB_BUFFER_FREE) {
        close(env);
      }

      return true;

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return false;
    }
  }


  /**
   * Frees resources associated with the LOB descriptor
   */
  public boolean free(Env env)
  {
    try {

      _lob = null;

      return true;

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return false;
    }
  }

  /**
   * Returns current state of buffering for the large object
   */
  public boolean getBuffering(Env env)
  {
    // XXX: we assume buffering is always turned on.
    return true;
  }

  /**
   * Imports file data to the LOB
   */
  public boolean quercus_import(Env env,
                                Path file)
  {
    try {

      ReadStream readStream = file.openRead();

      if (_lob instanceof Blob) {
        Blob blob = (Blob) _lob;
        blob.truncate(0);
        if (_outputStream != null) {
          _outputStream.close();
        }
        _outputStream = blob.setBinaryStream(0);
        long nbytes;
        byte buffer[] = new byte[128];
        while ((nbytes = readStream.read(buffer, 0, 128)) > 0) {
          _outputStream.write(buffer, 0, (int) nbytes);
          _currentPointer += nbytes;
        }
      } else if (_lob instanceof Clob) {
        Clob clob = (Clob) _lob;
        clob.truncate(0);
        if (_writer != null) {
          _writer.close();
        }
        _writer = clob.setCharacterStream(0);
        long nchars;
        char buffer[] = new char[128];
        while ((nchars = readStream.read(buffer, 0, 128)) > 0) {
          _writer.write(buffer, 0, (int) nchars);
          _currentPointer += nchars;
        }
      } else {
        readStream.close();
        return false;
      }

      readStream.close();
      return true;

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return false;
    }
  }

  /**
   * Returns large object's contents
   */
  @ReturnNullAsFalse
  public Object load(Env env)
  {
    try {
      switch (_type) {
      case OracleModule.OCI_D_FILE:
        break;
      case OracleModule.OCI_D_LOB:
        if (_lob instanceof Blob) {
          return readInternalBlob(env, -1);
        } else if (_lob instanceof Clob) {
          return readInternalClob(env, -1);
        }
        break;
      case OracleModule.OCI_D_ROWID:
        break;
      }

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
    }

    return null;
  }

  /**
   * Reads part of the large object
   */
  @ReturnNullAsFalse
  public Object read(Env env,
                     long length)
  {
    try {

      switch (_type) {
      case OracleModule.OCI_D_FILE:
        break;
      case OracleModule.OCI_D_LOB:
        if (_lob instanceof Blob) {
          return readInternalBlob(env, length);
        } else if (_lob instanceof Clob) {
          return readInternalClob(env, length);
        }
        break;
      case OracleModule.OCI_D_ROWID:
        break;
      }

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
    }

    return null;
  }

  /**
   * Moves the internal pointer to the beginning of the large object
   */
  public boolean rewind(Env env)
  {
    return seek(env, 0, OracleModule.OCI_SEEK_SET);
  }


  /**
   * Saves data to the large object
   */
  public boolean save(Env env,
                      @NotNull String data,
                      @Optional("0") long offset)
  {
    try {

      switch (_type) {
      case OracleModule.OCI_D_FILE:
        break;
      case OracleModule.OCI_D_LOB:
        if (_lob instanceof Blob) {
          Blob blob = (Blob) _lob;
          if (_outputStream != null) {
            _outputStream.close();
          }
          _outputStream = blob.setBinaryStream(offset);
          _outputStream.write(data.getBytes());
        } else if (_lob instanceof Clob) {
          Clob clob = (Clob) _lob;
          if (_writer != null) {
            _writer.close();
          }
          _writer = clob.setCharacterStream(offset);
          _writer.write(data);
        }
        _currentPointer = offset + data.length();
        break;
      case OracleModule.OCI_D_ROWID:
        break;
      }

      return true;

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return false;
    }
  }

  /**
   * Alias of import()
   */
  public boolean saveFile(Env env,
                          Path file)
  {
    return quercus_import(env, file);
  }

  /**
   * Sets the internal pointer of the large object
   */
  public boolean seek(Env env,
                      long offset,
                      @Optional("-1") int whence)
  {
    try {

      switch (whence) {
      case OracleModule.OCI_SEEK_SET:
        _currentPointer = offset;
        break;
      case OracleModule.OCI_SEEK_END:
        long length = 0;
        if (_lob instanceof Blob) {
          length = ((Blob) _lob).length();
        } else if (_lob instanceof Clob) {
          length = ((Clob) _lob).length();
        } else {
          L.l("Unable to determine large object's length trying to seek with OCI_SEEK_END");
          return false;
        }
        _currentPointer = length + offset;
        break;
      default: // OCI_SEEK_CUR
        _currentPointer += offset;
      }

      return true;

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return false;
    }
  }

  /**
   * Changes current state of buffering for the large object
   */
  public boolean setBuffering(Env env,
                              boolean onOff)
  {
    // XXX: we assume buffering is always turned on.
    return true;
  }


  /**
   * Sets the underlying LOB
   */
  protected void setLob(Object lob) {
    _lob = lob;
  }

  /**
   * Returns size of large object
   */
  @ReturnNullAsFalse
  public LongValue size(Env env)
  {
    try {

      switch (_type) {
      case OracleModule.OCI_D_FILE:
        break;
      case OracleModule.OCI_D_LOB:
        if (_lob instanceof Blob) {
          Blob blob = (Blob) _lob;
          return LongValue.create(blob.length());
        } else if (_lob instanceof Clob) {
          Clob clob = (Clob) _lob;
          return LongValue.create(clob.length());
        }
        break;
      case OracleModule.OCI_D_ROWID:
        break;
      }

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
    }

    return null;
  }

  /**
   * Returns current position of internal pointer of large object
   */
  @ReturnNullAsFalse
  public LongValue tell(Env env)
  {
    try {

      return LongValue.create(_currentPointer);

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return null;
    }
  }

  public String toString() {

    String typeName = "UNKNOWN";

    switch (_type) {
    case OracleModule.OCI_D_FILE:
      typeName = "OCI_D_FILE";
      break;
    case OracleModule.OCI_D_LOB:
      typeName = "OCI_D_LOB";
      break;
    case OracleModule.OCI_D_ROWID:
      typeName = "OCI_D_ROWID";
      break;
    }

    return "OracleOciLob("+typeName+")";
  }

  /**
   * Truncates large object
   */
  public boolean truncate(Env env,
                          @Optional("0") long length)
  {
    try {

      switch (_type) {
      case OracleModule.OCI_D_FILE:
        break;
      case OracleModule.OCI_D_LOB:
        if (_lob instanceof Blob) {
          Blob blob = (Blob) _lob;
          blob.truncate(length);
        } else if (_lob instanceof Clob) {
          Clob clob = (Clob) _lob;
          clob.truncate(length);
        }
        break;
      case OracleModule.OCI_D_ROWID:
        break;
      }

      return true;

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return false;
    }
  }

  /**
   * Writes data to the large object
   */
  @ReturnNullAsFalse
  public LongValue write(Env env,
                         String data,
                         @Optional("-1") long length)
  {
    try {

      long dataLength = data.length();

      if ((length < 0) || (length > dataLength)) {
        length = dataLength;
      }

      switch (_type) {
      case OracleModule.OCI_D_FILE:
        break;
      case OracleModule.OCI_D_LOB:
        long written = 0;
        if (_lob instanceof Blob) {
          Blob blob = (Blob) _lob;
          if (_outputStream == null) {
            _outputStream = blob.setBinaryStream(0);
          }
          _outputStream.write(data.getBytes());
        } else if (_lob instanceof Clob) {
          Clob clob = (Clob) _lob;
          if (_writer == null) {
            _writer = clob.setCharacterStream(0);
          }
          _writer.write(data);
        }
        _currentPointer += length;
        break;
      case OracleModule.OCI_D_ROWID:
        break;
      }

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
    }

    return null;
  }

  /**
   * Writes temporary large object
   */
  public boolean writeTemporary(Env env,
                                String data,
                                @Optional("-1") int lobType)
  {
    try {

      if (_type != OracleModule.OCI_D_LOB) {
        L.l("Unable to write a temporary LOB into a non-lob object");
        return false;
      }

      if (lobType == OracleModule.OCI_TEMP_BLOB) {
        _lob = createTemporaryBLOB.invoke(classOracleBLOB,
                                          new Object[] {_conn,
                                                        true,
                                                        BLOB_DURATION_SESSION});
      } else {
        _lob = createTemporaryCLOB.invoke(classOracleCLOB,
                                          new Object[] {_conn,
                                                        true,
                                                        CLOB_DURATION_SESSION});
      }

      return true;

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return false;
    }
  }

  /**
   * Alias of export()
   */
  public boolean writeToFile(Env env,
                             Path file,
                             @Optional("0") long start,
                             @Optional("-1") long length)
  {
    return export(env, file, start, length);
  }

  private boolean appendInternalBlob(Env env,
                                     OracleOciLob lobFrom)
  {
    try {

      Blob blob = (Blob) _lob;
      long blobLength = blob.length();
      if (_currentPointer != blobLength) {
        if (_outputStream != null) {
          _outputStream.close();
        }
        _outputStream = blob.setBinaryStream(blobLength);
        _currentPointer = blobLength;
      }

      Blob blobFrom = (Blob) lobFrom;
      InputStream is = blobFrom.getBinaryStream();

      long nbytes;
      byte buffer[] = new byte[128];
      while ((nbytes = is.read(buffer)) > 0) {
        _outputStream.write(buffer, 0, (int) nbytes);
        _currentPointer += nbytes;
      }

      is.close();
      // Keep this output stream open to be reused.

      return true;

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return false;
    }
  }

  private boolean appendInternalClob(Env env,
                                     OracleOciLob lobFrom)
  {
    try {

      Clob clob = (Clob) _lob;
      long clobLength = clob.length();
      if (_currentPointer != clobLength) {
        if (_writer != null) {
          _writer.close();
        }
        _writer = clob.setCharacterStream(clobLength);
        _currentPointer = clobLength;
      }

      Clob clobFrom = (Clob) lobFrom;
      Reader reader = clobFrom.getCharacterStream();

      long nchars;
      char buffer[] = new char[128];
      while ((nchars = reader.read(buffer)) > 0) {
        _writer.write(buffer, 0, (int) nchars);
        _currentPointer += nchars;
      }

      reader.close();
      // Keep this writer open to be reused.

      return true;

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return false;
    }
  }

  private BinaryBuilderValue readInternalBlob(Env env,
                                              long length)
  {
    try {

      BinaryBuilderValue contents = new BinaryBuilderValue();

      Blob blob = (Blob) _lob;
      InputStream is = blob.getBinaryStream();
      is.skip(_currentPointer);

      long remaining = length < 0 ? Integer.MAX_VALUE : length;

      long nbytes;
      byte buffer[] = new byte[128];
      while ( (remaining > 0) &&
              ((nbytes = is.read(buffer)) > 0) ) {
        if (nbytes > remaining)
          nbytes = remaining;
        contents.append(buffer, 0, (int) nbytes);
        remaining -= nbytes;
      }

      is.close();

      return contents;

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return null;
    }
  }

  private StringBuilderValue readInternalClob(Env env,
                                              long length)
  {
    try {

      StringBuilderValue contents = new StringBuilderValue();

      Clob clob = (Clob) _lob;
      Reader reader = clob.getCharacterStream();
      reader.skip(_currentPointer);

      long remaining = length < 0 ? Integer.MAX_VALUE : length;

      long nchars;
      char buffer[] = new char[128];
      while ( (remaining > 0) &&
              ((nchars = reader.read(buffer)) > 0) ) {
        if (nchars > remaining)
          nchars = remaining;
        contents.append(buffer, 0, (int) nchars);
        remaining -= nchars;
      }

      reader.close();

      return contents;

    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return null;
    }
  }
}
