/*
 * 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.ArrayValue;
import com.caucho.quercus.env.ArrayValueImpl;
import com.caucho.quercus.env.BinaryBuilderValue;
import com.caucho.quercus.env.BooleanValue;
import com.caucho.quercus.env.Env;
import com.caucho.quercus.env.LongValue;
import com.caucho.quercus.env.NullValue;
import com.caucho.quercus.env.ResourceValue;
import com.caucho.quercus.env.StringBuilderValue;
import com.caucho.quercus.env.StringValue;
import com.caucho.quercus.env.StringValueImpl;
import com.caucho.quercus.env.Value;

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

import com.caucho.quercus.UnimplementedException;

import com.caucho.sql.UserConnection;

import com.caucho.util.L10N;
import com.caucho.util.Log;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import java.sql.Array;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.sql.SQLException;
import java.sql.Types;

import java.util.HashMap;

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

import java.util.Map;

import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * Quercus oracle routines.
 *
 * NOTE from php.net:
 *
 * "...
 * These functions allow you to access Oracle 10, Oracle 9, Oracle 8 and Oracle 7
 * databases using the Oracle Call Interface (OCI). They support binding of PHP
 * variables to Oracle placeholders, have full LOB, FILE and ROWID support, and
 * allow you to use user-supplied define variables.
 *
 * Requirements
 *
 * You will need the Oracle client libraries to use this extension.
 * Windows users will need libraries with version at least 10 to use the php_oci8.dll.
 *
 * ..."
 *
 */
public class OracleModule extends AbstractQuercusModule {
  private static final Logger log = Log.open(OracleModule.class);
  private static final L10N L = new L10N(OracleModule.class);

  // WARNING: Do not change order or constant values.
  // They are mapped to oracle types below.
  // See arrayPhpToOracleType.
  public static final int OCI_B_BFILE                    = 0x01;
  public static final int OCI_B_CFILEE                   = 0x02;
  public static final int OCI_B_CLOB                     = 0x03;
  public static final int OCI_B_BLOB                     = 0x04;
  public static final int OCI_B_ROWID                    = 0x05;
  public static final int OCI_B_CURSOR                   = 0x06;
  public static final int OCI_B_NTY                      = 0x07;
  public static final int OCI_B_BIN                      = 0x08;
  public static final int OCI_DTYPE_FILE                 = 0x09;
  public static final int OCI_DTYPE_LOB                  = 0x0A;
  public static final int OCI_DTYPE_ROWID                = 0x0B;
  public static final int OCI_D_FILE                     = 0x0C;
  public static final int OCI_D_LOB                      = 0x0D;
  public static final int OCI_D_ROWID                    = 0x0E;
  public static final int OCI_SYSDATE                    = 0x0F;
  public static final int OCI_TEMP_CLOB                  = 0x10;
  public static final int OCI_TEMP_BLOB                  = 0x11;
  public static final int SQLT_BFILEE                    = 0x12;
  public static final int SQLT_CFILEE                    = 0x13;
  public static final int SQLT_CLOB                      = 0x14;
  public static final int SQLT_BLOB                      = 0x15;
  public static final int SQLT_RDD                       = 0x16;
  public static final int SQLT_NTY                       = 0x17;
  public static final int SQLT_LNG                       = 0x18;
  public static final int SQLT_LBI                       = 0x19;
  public static final int SQLT_BIN                       = 0x1A;
  public static final int SQLT_NUM                       = 0x1B;
  public static final int SQLT_INT                       = 0x1C;
  public static final int SQLT_AFC                       = 0x1D;
  public static final int SQLT_CHR                       = 0x1E;
  public static final int SQLT_VCS                       = 0x1F;
  public static final int SQLT_AVC                       = 0x20;
  public static final int SQLT_STR                       = 0x21;
  public static final int SQLT_LVC                       = 0x22;
  public static final int SQLT_FLT                       = 0x23;
  public static final int SQLT_ODT                       = 0x24;
  public static final int SQLT_BDOUBLE                   = 0x25;
  public static final int SQLT_BFLOAT                    = 0x26;
  public static final int SQLT_RSET                      = 0x27;
  public static final int SQLT_FILE                      = 0x28;
  public static final int SQLT_CFILE                     = 0x29;

  // Reserved for future types and extensions
  // 0x30 - 0x4F

  // OCI Control Constants 0x50 - ...
  public static final int OCI_DEFAULT                    = 0x50;
  public static final int OCI_DESCRIBE_ONLY              = 0x51;
  public static final int OCI_COMMIT_ON_SUCCESS          = 0x52;
  public static final int OCI_EXACT_FETCH                = 0x53;
  public static final int OCI_FETCHSTATEMENT_BY_COLUMN   = 0x54;
  public static final int OCI_FETCHSTATEMENT_BY_ROW      = 0x55;
  public static final int OCI_ASSOC                      = 0x56;
  public static final int OCI_NUM                        = 0x57;
  public static final int OCI_BOTH                       = 0x58;
  public static final int OCI_RETURN_NULLS               = 0x59;
  public static final int OCI_RETURN_LOBS                = 0x5A;
  public static final int OCI_SYSOPER                    = 0x5B;
  public static final int OCI_SYSDBA                     = 0x5C;
  public static final int OCI_LOB_BUFFER_FREE            = 0x5D;
  public static final int OCI_SEEK_SET                   = 0x5E;
  public static final int OCI_SEEK_CUR                   = 0x5F;
  public static final int OCI_SEEK_END                   = 0x6A;


  // Cache class oracle.jdbc.OracleTypes to be used below.
  private static Class classOracleTypes;

  // Map php to oracle type
  private static int arrayPhpToOracleType[];

  static {
    try {
      classOracleTypes = Class.forName("oracle.jdbc.OracleTypes");

      arrayPhpToOracleType = new int[] {
        -1,
        classOracleTypes.getDeclaredField("BFILE").getInt(null), // OCI_B_BFILE
        -1, // OCI_B_CFILEE
        classOracleTypes.getDeclaredField("CLOB").getInt(null), // OCI_B_CLOB
        classOracleTypes.getDeclaredField("BLOB").getInt(null), // OCI_B_BLOB
        classOracleTypes.getDeclaredField("ROWID").getInt(null), // OCI_B_ROWID
        classOracleTypes.getDeclaredField("CURSOR").getInt(null), // OCI_B_CURSOR
        classOracleTypes.getDeclaredField("OTHER").getInt(null), // OCI_B_NTY
        classOracleTypes.getDeclaredField("RAW").getInt(null), // OCI_B_BIN
        -1, // OCI_DTYPE_FILE
        -1, // OCI_DTYPE_LOB
        -1, // OCI_DTYPE_ROWID
        -1, // OCI_D_FILE
        -1, // OCI_D_LOB
        -1, // OCI_D_ROWID
        classOracleTypes.getDeclaredField("TIMESTAMP").getInt(null), // OCI_SYSDATE
        -1, // OCI_TEMP_CLOB
        -1, // OCI_TEMP_BLOB
        classOracleTypes.getDeclaredField("BFILE").getInt(null), // SQLT_BFILEE
        -1, // SQLT_CFILEE
        classOracleTypes.getDeclaredField("CLOB").getInt(null), // SQLT_CLOB
        classOracleTypes.getDeclaredField("BLOB").getInt(null), // SQLT_BLOB
        classOracleTypes.getDeclaredField("ROWID").getInt(null), // SQLT_RDD
        classOracleTypes.getDeclaredField("OTHER").getInt(null), // SQLT_NTY
        classOracleTypes.getDeclaredField("NUMBER").getInt(null), // SQLT_LNG
        classOracleTypes.getDeclaredField("RAW").getInt(null), // SQLT_LBI
        classOracleTypes.getDeclaredField("RAW").getInt(null), // SQLT_BIN
        classOracleTypes.getDeclaredField("NUMBER").getInt(null), // SQLT_NUM
        classOracleTypes.getDeclaredField("INTEGER").getInt(null), // SQLT_INT
        classOracleTypes.getDeclaredField("CHAR").getInt(null), // SQLT_AFC
        classOracleTypes.getDeclaredField("CHAR").getInt(null), // SQLT_CHR
        classOracleTypes.getDeclaredField("VARCHAR").getInt(null), // SQLT_VCS
        classOracleTypes.getDeclaredField("CHAR").getInt(null), // SQLT_AVC
        classOracleTypes.getDeclaredField("VARCHAR").getInt(null), // SQLT_STR
        classOracleTypes.getDeclaredField("LONGVARCHAR").getInt(null), // SQLT_LVC
        classOracleTypes.getDeclaredField("FLOAT").getInt(null), // SQLT_FLT
        classOracleTypes.getDeclaredField("DATE").getInt(null), // SQLT_ODT
        classOracleTypes.getDeclaredField("DOUBLE").getInt(null), // SQLT_BDOUBLE
        classOracleTypes.getDeclaredField("FLOAT").getInt(null), // SQLT_BFLOAT
        classOracleTypes.getDeclaredField("CURSOR").getInt(null), // SQLT_RSET
        classOracleTypes.getDeclaredField("BFILE").getInt(null), // SQLT_FILE
        -1 // SQLT_CFILE
      };
    } catch (Exception e) {
      L.l("Unable to load Oracle types from oracle.jdbc.OracleTypes. Check your Oracle JDBC driver version.");
    }
  }

  public OracleModule()
  {
  }

  /**
   * Returns true for the oracle extension.
   */
  public String []getLoadedExtensions()
  {
    return new String[] { "oracle" };
  }

  /**
   * Binds PHP array to Oracle PL/SQL array by name.
   *
   * oci_bind_array_by_name() binds the PHP array
   * varArray to the Oracle placeholder name, which
   * points to Oracle PL/SQL array. Whether it will
   * be used for input or output will be determined
   * at run-time. The maxTableLength parameter sets
   * the maximum length both for incoming and result
   * arrays. Parameter maxItemLength sets maximum
   * length for array items. If maxItemLength was
   * not specified or equals to -1,
   * oci_bind_array_by_name() will find the longest
   * element in the incoming array and will use it as
   * maximum length for array items. type parameter
   * should be used to set the type of PL/SQL array
   * items. See list of available types below.
   *
   * @param env the PHP executing environment
   * @param stmt the Oracle statement
   * @param name the Oracle placeholder
   * @param varArray the array to be binded
   * @param maxTableLength maximum table length
   * @param maxItemLength maximum item length
   * @param type one of the following types:
   * <br/>
   * SQLT_NUM - for arrays of NUMBER.
   * <br/>
   * SQLT_INT - for arrays of INTEGER
   * (Note: INTEGER it is actually a synonym for
   *  NUMBER(38), but SQLT_NUM type won't work in
   *  this case even though they are synonyms).
   * <br/>
   * SQLT_FLT - for arrays of FLOAT.
   * <br/>
   * SQLT_AFC - for arrays of CHAR.
   * <br/>
   * SQLT_CHR - for arrays of VARCHAR2.
   * <br/>
   * SQLT_VCS - for arrays of VARCHAR.
   * <br/>
   * SQLT_AVC - for arrays of CHARZ.
   * <br/>
   * SQLT_STR - for arrays of STRING.
   * <br/>
   * SQLT_LVC - for arrays of LONG VARCHAR.
   * <br/>
   * SQLT_ODT - for arrays of DATE.
   *
   * @return true on success of false on failure
   */
  public static boolean oci_bind_array_by_name(Env env,
                                               @NotNull OracleStatement stmt,
                                               @NotNull String name,
                                               @NotNull ArrayValue varArray,
                                               @NotNull int maxTableLength,
                                               @Optional("0") int maxItemLength,
                                               @Optional("0") int type)
  {
    try {

      // JDBC underlying connection
      Connection conn = stmt.getJavaConnection();

      // Oracle underlying statement
      PreparedStatement oracleStmt = stmt.getPreparedStatement();

      // Create an oracle.sql.ARRAY object to hold the values
      // oracle.sql.ArrayDescriptor arrayDesc =
      //   oracle.sql.ArrayDescriptor.createDescriptor("number_varray", conn);


      Class clArrayDescriptor = Class.forName("oracle.sql.ArrayDescriptor");

      Method method
        = clArrayDescriptor.getDeclaredMethod("createDescriptor",
                                              new Class[] {String.class, Connection.class});

      Object arrayDesc = method.invoke(clArrayDescriptor,
                                       new Object[] {"NUMBER_VARRAY", conn});

      Value valueArray[] = varArray.valuesToArray(); // int arrayValues[] = {123, 234};

      Object objectArray[] = new Object[5]; // {"aaa", "bbb", "ccc"};
      for (int i=0; i<valueArray.length; i++) {
        Object obj = valueArray[i].toJavaObject();
        objectArray[i] = obj;
      }

      // oracle.sql.ARRAY array = new oracle.sql.ARRAY(arrayDesc, conn, arrayValues);

      Class clARRAY = Class.forName("oracle.sql.ARRAY");

      Constructor constructor = clARRAY.getDeclaredConstructor(new Class[] {
        clArrayDescriptor, Connection.class, Object.class});

      Array oracleArray = (Array) constructor.newInstance(new Object[]
        {arrayDesc, conn, objectArray});

      // Bind array
      // ((oracle.jdbc.OraclePreparedStatement)oracleStmt).setARRAY(1, array);

      // cl = Class.forName("oracle.jdbc.OraclePreparedStatement");

      // method = cl.getDeclaredMethod("setARRAY",
      //                              new Class[] {Integer.TYPE, Object[].class});

      if (name == null) {
        return false;
      }

      if (!name.startsWith(":")) {
        name = ":" + name;
      }

      if (name.length() < 2) {
        return false;
      }

      // method.invoke(oracleStmt, new Object[] {name, oracleArray});

      Integer index = stmt.getBindingVariable(name);

      if (index == null)
        return false;

      int i = index.intValue();
      Object object = varArray.toJavaObject();

      if (object instanceof OracleOciCollection) {
        oracleArray = ((OracleOciCollection) object).getCollection();
        oracleStmt.setArray(i, oracleArray);
      } else if (varArray instanceof ArrayValueImpl) {
        // oracleStmt.setObject(i, varArray.getKeyArray());
        // Object objectArray[] = new Object[] {"aaa", "bbb", "ccc"};
        // oracleStmt.setObject(i, objectArray);
        oracleStmt.setArray(i, oracleArray);
      } else {
        oracleStmt.setObject(i, object);
      }

      // drop descriptor ???? 'number_varray' ????

      return true;

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

  /**
   * Binds the PHP variable to the Oracle placeholder
   *
   * @param type one of the following types:
   *
   * SQLT_INT - for integers;
   *
   * SQLT_CHR - for VARCHARs;
   *
   * SQLT_RSET - for cursors, that were created before with oci_new_cursor()
   *
   * OCI_B_BFILE (integer)
   *
   *    Used with oci_bind_by_name() when binding BFILEs.
   *
   * OCI_B_CFILEE (integer)
   *
   *    Used with oci_bind_by_name() when binding CFILEs.
   *
   * OCI_B_CLOB (integer)
   *
   *    Used with oci_bind_by_name() when binding CLOBs.
   *
   * OCI_B_BLOB (integer)
   *
   *    Used with oci_bind_by_name() when binding BLOBs.
   *
   * OCI_B_ROWID (integer)
   *
   *    Used with oci_bind_by_name() when binding ROWIDs.
   *
   * OCI_B_CURSOR (integer)
   *
   *    Used with oci_bind_by_name() when binding cursors,
   *    previously allocated with oci_new_descriptor().
   *
   * OCI_B_NTY (integer)
   *
   *    Used with oci_bind_by_name() when binding named data
   *    types. Note: in PHP < 5.0 it was called OCI_B_SQLT_NTY.
   *
   * OCI_B_BIN (integer)
   *
   * SQLT_FILE (integer)
   *
   * SQLT_BFILEE (integer)
   *
   *    The same as OCI_B_BFILE.
   *
   * SQLT_CFILE (integer)
   *
   * SQLT_CFILEE (integer)
   *
   *    The same as OCI_B_CFILEE.
   *
   * SQLT_CLOB (integer)
   *
   *    The same as OCI_B_CLOB.
   *
   * SQLT_BLOB (integer)
   *
   *    The same as OCI_B_BLOB.
   *
   * SQLT_RDD (integer)
   *
   *    The same as OCI_B_ROWID.
   *
   * SQLT_NTY (integer)
   *
   *    The same as OCI_B_NTY.
   *
   * SQLT_LNG (integer)
   *
   *    Used with oci_bind_by_name() to bind LONG values.
   *
   * SQLT_LBI (integer)
   *
   *    Used with oci_bind_by_name() to bind LONG RAW values.
   *
   * SQLT_BIN (integer)
   *
   *    Used with oci_bind_by_name() to bind RAW values.
   *
   */
  public static boolean oci_bind_by_name(Env env,
                                         @NotNull OracleStatement stmt,
                                         @NotNull String placeholderName,
                                         Value variable,
                                         @Optional("0") int maxLength,
                                         @Optional("0") int type)
  {
    if ((type == OCI_B_CFILEE) ||
        (type == SQLT_CFILE) ||
        (type == SQLT_CFILEE)) {
      throw new UnimplementedException("oci_bind_by_name with CFILE");
    }

    try {

      if (placeholderName == null) {
        return false;
      }

      if (!placeholderName.startsWith(":")) {
        placeholderName = ":" + placeholderName;
      }

      if (placeholderName.length() < 2) {
        return false;
      }

      Integer index = stmt.getBindingVariable(placeholderName);

      if (index == null)
        return false;

      int i = index.intValue();

      PreparedStatement pstmt = stmt.getPreparedStatement();

      CallableStatement callableStmt = (CallableStatement) pstmt;

      // XXX: We could use ParameterMetaData.getParameterMode
      // to figure out which parameters are IN and/or OUT and
      // then setObject and/or registerOutParameter according
      // to the parameter mode. However, getParameterMode()
      // is unsupported from Oracle JDBC drivers (Jun-2006).
      //
      // ParameterMetaData metaData = pstmt.getParameterMetaData();
      //
      // int paramMode = metaData.getParameterMode(i);
      //
      //  switch (paramMode) {
      // case ParameterMetaData.parameterModeInOut:
      //   {
      //     int oracleType = arrayPhpToOracleType[type];
      //     callableStmt.registerOutParameter(i, oracleType);
      //     pstmt.setObject(i, variable.toJavaObject());
      //     break;
      //   }
      // case ParameterMetaData.parameterModeOut:
      //   {
      //     int oracleType = arrayPhpToOracleType[type];
      //     callableStmt.registerOutParameter(i, oracleType);
      //     break;
      //   }
      // default: // case ParameterMetaData.parameterModeIn:
      //   {
      //     pstmt.setObject(i, variable.toJavaObject());
      //     break;
      //   }
      // }

      switch (type) {
      case OCI_B_CURSOR:
      case SQLT_RSET:
        {
          // Assume the most common scenario: OUT parameter mode.
          int oracleType = arrayPhpToOracleType[type];
          callableStmt.registerOutParameter(i, oracleType);
          // Set the cursor's underlying statement (see php/4404).
          Object cursor = variable.toJavaObject();
          if ((cursor == null) || !(cursor instanceof OracleStatement)) {
            return false;
          }
          ((OracleStatement) cursor).setPreparedStatement(callableStmt);
          break;
        }
      case OCI_B_BFILE:   // BFILE
      case SQLT_BFILEE:   // ...
      case SQLT_FILE:     // ...
      case SQLT_BLOB:     // BLOB
      case OCI_B_BLOB:    // ...
      case SQLT_CLOB:     // CLOB
      case OCI_B_CLOB:    // ...
      case OCI_B_ROWID:   // ROWID
      case SQLT_RDD:      // ...
        {
          // Assume the most common scenario: OUT parameter mode.
          int oracleType = arrayPhpToOracleType[type];
          callableStmt.registerOutParameter(i, oracleType);
          Object ociLob = variable.toJavaObject();
          if ((ociLob == null) || !(ociLob instanceof OracleOciLob)) {
            return false;
          }
          stmt.setOutParameter((OracleOciLob) ociLob);
          break;
        }
      default:
        {
          // Assume the most common scenario: IN parameter mode.

          // XXX: Check the spec. if there is a case where the
          // variable would not be initialized yet
          // stmt.putByNameVariable(placeholderName, variable);
          Object object = variable.toJavaObject();
          if (object instanceof OracleOciCollection) {
            object = ((OracleOciCollection) object).getCollection();
          }
          pstmt.setObject(i, object);
        }
      }

      return true;

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

      try {
        stmt.resetBindingVariables();
        stmt.resetByNameVariables();
      } catch (Exception ex2) {
        log.log(Level.FINE, ex2.toString(), ex2);
      }
    }

    return false;
  }

  /**
   * Cancels reading from cursor
   */
  public static boolean oci_cancel(Env env,
                                   @NotNull OracleStatement stmt)
  {
    return oci_free_statement(env, stmt);
  }

  /**
   * Closes Oracle connection
   */
  public static boolean oci_close(Env env,
                                  @NotNull Oracle conn)
  {
    if (conn == null)
      conn = getConnection(env);

    if (conn != null) {
      if (conn == getConnection(env))
        env.removeSpecialValue("caucho.oracle");

      conn.close(env);

      return true;
    }
    else
      return false;
  }

  /**
   * Commits outstanding statements
   */
  public static boolean oci_commit(Env env,
                                   @NotNull Oracle conn)
  {
    try {
      return conn.commit();
    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return false;
    }
  }

  /**
   * Establishes a connection to the Oracle server
   */
  public static Value oci_connect(Env env,
                                  @NotNull String username,
                                  @NotNull String password,
                                  @Optional String db,
                                  @Optional String charset,
                                  @Optional("0") int sessionMode)
  {
    // Note:  The second and subsequent calls to oci_connect() with the same parameters
    // will return the connection handle returned from the first call. This means that
    // queries issued against one handle are also applied to the other handles, because
    // they are the same handle. (source: php.net)

    if (!((charset == null) || charset.length() == 0)) {
      throw new UnimplementedException("oci_connect with charset");
    }

    if ((sessionMode == OCI_DEFAULT) ||
        (sessionMode == OCI_SYSOPER) ||
        (sessionMode == OCI_SYSDBA)) {
      throw new UnimplementedException("oci_connect with session mode");
    }

    return connectInternal(env, true, username, password, db, charset, sessionMode);
  }

  /**
   * Uses a PHP variable for the define-step during a SELECT
   */
  public static boolean oci_define_by_name(Env env,
                                           @NotNull OracleStatement stmt,
                                           @NotNull String columnName,
                                           @NotNull @Reference Value variable,
                                           @Optional("0") int type)
  {
    // Example:
    //
    //  $stmt = oci_parse($conn, "SELECT id, data FROM test");
    //
    //  /* the define MUST be done BEFORE oci_execute! */
    //
    //  oci_define_by_name($stmt, "ID", $myid);
    //  oci_define_by_name($stmt, "DATA", $mydata);
    //
    //  oci_execute($stmt, OCI_DEFAULT);
    //
    //  while ($row = oci_fetch($stmt)) {
    //     echo "id:" . $myid . "\n";
    //     echo "data:" . $mydata . "\n";
    //  }

    try {
      stmt.putByNameVariable(columnName, variable);
      return true;
    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return false;
    }
  }

  /**
   * Returns the last error found
   */
  public static String oci_error(Env env,
                                 @Optional Value resource)
  {
    JdbcConnectionResource conn = null;

    if (resource == null) {
      conn = getConnection(env).validateConnection();
    } else {
      Object object = resource.toJavaObject();

      if (object instanceof Oracle) {
        conn = ((Oracle) object).validateConnection();
      } else {
        conn = ((OracleStatement) object).validateConnection();
      }
    }

    return conn.getErrorMessage();
  }

  /**
   * Executes a statement
   */
  public static boolean oci_execute(Env env,
                                    @NotNull OracleStatement stmt,
                                    @Optional("0") int mode)
  {
    try {

      //  Scenarios for oci_execute.
      //
      //
      //  1. Simple query: oci_parse / oci_execute
      //
      //  $query = 'SELECT * FROM TEST';
      //
      //  $stmt = oci_parse($conn, $query);
      //
      //  oci_execute($stmt, OCI_DEFAULT);
      //
      //  $result = oci_fetch_array($stid, OCI_BOTH);
      //
      //
      //  2. Define by name: oci_parse / oci_define_by_name / oci_execute
      //
      //  $stmt = oci_parse($conn, "SELECT id, data FROM test");
      //
      //  /* the define MUST be done BEFORE oci_execute! */
      //
      //  oci_define_by_name($stmt, "ID", $myid);
      //  oci_define_by_name($stmt, "DATA", $mydata);
      //
      //  oci_execute($stmt, OCI_DEFAULT);
      //
      //  while ($row = oci_fetch($stmt)) {
      //     echo "id:" . $myid . "\n";
      //     echo "data:" . $mydata . "\n";
      //  }
      //
      //
      //  3. Cursors: oci_new_cursor / oci_parse /
      //     oci_bind_by_name / oci_execute($stmt) / oci_execute($cursor)
      //
      //  $stmt = oci_parse($conn,
      //  "CREATE OR REPLACE PACKAGE cauchopkgtestoci AS ".
      //  "TYPE refcur IS REF CURSOR; ".
      //  "PROCEDURE testproc (var_result out cauchopkgtestoci.refcur); ".
      //  "END cauchopkgtestoci; ");
      //
      //  oci_execute($stmt);
      //
      //  $stmt = oci_parse($conn,
      //  "CREATE OR REPLACE PACKAGE BODY cauchopkgtestoci IS ".
      //  "PROCEDURE testproc (var_result out cauchopkgtestoci.refcur) IS ".
      //  "BEGIN ".
      //  "OPEN var_result FOR SELECT data FROM caucho.test; ".
      //  "END testproc; ".
      //  "END; ");
      //
      //  oci_execute($stmt);
      //
      //  $curs = oci_new_cursor($conn);
      //
      //  $stmt = oci_parse($conn, "begin cauchopkgtestoci.testproc(:dataCursor); end;");
      //
      //  oci_bind_by_name($stmt, "dataCursor", $curs, 255, SQLT_RSET);
      //
      //  oci_execute($stmt);
      //
      //  oci_execute($curs);
      //
      //  while ($data = oci_fetch_row($curs)) {
      //     var_dump($data);
      //  }

      // Get the underlying JDBC connection.
      Connection conn = stmt.getJavaConnection();

      // Large Objects can not be used in auto-commit mode.
      conn.setAutoCommit(false);

      // Use the underlying callable statement to check different scenarios.
      CallableStatement callableStatement = stmt.getCallableStatement();

      // Check for case (3) oci_execute($cursor);
      // A previous statement has been executed and holds the result set.
      Object cursorResult = null;
      try {
        cursorResult = callableStatement.getObject(1);
        if ((cursorResult != null) && (cursorResult instanceof ResultSet)) {
          ResultSet rs = (ResultSet) cursorResult;
          stmt.setResultSet(rs);
          return true;
        }
      } catch (Exception e) {
        // We assume this is not case (3). No error.
      }

      // Case (1) or executing a more complex query.
      stmt.execute(env);

      OracleOciLob ociLob = stmt.getOutParameter();
      if (ociLob != null) {
        // Ex: java.sql.Clob outParameter = callableStatement.getClob(1);
        ociLob.setLob(callableStatement.getObject(1));
      }

      // Fetch and assign values to corresponding binded variables
      // This is necessary for LOB support and
      // should be reworked calling a private fetch method instead.
      // oci_fetch(env, stmt);

      if (mode == OCI_COMMIT_ON_SUCCESS) {
        conn.commit();
      }

      return true;

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

      try {
        stmt.resetBindingVariables();
        stmt.resetByNameVariables();
      } catch (Exception ex2) {
        log.log(Level.FINE, ex2.toString(), ex2);
      }

      return false;
    }
  }

  /**
   * Fetches all rows of result data into an array
   */
  @ReturnNullAsFalse
  public static ArrayValue oci_fetch_all(Env env,
                                         @NotNull OracleStatement stmt,
                                         @NotNull Value output,
                                         @Optional int skip,
                                         @Optional int maxrows,
                                         @Optional int flags)
  {
    JdbcResultResource resource = null;

    ArrayValueImpl newArray = new ArrayValueImpl();

    try {
      if (stmt == null)
        return null;

      resource = new JdbcResultResource(null, stmt.getResultSet(), null);

      Value value = resource.fetchArray(env, JdbcResultResource.FETCH_ASSOC);

      int curr = 0;

      while(value != NullValue.NULL) {
        newArray.put(LongValue.create(curr), value);

        curr++;

        value = resource.fetchArray(env, JdbcResultResource.FETCH_ASSOC);
      }
    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return null;
    }

    return newArray;
  }

  /**
   * Returns the next row from the result data as an associative or numeric array, or both
   */
  @ReturnNullAsFalse
  public static ArrayValue oci_fetch_array(Env env,
                                           @NotNull OracleStatement stmt,
                                           @Optional("-1") int mode)
  {
    if (stmt == null)
      return null;

    if (mode == OCI_RETURN_LOBS)
      throw new UnimplementedException("oci_fetch_array with OCI_RETURN_LOBS");

    if (mode == OCI_RETURN_NULLS)
      throw new UnimplementedException("oci_fetch_array with OCI_RETURN_NULLS");

    try {

      JdbcResultResource resource = new JdbcResultResource(null, stmt.getResultSet(), null);

      switch (mode) {
      case OCI_ASSOC:
        return resource.fetchArray(env, JdbcResultResource.FETCH_ASSOC);
      case OCI_NUM:
        return resource.fetchArray(env, JdbcResultResource.FETCH_NUM);
      default: // case OCI_BOTH:
        return resource.fetchArray(env, JdbcResultResource.FETCH_BOTH);
      }

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

  /**
   * Returns the next row from the result data as an associative array
   */
  @ReturnNullAsFalse
  public static ArrayValue oci_fetch_assoc(Env env,
                                           @NotNull OracleStatement stmt)
  {
    try {

      if (stmt == null)
        return null;

      JdbcResultResource resource = new JdbcResultResource(null, stmt.getResultSet(), null);
      ArrayValue arrayValue = resource.fetchArray(env, JdbcResultResource.FETCH_ASSOC);

      return arrayValue;

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

  /**
   * Returns the next row from the result data as an object
   */
  public static Value oci_fetch_object(Env env,
                                       @NotNull OracleStatement stmt)
  {
    try {
      if (stmt == null)
        return BooleanValue.FALSE;

      JdbcResultResource resource = new JdbcResultResource(null, stmt.getResultSet(), null);
      return resource.fetchObject(env);
    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return BooleanValue.FALSE;
    }
  }

  /**
   * Returns the next row from the result data as a numeric array
   */
  @ReturnNullAsFalse
  public static ArrayValue oci_fetch_row(Env env,
                                         @NotNull OracleStatement stmt)
  {
    try {

      if (stmt == null)
        return null;

      JdbcResultResource resource = new JdbcResultResource(null, stmt.getResultSet(), null);
      return resource.fetchArray(env, JdbcResultResource.FETCH_NUM);

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

  /**
   * Fetches the next row into result-buffer
   */
  public static boolean oci_fetch(Env env,
                                  @NotNull OracleStatement stmt)
  {
    try {

      if (stmt == null)
        return false;

      JdbcResultResource resource = new JdbcResultResource(null, stmt.getResultSet(), null);

      Value result = resource.fetchArray(env, JdbcResultResource.FETCH_BOTH);

      stmt.setResultBuffer(result);

      if (!(result instanceof ArrayValue)) {
        return false;
      }

      ArrayValue arrayValue = (ArrayValue) result;

      for (Map.Entry<String,Value> entry : stmt.getByNameVariables().entrySet()) {
        String fieldName = entry.getKey();
        Value var = entry.getValue();

        Value newValue = arrayValue.get(StringValue.create(fieldName));
        var.set(newValue);
      }

      // See oci_num_rows()
      stmt.increaseFetchedRows();

      return true;

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

  /**
   * Checks if the field is NULL
   *
   * @param stmt oracle statement
   * @param fieldNameOrNumber field's index (1-based) or it's name
   * @return TRUE if the field is null or FALSE otherwise.
   */
  public static boolean oci_field_is_null(Env env,
                                          @NotNull OracleStatement stmt,
                                          @NotNull Value fieldNameOrNumber)
  {
    if (stmt == null)
      return false;

    try {

      ResultSet rs = stmt.getResultSet();

      JdbcResultResource resource = new JdbcResultResource(null, rs, null);

      int fieldNumber = resource.getColumnNumber(fieldNameOrNumber, 1);

      if (fieldNumber < 0)
        return false;

      ResultSetMetaData metaData = rs.getMetaData();

      return metaData.isNullable(fieldNumber + 1) == ResultSetMetaData.columnNullable;

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

  /**
   * Returns the name of a field from the statement
   */
  public static Value oci_field_name(Env env,
                                     @NotNull OracleStatement stmt,
                                     @NotNull int fieldNumber)
  {
    try {
      if (stmt == null)
        return BooleanValue.FALSE;

      JdbcResultResource resource = new JdbcResultResource(null, stmt.getResultSet(), null);

      return resource.getFieldName(env, fieldNumber);
    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return BooleanValue.FALSE;
    }
  }

  /**
   * Tell the precision of a field
   */
  @ReturnNullAsFalse
  public static LongValue oci_field_precision(Env env,
                                              @NotNull OracleStatement stmt,
                                              @NotNull int field)
  {
    if (stmt == null)
      return null;

    try {

      ResultSet rs = stmt.getResultSet();
      ResultSetMetaData metaData = rs.getMetaData();

      int precision = metaData.getPrecision(field);
      return LongValue.create(precision);

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

  /**
   * Tell the scale of the field
   */
  @ReturnNullAsFalse
  public static LongValue oci_field_scale(Env env,
                                          @NotNull OracleStatement stmt,
                                          @NotNull int field)
  {
    if (stmt == null)
      return null;

    try {

      ResultSet rs = stmt.getResultSet();
      ResultSetMetaData metaData = rs.getMetaData();

      int precision = metaData.getScale(field);
      return LongValue.create(precision);

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

  /**
   * Returns field's size
   *
   * @param stmt oracle statement
   * @param fieldNameOrNumber field's index (1-based) or it's name
   * @return the field's size
   */
  public static Value oci_field_size(Env env,
                                     @NotNull OracleStatement stmt,
                                     @Optional("-1") Value fieldNameOrNumber)
  {
    try {

      if (stmt == null)
        return BooleanValue.FALSE;

      ResultSet rs = stmt.getResultSet();

      JdbcResultResource resource = new JdbcResultResource(null, rs, null);

      int fieldNumber = resource.getColumnNumber(fieldNameOrNumber, 1);

      return resource.getFieldLength(env, fieldNumber);

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

  /**
   * Tell the raw Oracle data type of the field
   *
   * @param field the field number (1-based)
   */
  public static int oci_field_type_raw(Env env,
                                       @NotNull OracleStatement stmt,
                                       int field)
  {
    try {

      if (stmt == null)
        return -1;

      if (field <= 0)
        return -1;

      JdbcResultResource resource = new JdbcResultResource(null, stmt.getResultSet(), null);

      Value typeV = resource.getJdbcType(--field);

      if (typeV instanceof LongValue) {

        int type = typeV.toInt();

        switch (type) {

        case Types.BLOB:
        case Types.LONGVARCHAR:
        case Types.LONGVARBINARY:
          type = SQLT_BLOB;
          break;

        case Types.CLOB:
          type = SQLT_CLOB;
          break;

        case Types.BIGINT:
        case Types.BIT:
        case Types.BOOLEAN:
        case Types.DECIMAL:
        case Types.DOUBLE:
        case Types.FLOAT:
        case Types.INTEGER:
        case Types.NUMERIC:
        case Types.REAL:
        case Types.SMALLINT:
        case Types.TINYINT:
          type = SQLT_NUM;
          break;

        default:
          type = SQLT_CHR;
        }

        return type;
      }

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

    return -1;
  }

  /**
   * Returns field's data type
   */
  public static Value oci_field_type(Env env,
                                     @NotNull OracleStatement stmt,
                                     int fieldNumber)
  {
    try {
      if (stmt == null)
        return BooleanValue.FALSE;

      JdbcResultResource resource = new JdbcResultResource(null, stmt.getResultSet(), null);

      return resource.getFieldType(env, fieldNumber);
    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return BooleanValue.FALSE;
    }
  }

  /**
   *  Frees all resources associated with statement or cursor
   */
  public static boolean oci_free_statement(Env env,
                                           @NotNull OracleStatement stmt)
  {
    try {

      stmt.close();

      return true;

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

  /**
   * Enables or disables internal debug output
   */
  public static void oci_internal_debug(Env env,
                                        @NotNull int onoff)
  {
    throw new UnimplementedException("oci_internal_debug");
  }

  /**
   * Copies large object
   */
  public static boolean oci_lob_copy(Env env,
                                     @NotNull OracleOciLob lobTo,
                                     @NotNull OracleOciLob lobFrom,
                                     @Optional("-1") int length)
  {
    try {

      return lobTo.save(env, lobFrom.read(env, length).toString(), 0);

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

  /**
   * Compares two LOB/FILE locators for equality
   */
  public static boolean oci_lob_is_equal(Env env,
                                         @NotNull OracleOciLob lob1,
                                         @NotNull OracleOciLob lob2)
  {
    try {

      return lob1.equals(lob2);

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

  /**
   * Allocates new collection object
   */
  @ReturnNullAsFalse
  public static OracleOciCollection oci_new_collection(Env env,
                                                       @NotNull Oracle conn,
                                                       @NotNull String tdo,
                                                       @Optional String schema)
  {
    try {

      String typeName = tdo;

      if ((schema != null) && (schema.length() > 0)) {
        typeName = schema + "." + tdo;
      }

      // XXX: Is this case ever possible?
      // StructDescriptor structDesc = StructDescriptor.createDescriptor(typeName, jdbcConn);

      // JDBC underlying connection
      Connection jdbcConn = conn.getJavaConnection();

      // Oracle underlying statement
      // PreparedStatement oracleStmt = stmt.getPreparedStatement();

      // Use reflection
      // ArrayDescriptor arrayDesc = ArrayDescriptor.createDescriptor(typeName, jdbcConn);

      Class clArrayDescriptor = Class.forName("oracle.sql.ArrayDescriptor");

      Method method = clArrayDescriptor.getDeclaredMethod("createDescriptor",
                                                          new Class[] {String.class,
                                                                       Connection.class});

      Object arrayDesc = method.invoke(clArrayDescriptor,
                                       new Object[] {typeName, jdbcConn});

      if (arrayDesc != null) {
        return new OracleOciCollection(jdbcConn, arrayDesc);
      }

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

    return null;
  }

  /**
   * Establishes a new connection to the Oracle server
   */
  public static Value oci_new_connect(Env env,
                                      @NotNull String username,
                                      @NotNull String password,
                                      @Optional String db,
                                      @Optional String charset,
                                      @Optional("0") int sessionMode)
  {
    if (!((charset == null) || charset.length() == 0)) {
      throw new UnimplementedException("oci_new_connect with charset");
    }

    if ((sessionMode == OCI_DEFAULT) ||
        (sessionMode == OCI_SYSOPER) ||
        (sessionMode == OCI_SYSDBA)) {
      throw new UnimplementedException("oci_new_connect with session mode");
    }

    return connectInternal(env, false, username, password, db, charset, sessionMode);
  }

  /**
   * Allocates and returns a new cursor (statement handle)
   */
  @ReturnNullAsFalse
  public static OracleStatement oci_new_cursor(Env env,
                                               @NotNull Oracle conn)
  {
    try {

      OracleStatement stmt = new OracleStatement((Oracle) conn.validateConnection());

      return stmt;

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

  /**
   * Initializes a new empty LOB or FILE descriptor
   *
   * @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
   */
  @ReturnNullAsFalse
  public static OracleOciLob oci_new_descriptor(Env env,
                                                @NotNull Oracle conn,
                                                @Optional("-1") int type)
  {
    try {

      if ((type == OCI_D_FILE) ||
          (type == OCI_D_LOB) ||
          (type == OCI_D_ROWID)) {

        OracleOciLob oracleLob = new OracleOciLob(conn, type);

        return oracleLob;
      }

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

    return null;
  }

  /**
   *  Returns the number of result columns in a statement
   */
  public static Value oci_num_fields(Env env,
                                     @NotNull OracleStatement stmt)
  {
    try {
      if (stmt == null)
        return BooleanValue.FALSE;

      JdbcResultResource resource = new JdbcResultResource(null, stmt.getResultSet(), null);

      return LongValue.create(resource.getFieldCount());
    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return BooleanValue.FALSE;
    }
  }

  /**
   * Returns number of rows affected during statement execution
   *
   * Note:  This function does not return number of rows selected!
   * For SELECT statements this function will return the number of rows,
   * that were fetched to the buffer with oci_fetchxxxx() functions.
   */
  @ReturnNullAsFalse
  public static LongValue oci_num_rows(Env env,
                                       @NotNull OracleStatement stmt)
  {
    try {

      if (stmt == null)
        return null;

      // JdbcResultResource resource = new JdbcResultResource(null, stmt.getResultSet(), null);

      // return LongValue.create(resource.getNumRows());

      return LongValue.create(stmt.getFetchedRows());

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

  /**
   * Prepares Oracle statement for execution
   */
  @ReturnNullAsFalse
  public static OracleStatement oci_parse(Env env,
                                          @NotNull Oracle conn,
                                          String query)
  {
    try {
      // XXX: Rework this.
      // Enclose the query with "begin ...; end;" so any regular statement
      // or stored procedure call can be executed with a CallableStatement.
      query = query.trim();

      String lowerCaseQuery = query.toLowerCase();

      if (lowerCaseQuery.startsWith("insert") ||
          lowerCaseQuery.startsWith("update") ||
          lowerCaseQuery.startsWith("delete")) {
        if (!lowerCaseQuery.startsWith("begin ")) {
          query = "begin " + query;
        }

        if (!lowerCaseQuery.endsWith(";")) {
          query += ";";
        }

        if (!lowerCaseQuery.endsWith(" end;")) {
          query += " end;";
        }
      }

      // Make the PHP query a JDBC like query replacing (:mydata -> ?) with question marks.
      // Store binding names for future reference (see oci_execute)
      String regex = ":[a-zA-Z0-9_]+";
      String jdbcQuery = query.replaceAll(regex, "?");
      OracleStatement pstmt = conn.prepare(env, jdbcQuery);

      Pattern pattern = Pattern.compile(regex);
      Matcher matcher = pattern.matcher(query);
      int i = 0;
      while (matcher.find()) {
        String group = matcher.group();
        pstmt.putBindingVariable(group, new Integer(++i));
      }

      return pstmt;

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

  /**
   * Changes password of Oracle's user
   */
  public static boolean oci_password_change(Env env,
                                            @NotNull Oracle conn,
                                            @NotNull String username,
                                            @NotNull String oldPassword,
                                            @NotNull String newPassword)
  {
    try {

      // XXX: When is oldPassword used?

      if (conn == null)
        return false;

      OracleStatement oracleStmt;

      oracleStmt = oci_parse(env, conn, "ALTER USER "+username+" IDENTIFIED BY "+newPassword);

      oci_execute(env, oracleStmt, 0);

      return true;

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

  /**
   * Connect to an Oracle database using a persistent connection
   */
  public static Value oci_pconnect(Env env,
                                   @NotNull String username,
                                   @NotNull String password,
                                   @Optional String db,
                                   @Optional String charset,
                                   @Optional("0") int sessionMode)
  {
    if (!((charset == null) || charset.length() == 0)) {
      throw new UnimplementedException("oci_pconnect with charset");
    }

    if ((sessionMode == OCI_DEFAULT) ||
        (sessionMode == OCI_SYSOPER) ||
        (sessionMode == OCI_SYSDBA)) {
      throw new UnimplementedException("oci_pconnect with session mode");
    }

    return connectInternal(env, true, username, password, db, charset, sessionMode);
  }

  /**
   * Returns field's value from the fetched row
   */
  public static Value oci_result(Env env,
                                 @NotNull OracleStatement stmt,
                                 @NotNull Value field)
  {
    try {
      if (stmt == null)
        return BooleanValue.FALSE;

      Value result = stmt.getResultBuffer();

      return ((ArrayValueImpl)result).get(field);
    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return BooleanValue.FALSE;
    }
  }

  /**
   * Rolls back outstanding transaction
   */
  public static Value oci_rollback(Env env,
                                   @NotNull Oracle conn)
  {
    try {
      return BooleanValue.create(conn.rollback());
    } catch (Exception ex) {
      log.log(Level.FINE, ex.toString(), ex);
      return BooleanValue.FALSE;
    }
  }

  /**
   * Returns server version
   */
  @ReturnNullAsFalse
  public static String oci_server_version(Env env,
                                          @NotNull Oracle conn)
  {
    try {
      if (conn == null)
        conn = getConnection(env);

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

  /**
   * Sets number of rows to be prefetched
   */
  public static boolean oci_set_prefetch(Env env,
                                         @NotNull OracleStatement stmt,
                                         @Optional("1") int rows)
  {
    try {

      if (stmt == null)
        return false;

      PreparedStatement pstmt = stmt.getPreparedStatement();
      pstmt.setFetchSize(rows);

      return true;

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

  /**
   * Returns the type of an OCI statement
   */
  public static String oci_statement_type(Env env,
                                          @NotNull OracleStatement stmt)
  {
    return stmt.getStatementType();
  }

  /**
   * Alias of oci_bind_by_name()
   */
  public static boolean ocibindbyname(Env env,
                                      @NotNull OracleStatement stmt,
                                      @NotNull String variable,
                                      @NotNull Value value,
                                      @Optional("0") int maxLength,
                                      @Optional("0") int type)
  {
    return oci_bind_by_name(env, stmt, variable, value, maxLength, type);
  }

  /**
   * Alias of oci_cancel()
   */
  public static boolean ocicancel(Env env,
                                  @NotNull OracleStatement stmt)
  {
    return oci_cancel(env, stmt);
  }

  /**
   * Alias of OCI-Lob->close
   */
  public static Value ocicloselob(Env env,
                                  @NotNull Oracle conn)
  {
    throw new UnimplementedException("ocicloselob");
  }

  /**
   * Alias of OCI-Collection->append
   */
  public static Value ocicollappend(Env env,
                                    @NotNull Oracle conn)
  {
    throw new UnimplementedException("ocicollappend");
  }

  /**
   * Alias of OCI-Collection->assign
   */
  public static Value ocicollassign(Env env,
                                    @NotNull Oracle conn)
  {
    throw new UnimplementedException("ocicollassign");
  }

  /**
   * Alias of OCI-Collection->assignElem
   */
  public static Value ocicollassignelem(Env env,
                                        @NotNull Oracle conn)
  {
    throw new UnimplementedException("ocicollassignelem");
  }

  /**
   * Alias of OCI-Collection->getElem
   */
  public static Value ocicollgetelem(Env env,
                                     @NotNull Oracle conn)
  {
    throw new UnimplementedException("ocicollgetelem");
  }

  /**
   * Alias of OCI-Collection->max
   */
  public static Value ocicollmax(Env env,
                                 @NotNull Oracle conn)
  {
    throw new UnimplementedException("ocicollmax");
  }

  /**
   * Alias of OCI-Collection->size
   */
  public static Value ocicollsize(Env env,
                                  @NotNull Oracle conn)
  {
    throw new UnimplementedException("ocicollsize");
  }

  /**
   * Alias of OCI-Collection->trim
   */
  public static Value ocicolltrim(Env env,
                                  @NotNull Oracle conn)
  {
    throw new UnimplementedException("ocicolltrim");
  }

  /**
   * Alias of oci_field_is_null()
   */
  public static boolean ocicolumnisnull(Env env,
                                        @NotNull OracleStatement stmt,
                                        @NotNull Value field)
  {
    return oci_field_is_null(env, stmt, field);
  }

  /**
   * Alias of oci_field_name()
   */
  public static Value ocicolumnname(Env env,
                                    @NotNull OracleStatement stmt,
                                    @NotNull int fieldNumber)
  {
    return oci_field_name(env, stmt, fieldNumber);
  }

  /**
   * Alias of oci_field_precision()
   */
  public static Value ocicolumnprecision(Env env,
                                         @NotNull OracleStatement stmt,
                                         @NotNull int field)
  {
    return oci_field_precision(env, stmt, field);
  }

  /**
   * Alias of oci_field_scale()
   */
  public static Value ocicolumnscale(Env env,
                                     @NotNull OracleStatement stmt,
                                     @NotNull int field)
  {
    return oci_field_scale(env, stmt, field);
  }

  /**
   * Alias of oci_field_size()
   */
  public static Value ocicolumnsize(Env env,
                                    @NotNull OracleStatement stmt,
                                    @Optional Value field)
  {
    return oci_field_size(env, stmt, field);
  }

  /**
   * Alias of oci_field_type()
   */
  public static Value ocicolumntype(Env env,
                                    @NotNull OracleStatement stmt,
                                    @Optional int fieldNumber)
  {
    return oci_field_type(env, stmt, fieldNumber);
  }

  /**
   * Alias of oci_field_type_raw()
   */
  public static int ocicolumntyperaw(Env env,
                                     @NotNull OracleStatement stmt,
                                     @Optional int field)
  {
    return oci_field_type_raw(env, stmt, field);
  }

  /**
   * Alias of oci_commit()
   */
  public static boolean ocicommit(Env env,
                                  @NotNull Oracle conn)
  {
    return oci_commit(env, conn);
  }

  /**
   * Alias of oci_define_by_name()
   */
  public static boolean ocidefinebyname(Env env,
                                        @NotNull OracleStatement stmt,
                                        @NotNull String columnName,
                                        @NotNull Value variable,
                                        @Optional("0") int type)
  {
    return oci_define_by_name(env, stmt, columnName, variable, type);
  }

  /**
   * Alias of oci_error()
   */
  public static String ocierror(Env env,
                                @Optional Value resource)
  {
    return oci_error(env, resource);
  }

  /**
   * Alias of oci_execute()
   */
  public static boolean ociexecute(Env env,
                                   @NotNull OracleStatement stmt,
                                   @Optional("0") int mode)
  {
    return oci_execute(env, stmt, mode);
  }

  /**
   * Alias of oci_fetch()
   */
  public static boolean ocifetch(Env env,
                                 @NotNull OracleStatement stmt)
  {
    return oci_fetch(env, stmt);
  }

  /**
   * Fetches the next row into an array
   */
  public static Value ocifetchinto(Env env,
                                   @NotNull Oracle conn)
  {
    throw new UnimplementedException("ocifetchinto");
  }

  /**
   * Alias of oci_fetch_all()
   */
  public static Value ocifetchstatement(Env env,
                                        @NotNull OracleStatement stmt,
                                        @NotNull Value output,
                                        @Optional int skip,
                                        @Optional int maxrows,
                                        @Optional int flags)
  {
    return oci_fetch_all(env, stmt, output, skip, maxrows, flags);
  }

  /**
   * Alias of OCI-Collection->free
   */
  public static Value ocifreecollection(Env env,
                                        @NotNull Oracle conn)
  {
    throw new UnimplementedException("ocifreecollection");
  }

  /**
   * Alias of oci_free_statement()
   */
  public static boolean ocifreecursor(Env env,
                                      @NotNull OracleStatement stmt)
  {
    return oci_free_statement(env, stmt);
  }

  /**
   * Alias of OCI-Lob->free
   */
  public static Value ocifreedesc(Env env,
                                  @NotNull Oracle conn)
  {
    throw new UnimplementedException("ocifreedesc");
  }

  /**
   * Alias of oci_free_statement()
   */
  public static boolean ocifreestatement(Env env,
                                         @NotNull OracleStatement stmt)
  {
    return oci_free_statement(env, stmt);
  }

  /**
   * Alias of oci_internal_debug()
   */
  public static void ociinternaldebug(Env env,
                                      @NotNull int onoff)
  {
    oci_internal_debug(env, onoff);
  }

  /**
   * Alias of OCI-Lob->load
   */
  public static Value ociloadlob(Env env,
                                 @NotNull Oracle conn)
  {
    throw new UnimplementedException("ociloadlob");
  }

  /**
   * Alias of oci_close()
   */
  public static boolean ocilogoff(Env env,
                                  @NotNull Oracle conn)
  {
    return oci_close(env, conn);
  }

  /**
   * Alias of oci_connect()
   */
  public static Value ocilogon(Env env,
                               @NotNull String username,
                               @NotNull String password,
                               @Optional String db,
                               @Optional String charset,
                               @Optional("0") int sessionMode)
  {
    return oci_connect(env, username, password, db, charset, sessionMode);
  }

  /**
   * Alias of oci_new_collection()
   */
  @ReturnNullAsFalse
  public static OracleOciCollection ocinewcollection(Env env,
                                                     @NotNull Oracle conn,
                                                     @NotNull String tdo,
                                                     @Optional String schema)
  {
    return oci_new_collection(env, conn, tdo, schema);
  }

  /**
   * Alias of oci_new_cursor()
   */
  @ReturnNullAsFalse
  public static OracleStatement ocinewcursor(Env env,
                                             @NotNull Oracle conn)
  {
    return oci_new_cursor(env, conn);
  }

  /**
   * Alias of oci_new_descriptor()
   */
  public static OracleOciLob ocinewdescriptor(Env env,
                                              @NotNull Oracle conn,
                                              @Optional("-1") int type)
  {
    return oci_new_descriptor(env, conn, type);
  }

  /**
   * Alias of oci_new_connect()
   */
  public static Value ocinlogon(Env env,
                                @NotNull String username,
                                @NotNull String password,
                                @Optional String db,
                                @Optional String charset,
                                @Optional("0") int sessionMode)
  {
    return oci_new_connect(env, username, password, db, charset, sessionMode);
  }

  /**
   * Alias of oci_num_fields()
   */
  public static Value ocinumcols(Env env,
                                 @NotNull OracleStatement stmt)
  {
    return oci_num_fields(env, stmt);
  }

  /**
   * Alias of oci_parse()
   */
  @ReturnNullAsFalse
  public static OracleStatement ociparse(Env env,
                                         @NotNull Oracle conn,
                                         @NotNull String query)
  {
    return oci_parse(env, conn, query);
  }

  /**
   * Alias of oci_pconnect()
   */
  public static Value ociplogon(Env env,
                                @NotNull String username,
                                @NotNull String password,
                                @Optional String db,
                                @Optional String charset,
                                @Optional("0") int sessionMode)
  {
    return oci_pconnect(env, username, password, db, charset, sessionMode);
  }

  /**
   * Alias of oci_result()
   */
  public static Value ociresult(Env env,
                                @NotNull OracleStatement stmt,
                                @NotNull Value field)
  {
    return oci_result(env, stmt, field);
  }

  /**
   * Alias of oci_rollback()
   */
  public static Value ocirollback(Env env,
                                  @NotNull Oracle conn)
  {
    return oci_rollback(env, conn);
  }

  /**
   * Alias of oci_num_rows()
   */
  public static Value ocirowcount(Env env,
                                  @NotNull OracleStatement stmt)
  {
    return oci_num_rows(env, stmt);
  }

  /**
   * Alias of OCI-Lob->save
   */
  public static Value ocisavelob(Env env,
                                 @NotNull Oracle conn)
  {
    throw new UnimplementedException("ocisavelob");
  }

  /**
   * Alias of OCI-Lob->import
   */
  public static Value ocisavelobfile(Env env,
                                     @NotNull Oracle conn)
  {
    throw new UnimplementedException("ocisavelobfile");
  }

  /**
   * Alias of oci_server_version()
   */
  public static String ociserverversion(Env env,
                                        @NotNull Oracle conn)
  {
    return oci_server_version(env, conn);
  }

  /**
   * Alias of oci_set_prefetch()
   */
  public static boolean ocisetprefetch(Env env,
                                       @NotNull OracleStatement stmt,
                                       @Optional("1") int rows)
  {
    return oci_set_prefetch(env, stmt, rows);
  }

  /**
   * Alias of oci_statement_type()
   */
  public static String ocistatementtype(Env env,
                                        @NotNull OracleStatement stmt)
  {
    return oci_statement_type(env, stmt);
  }

  /**
   * Alias of OCI-Lob->export
   */
  public static Value ociwritelobtofile(Env env,
                                        @NotNull Oracle conn)
  {
    throw new UnimplementedException("ociwritelobtofile");
  }

  /**
   * Alias of OCI-Lob->writeTemporary
   */
  public static Value ociwritetemporarylob(Env env,
                                           @NotNull Oracle conn)
  {
    throw new UnimplementedException("ociwritetemporarylob");
  }

  private static Oracle getConnection(Env env)
  {
    Oracle conn = null;

    ConnectionInfo connectionInfo = (ConnectionInfo) env.getSpecialValue("caucho.oracle");

    if (connectionInfo != null) {
      // Reuse the cached connection
      conn = connectionInfo.getConnection();
      return conn;
    }

    String driver = "oracle.jdbc.OracleDriver";
    String url = "jdbc:oracle:thin:@localhost:1521";

    conn = new Oracle(env, "localhost", "", "", "", 1521, driver, url);

    env.setSpecialValue("caucho.oracle", conn);

    return conn;
  }

  private static Value connectInternal(Env env,
                                       boolean reuseConnection,
                                       String username,
                                       String password,
                                       String db,
                                       String charset,
                                       int sessionMode)
  {
    String host = "localhost";
    int port = 1521;

    String driver = "oracle.jdbc.OracleDriver";

    String url;

    if (db.indexOf("//") == 0) {
      // db is the url itself: "//db_host[:port]/database_name"
      url = "jdbc:oracle:thin:@" + db.substring(2);
      url = url.replace('/', ':');
    } else {
      url = "jdbc:oracle:thin:@" + host + ":" + port + ":" + db;
    }

    Oracle conn = null;

    ConnectionInfo connectionInfo = (ConnectionInfo)env.getSpecialValue("caucho.oracle");

    if (reuseConnection && (connectionInfo != null) && url.equals(connectionInfo.getUrl())) {
      // Reuse the cached connection
      conn = connectionInfo.getConnection();
    } else {
      conn = new Oracle(env, host, username, password, db, port, driver, url);

      connectionInfo = new ConnectionInfo(url, conn);

      env.setSpecialValue("caucho.oracle", connectionInfo);
    }

    Value value = env.wrapJava(conn);

    return value;
  }

  private static class ConnectionInfo {
    private String _url;
    private Oracle _conn;

    public ConnectionInfo(String url, Oracle conn)
    {
      _url = url;
      _conn = conn;
    }

    public String getUrl()
    {
      return _url;
    }

    public Oracle getConnection()
    {
      return _conn;
    }
  }
}
