/*
 * 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 Sam
 */

package com.caucho.quercus.lib.db;

import java.sql.*;

import java.util.Iterator;
import java.util.Map;
import java.util.Collections;
import java.util.Set;
import java.util.ArrayList;

import java.util.logging.Logger;
import java.util.logging.Level;
import java.io.InputStream;
import java.io.IOException;

import com.caucho.util.L10N;
import com.caucho.util.IntMap;

import com.caucho.quercus.env.*;

import com.caucho.quercus.module.Optional;
import com.caucho.quercus.module.Reference;
import com.caucho.quercus.module.ReadOnly;
import com.caucho.quercus.lib.db.PDO;
import com.caucho.quercus.lib.db.PDOError;
import com.caucho.quercus.UnimplementedException;
import com.caucho.quercus.lib.file.FileReadValue;
import com.caucho.vfs.TempBuffer;
import com.caucho.vfs.TempReadStream;
import com.caucho.vfs.ReadStream;

/**
 * PDO object oriented API facade.
 */
public class PDOStatement
  implements Iterable<Value>, java.io.Closeable
{
  private static final Logger log = Logger.getLogger(PDOStatement.class.getName());
  private static final L10N L = new L10N(PDOStatement.class);

  private static final Value[] NULL_VALUES = new Value[0];

  private static final Value FETCH_FAILURE = new BooleanValue(false) {};
  private static final Value FETCH_EXHAUSTED = new BooleanValue(false) {};
  private static final Value FETCH_CONTINUE = new BooleanValue(false) {};
  private static final Value FETCH_SUCCESS = new BooleanValue(true) {};

  private final Env _env;
  private final PDOError _error;

  private final String _query;

  private Statement _statement;
  private PreparedStatement _preparedStatement;

  private ResultSet _resultSet;
  private ResultSetMetaData _resultSetMetaData;
  private boolean _resultSetExhausted = true;
  private String _lastInsertId;

  private int _fetchMode = PDO.FETCH_BOTH;
  private Value[] _fetchModeArgs = NULL_VALUES;
  private ArrayList<BindColumn> _bindColumns;
  private ArrayList<BindParam> _bindParams;
  private IntMap _parameterNameMap;

  PDOStatement(Env env, Connection conn, String query, boolean isPrepared, ArrayValue options)
    throws SQLException

  {
    _env = env;
    _error = new PDOError(_env);

    _query = query;

    env.addClose(this);

    if (options != null && options.getSize() > 0) {
      _env.notice(L.l("PDOStatement options unsupported"));
    }

    query = parseQueryString(query);

    if (isPrepared) {
      _statement = null;
      _preparedStatement = conn.prepareStatement(query);
      _preparedStatement.setEscapeProcessing(false);
    }
    else {
      _preparedStatement = null;

      Statement statement = null;

      try {
        statement = conn.createStatement();
        statement.setEscapeProcessing(false);

        if (statement.execute(query)) {
          _resultSet = statement.getResultSet();
          _resultSetExhausted = false;
        }

        _statement = statement;

        statement = null;

      } finally {
        try {
          if (statement != null)
            statement.close();
        } catch (SQLException e) {
          log.log(Level.FINE, e.toString(), e);
        }
      }
    }
  }

  // side-effect, updates _parameterNameMap
  private String parseQueryString(String query)
  {
    final int queryLength = query.length();
    StringBuilder parsedQuery = new StringBuilder(queryLength);

    int parameterCount = 0;
    StringBuilder name = null;

    int quote = 0;

    for (int i = 0; i < queryLength + 1; i++) {
      int ch = -1;

      if (i < queryLength)
        ch = query.charAt(i);

      if (ch == '\'' || ch == '"') {
        if (quote == 0)
          quote = ch;
        else if (quote == ch)
          quote = 0;
      }
      else if (quote == 0 && ch == '?') {
        parameterCount++;
      }
      else if (quote == 0 && ch == ':') {
        parameterCount++;
        name = new StringBuilder();
        continue;
      }
      else if (name != null && (ch == -1 || !Character.isJavaIdentifierStart(ch))) {
        if (_parameterNameMap == null)
          _parameterNameMap = new IntMap();

        _parameterNameMap.put(name.toString(), parameterCount);

        parsedQuery.append('?');

        name = null;
      }

      if (ch != -1) {
        if (name != null)
          name.append((char) ch);
        else
          parsedQuery.append((char) ch);
      }
    }

    return parsedQuery.toString();
  }

  private boolean advanceResultSet()
  {
    if (_resultSet == null || _resultSetExhausted)
      return false;

    try {
      boolean isNext =  _resultSet.next();

      if (!isNext)
        _resultSetExhausted = true;

      if (!isNext)
        return false;

      if (_bindColumns != null) {
        for (BindColumn bindColumn : _bindColumns)
          if (!bindColumn.bind())
            return false;
      }

      return isNext;
    }
    catch (SQLException ex) {
      _error.error(ex);
      return false;
    }
  }

  public boolean bindColumn(Value column, @Reference Value var, @Optional("-1") int type)
  {
    if (_bindColumns == null)
      _bindColumns = new ArrayList<BindColumn>();

    try {
      _bindColumns.add(new BindColumn(column, var, type));
    }
    catch (SQLException ex) {
      _error.error(ex);
      return false;
    }

    return true;
  }

  public boolean bindParam(Value parameter,
                           @Reference Value variable,
                           @Optional("-1") int dataType,
                           @Optional("-1") int length,
                           @Optional Value driverOptions)
  {
    if (length != -1)
      throw new UnimplementedException("length");

    if (!(driverOptions == null || driverOptions.isNull()))
      throw new UnimplementedException("driverOptions");

    if (dataType == -1)
      dataType = PDO.PARAM_STR;

    boolean isInputOutput = (dataType & PDO.PARAM_INPUT_OUTPUT) != 0;

    if (isInputOutput) {
      dataType = dataType & (~PDO.PARAM_INPUT_OUTPUT);
      if (true) throw new UnimplementedException("PARAM_INPUT_OUTPUT");
    }

    switch (dataType) {
      case PDO.PARAM_BOOL:
      case PDO.PARAM_INT:
      case PDO.PARAM_LOB:
      case PDO.PARAM_NULL:
      case PDO.PARAM_STMT:
      case PDO.PARAM_STR:
        break;

      default:
        _error.warning(L.l("unknown dataType `{0}'", dataType));
        return false;
    }

    if (_bindParams == null)
      _bindParams = new ArrayList<BindParam>();

    BindParam bindParam = new BindParam(parameter, variable, dataType, length, driverOptions);

    _bindParams.add(bindParam);

    return true;
  }

  public boolean bindValue(Value parameter,
                           Value value,
                           @Optional("-1") int dataType)
  {
    return bindParam(parameter, value.toValue(), dataType, -1, null);
  }

  /**
   * Closes the current cursor.
   */
  public boolean closeCursor()
  {
    if (_resultSet == null)
      return false;

    ResultSet resultSet = _resultSet;

    _resultSet = null;
    _resultSetMetaData = null;
    _resultSetExhausted = true;
    _lastInsertId = null;

    try {
      resultSet.close();
    }
    catch (SQLException e) {
      _error.error(e);

      return false;
    }

    return true;
  }

  /**
   * Returns the number of columns.
   */
  public int columnCount()
  {
    if (_resultSet == null)
      return 0;

    try {
      return getResultSetMetaData().getColumnCount();
    }
    catch (SQLException e) {
      _error.error(e);

      return 0;
    }
  }

  public BindParam createBindParam(Value parameter, Value value, int dataType, int length, Value driverOptions)
  {
    return new BindParam(parameter, value, dataType, length, driverOptions);
  }

  public void close()
  {
    ResultSet resultSet = _resultSet;
    Statement statement = _statement;
    PreparedStatement preparedStatement = _preparedStatement;

    _resultSet = null;
    _resultSetMetaData = null;
    _resultSetExhausted = true;
    _lastInsertId = null;
    _statement = null;
    _preparedStatement = null;

    if (resultSet != null)  {
      try {
        resultSet.close();
      }
      catch (SQLException e) {
        log.log(Level.WARNING, e.toString(), e);
      }
    }

    if (statement != null)  {
      try {
        statement.close();
      }
      catch (SQLException e) {
        log.log(Level.WARNING, e.toString(), e);
      }
    }

    if (preparedStatement != null)  {
      try {
        preparedStatement.close();
      }
      catch (SQLException e) {
        log.log(Level.WARNING, e.toString(), e);
      }
    }
  }

  public String errorCode()
  {
    return _error.errorCode();
  }

  public ArrayValue errorInfo()
  {
    return _error.errorInfo();
  }

  /**
   * Execute the statement.
   *
   * @param inputParameters an array containing input values to correspond to
   * the bound parameters for the statement.
   *
   * @return true for success, false for failure
   */
  public boolean execute(@Optional @ReadOnly Value inputParameters)
  {
    // XXX: s/b to do this with ArrayValue arg, but cannot differentiate between
    // no args and bad arg that isn't an ArrayValue
    ArrayValue parameters;

    if (inputParameters instanceof ArrayValue)
      parameters = (ArrayValue) inputParameters;
    else if (inputParameters instanceof DefaultValue)
      parameters = null;
    else {
      _env.warning(L.l("'{0}' is an unexpected argument, expected ArrayValue", inputParameters));
      return false;
    }

    closeCursor();

    try {
      _preparedStatement.clearParameters();
      _preparedStatement.clearWarnings();

      if (parameters != null) {
        for (Map.Entry<Value, Value> entry : parameters.entrySet()) {
          Value key = entry.getKey();

          if (key.isNumberConvertible()) {
            if (! setParameter(key.toInt() + 1, entry.getValue(), -1))
              return false;
          }
          else {
            if (! setParameter(resolveParameter(key), entry.getValue(), -1))
              return false;
          }
        }
      }
      else if (_bindParams != null) {
        for (BindParam bindParam : _bindParams) {
          if (!bindParam.apply())
            return false;
        }
      }

      if (_preparedStatement.execute()) {
        _resultSet = _preparedStatement.getResultSet();
        _resultSetExhausted = false;
      }

      SQLWarning sqlWarning = _preparedStatement.getWarnings();

      if (sqlWarning != null) {
        _error.error(sqlWarning);
        return false;
      }

      return true;
    } catch (SQLException e) {
      _error.error(e);

      return false;
    }
  }

  /**
   * Fetch the next row.
   *
   * @param fetchMode the mode, 0 to use the value set by {@link #setFetchMode}.
   * @return a value, BooleanValue.FALSE if there are no more rows or an error occurs.
   */
  public Value fetch(@Optional int fetchMode,
                     @Optional("-1") int cursorOrientation,
                     @Optional("-1") int cursorOffset)
  {
    if (cursorOrientation != -1)
      throw new UnimplementedException("fetch with cursorOrientation");

    if (cursorOffset != -1)
      throw new UnimplementedException("fetch with cursorOffset");

    return fetchImpl(fetchMode, -1);
  }

  /**
   *
   * @param fetchMode
   * @param columnIndex 0-based column index when fetchMode is FETCH_BOTH
   */
  public Value fetchAll(@Optional("0") int fetchMode, @Optional("-1") int columnIndex)
  {
    int effectiveFetchMode;

    if (fetchMode == 0) {
      effectiveFetchMode = _fetchMode;
    }
    else {
      effectiveFetchMode = fetchMode;
    }

    boolean isGroup = (fetchMode & PDO.FETCH_GROUP) != 0;
    boolean isUnique = (fetchMode & PDO.FETCH_UNIQUE) != 0;

    if (isGroup)
      throw new UnimplementedException("PDO.FETCH_GROUP");

    if (isUnique)
      throw new UnimplementedException("PDO.FETCH_UNIQUE");

    effectiveFetchMode = effectiveFetchMode & (~(PDO.FETCH_GROUP | PDO.FETCH_UNIQUE));

    switch (effectiveFetchMode) {
      case PDO.FETCH_COLUMN:
        break;

      case PDO.FETCH_LAZY:
        _error.warning(L.l("PDO::FETCH_LAZY can't be used with PDOStatement::fetchAll()"));
        return BooleanValue.FALSE;

      default:
        if (columnIndex != -1) {
          _error.warning(L.l("unexpected arguments"));
          return BooleanValue.FALSE;
        }
    }

    ArrayValueImpl rows = new ArrayValueImpl();

    while (true) {
      Value value = fetchImpl(effectiveFetchMode, columnIndex);

      if (value == FETCH_FAILURE) {
        rows.clear();
        return rows;
      }

      if (value == FETCH_EXHAUSTED)
        break;

      if (value == FETCH_CONTINUE)
        continue;

      rows.put(value);
    }

    return rows;
  }

  private Value fetchAssoc()
  {
    try {
      if (!advanceResultSet())
        return FETCH_EXHAUSTED;

      if (_fetchModeArgs.length != 0) {
        _error.notice(L.l("unexpected arguments"));
        return FETCH_FAILURE;
      }

      ArrayValueImpl array = new ArrayValueImpl();

      int columnCount = getResultSetMetaData().getColumnCount();

      for (int i = 1; i <= columnCount; i++) {
        String name = getResultSetMetaData().getColumnName(i);
        Value value = getColumnValue(i);

        array.put(new StringValueImpl(name), value);
      }

      return array;
    }
    catch (SQLException ex) {
      _error.error(ex);
      return FETCH_FAILURE;
    }
  }

  private Value fetchBoth()
  {
    try {
      if (!advanceResultSet())
        return FETCH_EXHAUSTED;

      if (_fetchModeArgs.length != 0) {
        _error.notice(L.l("unexpected arguments"));
        return FETCH_FAILURE;
      }


      ArrayValueImpl array = new ArrayValueImpl();

      int columnCount = getResultSetMetaData().getColumnCount();

      for (int i = 1; i <= columnCount; i++) {
        String name = getResultSetMetaData().getColumnName(i);
        Value value = getColumnValue(i);

        array.put(new StringValueImpl(name), value);
        array.put(new LongValue(i - 1), value);
      }

      return array;
    }
    catch (SQLException ex) {
      _error.error(ex);
      return FETCH_FAILURE;
    }
  }

  private Value fetchBound()
  {
    if (!advanceResultSet())
      return FETCH_EXHAUSTED;

    return FETCH_SUCCESS;
  }

  private Value fetchClass()
  {
    String className;
    Value[] ctorArgs;

    if (_fetchModeArgs.length == 0 || _fetchModeArgs.length > 2)
      return fetchBoth();

    className = _fetchModeArgs[0].toString();

    if (_fetchModeArgs.length == 2) {
      if (_fetchModeArgs[1].isArray()) {
        // XXX: inefiicient, but args[1].getValueArray(_env) doesn't handle references
        ArrayValue argsArray = (ArrayValue) _fetchModeArgs[1];

        ctorArgs = new Value[argsArray.getSize()];

        int i = 0;

        for (Value key : argsArray.keySet())
          ctorArgs[i++] = argsArray.getRef(key);
      }
      else
        return fetchBoth();
    }
    else
      ctorArgs = NULL_VALUES;

    return fetchObject(className, ctorArgs);
  }

  /**
   * @param column 0-based column number
   */
  public Value fetchColumn(@Optional int column)
  {
    if (!advanceResultSet())
      return FETCH_EXHAUSTED;

    if (column < 0 && _fetchModeArgs.length > 0)
      column = _fetchModeArgs[0].toInt();

    try {
      if (column < 0 || column >= getResultSetMetaData().getColumnCount())
        return FETCH_CONTINUE;

      return getColumnValue(column + 1);
    }
    catch (SQLException ex) {
      _error.error(ex);
      return FETCH_FAILURE;
    }
  }

  private Value fetchFunc()
  {
    throw new UnimplementedException();
  }

  /**
   * Fetch the next row.
   *
   * @param fetchMode the mode, 0 to use the value set by {@link #setFetchMode}.
   * @return a value, BooleanValue.FALSE if there are no more rows or an error occurs.
   */
  private Value fetchImpl(int fetchMode, int columnIndex)
  {
    if (fetchMode == 0) {
      fetchMode = _fetchMode;

      fetchMode = fetchMode & (~(PDO.FETCH_GROUP | PDO.FETCH_UNIQUE));
    }
    else {
      if ((fetchMode & PDO.FETCH_GROUP) != 0) {
        _error.warning(L.l("FETCH_GROUP is not allowed"));
        return BooleanValue.FALSE;
      }
      else if ((fetchMode & PDO.FETCH_UNIQUE) != 0) {
        _error.warning(L.l("FETCH_UNIQUE is not allowed"));
        return BooleanValue.FALSE;
      }
    }

    boolean isClasstype = (fetchMode & PDO.FETCH_CLASSTYPE) != 0;
    boolean isSerialize = (fetchMode & PDO.FETCH_SERIALIZE) != 0;

    fetchMode = fetchMode & (~(PDO.FETCH_CLASSTYPE | PDO.FETCH_SERIALIZE));

    switch (fetchMode) {
      case PDO.FETCH_ASSOC:
        return fetchAssoc();

      case PDO.FETCH_BOTH:
        return fetchBoth();

      case PDO.FETCH_BOUND:
        return fetchBound();

      case PDO.FETCH_COLUMN:
        return fetchColumn(columnIndex);

      case PDO.FETCH_CLASS:
        return fetchClass();

      case PDO.FETCH_FUNC:
        return fetchFunc();

      case PDO.FETCH_INTO:
        return fetchInto();

      case PDO.FETCH_LAZY:
        return fetchLazy();

      case PDO.FETCH_NAMED:
        return fetchNamed();

      case PDO.FETCH_NUM:
        return fetchNum();

      case PDO.FETCH_OBJ:
        return fetchObject();

    default:
      _error.warning(L.l("invalid fetch mode {0}",  fetchMode));
      closeCursor();
      return BooleanValue.FALSE;
    }
  }

  private Value fetchInto()
  {
    assert _fetchModeArgs.length > 0;
    assert _fetchModeArgs[0].isObject();

    Value var = _fetchModeArgs[0];

    if (!advanceResultSet())
      return FETCH_EXHAUSTED;

    try {
      int columnCount = getResultSetMetaData().getColumnCount();

      for (int i = 1; i <= columnCount; i++) {
        String name = getResultSetMetaData().getColumnName(i);
        Value value = getColumnValue(i);

        var.putField(_env, name, value);
      }
    }
    catch (SQLException ex) {
      _error.error(ex);
      return FETCH_FAILURE;
    }

    return var;
  }

  private Value fetchLazy()
  {
    // XXX: need to check why lazy is no different than object
    return fetchObject(null, NULL_VALUES);
  }

  private Value fetchNamed()
  {
    try {
      if (!advanceResultSet())
        return FETCH_EXHAUSTED;

      ArrayValue array = new ArrayValueImpl();

      int columnCount = getResultSetMetaData().getColumnCount();

      for (int i = 1; i <= columnCount; i++) {
        Value name = new StringValueImpl(getResultSetMetaData().getColumnName(i));
        Value value = getColumnValue(i);

        Value existingValue = array.get(name);

        if (! (existingValue instanceof UnsetValue)) {

          if (! existingValue.isArray()) {
            ArrayValue arrayValue = new ArrayValueImpl();
            arrayValue.put(existingValue);
            array.put(name, arrayValue);
            existingValue = arrayValue;
          }

          existingValue.put(value);
        }
        else
          array.put(name, value);
      }

      return array;
    }
    catch (SQLException ex) {
      _error.error(ex);
      return FETCH_FAILURE;
    }
  }

  private Value fetchNum()
  {
    try {
      if (!advanceResultSet())
        return FETCH_EXHAUSTED;

      if (_fetchModeArgs.length != 0) {
        _error.notice(L.l("unexpected arguments"));
        return FETCH_FAILURE;
      }

      ArrayValueImpl array = new ArrayValueImpl();

      int columnCount = getResultSetMetaData().getColumnCount();

      for (int i = 1; i <= columnCount; i++) {
        Value value = getColumnValue(i);

        array.put(value);
      }

      return array;
    }
    catch (SQLException ex) {
      _error.error(ex);
      return FETCH_FAILURE;
    }
  }

  private Value fetchObject()
  {
    return fetchObject(null, NULL_VALUES);
  }

  public Value fetchObject(@Optional String className, @Optional Value[] args)
  {
    QuercusClass cl;

    if (className != null) {
      cl = _env.findAbstractClass(className);

      if (cl == null)
        return fetchBoth();
    }
    else {
      cl = null;

      if (args.length != 0) {
        advanceResultSet();
        _error.warning(L.l("unexpected arguments"));
        return BooleanValue.FALSE;
      }
    }

    if (!advanceResultSet())
      return FETCH_EXHAUSTED;

    try {
      Value object;

      if (cl != null)
        object = cl.callNew(_env, args);
      else
        object = _env.createObject();

      int columnCount = getResultSetMetaData().getColumnCount();

      for (int i = 1; i <= columnCount; i++) {
        String name = getResultSetMetaData().getColumnName(i);
        Value value = getColumnValue(i);

        object.putField(_env, name, value);
      }

      return object;
    }
    catch (Throwable ex) {
      _error.error(ex);
      return FETCH_FAILURE;
    }

  }

  public Value getAttribute(int attribute)
  {
    _error.unsupportedAttribute(attribute);

    return BooleanValue.FALSE;
  }

  /**
   * @param column 0-based column index
   */
  public Value getColumnMeta(int column)
  {
    throw new UnimplementedException();
  }

  /**
   * @param column 1-based column index
   */
  private Value getColumnValue(int column)
    throws SQLException
  {
    return getColumnValue(column, -1, -1);
  }

  /**
   * @param column 1-based column index
   * @param jdbcType a jdbc type, or -1 if it is unknown
   * @param returnType a PDO.PARAM_* type, or -1
   */
  private Value getColumnValue(int column, int jdbcType, int returnType)
    throws SQLException
  {
    if (returnType != -1)
      throw new UnimplementedException("parm type " + returnType);

    if (jdbcType == -1)
      jdbcType = getResultSetMetaData().getColumnType(column);

    // XXX: needs tests

    switch (jdbcType) {
      case Types.NULL:
        return NullValue.NULL;

      case Types.BIT:
      case Types.TINYINT:
      case Types.SMALLINT:
      case Types.INTEGER:
      case Types.BIGINT:
      {
        String value = _resultSet.getString(column);

        if (value == null || _resultSet.wasNull())
          return NullValue.NULL;
        else
          return new StringValueImpl(value);
      }

      case Types.DOUBLE:
      {
        double value = _resultSet.getDouble(column);

        if (_resultSet.wasNull())
          return NullValue.NULL;
        else
          return (new DoubleValue(value)).toStringValue();
      }

      // XXX: lob

      default:
      {
        String value = _resultSet.getString(column);

        if (value == null || _resultSet.wasNull())
          return NullValue.NULL;
        else
          return new StringValueImpl(value);
      }
    }

  }

  private ResultSetMetaData getResultSetMetaData()
    throws SQLException
  {
    if (_resultSetMetaData == null)
      _resultSetMetaData = _resultSet.getMetaData();

    return _resultSetMetaData;
  }

  /**
   * Returns an iterator of the values.
   */
  public Iterator<Value> iterator()
  {
    Value value = fetchAll(0, -1);

    if (value instanceof ArrayValue)
      return ((ArrayValue) value).values().iterator();
    else {
      Set<Value> emptySet = Collections.emptySet();
      return emptySet.iterator();
    }
  }

  String lastInsertId(String name)
  {
    if (!(name == null || name.length() == 0))
      throw new UnimplementedException("lastInsertId with name ");

    if (_lastInsertId != null)
      return _lastInsertId;

    String lastInsertId = null;

    Statement stmt;

    if (_preparedStatement != null)
      stmt = _preparedStatement;
    else
      stmt = _statement;

    ResultSet resultSet = null;

    try {
      resultSet = stmt.getGeneratedKeys();

      if (resultSet.next())
        lastInsertId = resultSet.getString(1);
    }
    catch (SQLException ex) {
      _error.error(ex);
    }
    finally {
      try {
        if (resultSet != null)
          resultSet.close();
      }
      catch (SQLException ex) {
        log.log(Level.WARNING, ex.toString(), ex);
      }
    }

    _lastInsertId = lastInsertId == null ? "0" : lastInsertId;

    return _lastInsertId;
  }

  public boolean nextRowset()
  {
    throw new UnimplementedException();
  }

  private int resolveParameter(Value parameter)
  {
    int index = -1;

    if (parameter instanceof LongValue) {
      // slight optimization for normal case
      index = parameter.toInt();
    }
    else {
      String name = parameter.toString();

      if (name.length() > 1 && name.charAt(0) == ':') {
        name = name.substring(1);
        if (_parameterNameMap != null)
          index = _parameterNameMap.get(name);
      }
      else
        index = parameter.toInt();
    }

    return index;
  }

  public int rowCount()
  {
    if (_resultSet == null)
      return 0;

    try {
      int row = _resultSet.getRow();

      try {
        _resultSet.last();

        return _resultSet.getRow();
      }
      finally {
        if (row == 0)
          _resultSet.beforeFirst();
        else
          _resultSet.absolute(row);
      }
    }
    catch (SQLException ex) {
      _error.error(ex);
      return 0;
    }
  }

  public boolean setAttribute(int attribute, Value value)
  {
    return setAttribute(attribute, value, false);
  }

  public boolean setAttribute(int attribute, Value value, boolean isFromConstructor)
  {
    if (isFromConstructor) {
      switch (attribute) {
        case PDO.CURSOR_FWDONLY:
        case PDO.CURSOR_SCROLL:
          return setCursor(attribute);
      }
    }

    _error.unsupportedAttribute(attribute);

    return false;
  }

  private boolean setCursor(int attribute)
  {
    switch (attribute) {
      case PDO.CURSOR_FWDONLY:
        throw new UnimplementedException();
      case PDO.CURSOR_SCROLL:
        throw new UnimplementedException();

      default:
        _error.unsupportedAttribute(attribute);
        return false;
    }
  }


  /**
   * Sets the fetch mode, the default is {@link PDO.FETCH_BOTH}.
   */
  public boolean setFetchMode(int fetchMode, Value[] args)
  {
    _fetchMode = PDO.FETCH_BOTH;
    _fetchModeArgs = NULL_VALUES;

    int fetchStyle = fetchMode;

    boolean isGroup = (fetchMode & PDO.FETCH_GROUP) != 0;
    boolean isUnique = (fetchMode & PDO.FETCH_UNIQUE) != 0;

    if (isGroup)
      throw new UnimplementedException("PDO.FETCH_GROUP");

    if (isUnique)
      throw new UnimplementedException("PDO.FETCH_UNIQUE");

    fetchStyle = fetchStyle & (~(PDO.FETCH_GROUP | PDO.FETCH_UNIQUE));

    boolean isClasstype = (fetchMode & PDO.FETCH_CLASSTYPE) != 0;
    boolean isSerialize = (fetchMode & PDO.FETCH_SERIALIZE) != 0;

    fetchStyle = fetchStyle & (~(PDO.FETCH_CLASSTYPE | PDO.FETCH_SERIALIZE));

    switch (fetchStyle) {
      case PDO.FETCH_ASSOC:
      case PDO.FETCH_BOTH:
      case PDO.FETCH_BOUND:
      case PDO.FETCH_LAZY:
      case PDO.FETCH_NAMED:
      case PDO.FETCH_NUM:
      case PDO.FETCH_OBJ:
        break;

      case PDO.FETCH_CLASS:
        if (args.length < 1 || args.length > 2)
          return false;

        if (_env.findAbstractClass(args[0].toString()) == null)
          return false;

        if (args.length == 2 && !(args[1].isNull() || args[1].isArray())) {
          _env.warning(L.l("constructor args must be an array"));

          return false;
        }

        break;

      case PDO.FETCH_COLUMN:
        if (args.length != 1)
          return false;

        break;

     case PDO.FETCH_FUNC:
       _error.warning(L.l("PDO::FETCH_FUNC can only be used with PDOStatement::fetchAll()"));
       return false;

      case PDO.FETCH_INTO:
        if (args.length != 1 || !args[0].isObject())
          return false;

        break;

      default:
        _error.warning(L.l("invalid fetch mode"));
        break;
    }

    _fetchModeArgs = args;
    _fetchMode = fetchMode;

    return true;
  }

  /**
   * @param index 1-based position number
   * @param value the value for the parameter
   *
   * @return true for success, false for failure
   */
  private boolean setLobParameter(int index, Value value, long length)
  {
    try {
      if (value == null || value.isNull()) {
        _preparedStatement.setObject(index, null);
      }
      else if (value instanceof StringValue) {
        if (length < 0) {
          _preparedStatement.setBinaryStream(index, value.toInputStream(), value.toString().length());
        }
        else
          _preparedStatement.setBinaryStream(index, value.toInputStream(), (int) length);
      }
      else {
        InputStream inputStream = value.toInputStream();

        if (inputStream == null) {
          _error.warning(L.l("type {0} ({1}) for parameter index {2} cannot be used for lob", value.getType(), value.getClass(), index));
          return false;
        }

        if (length < 0 && (value instanceof FileReadValue)) {
          length = ((FileReadValue) value).getLength();

          if (length <= 0)
            length = -1;
        }

        if (length < 0) {
          TempBuffer tempBuffer = TempBuffer.allocate();

          try {
            byte[] bytes = new byte[1024];

            int len;

            while ((len = inputStream.read(bytes, 0, 1024)) != -1)
              tempBuffer.write(bytes, 0, len);
          }
          catch (IOException ex) {
            _error.error(ex);
            return false;
          }

          TempReadStream tempReadStream = new TempReadStream(tempBuffer);
          tempReadStream.setFreeWhenDone(true);

          _preparedStatement.setBinaryStream(index, new ReadStream(tempReadStream), tempBuffer.getLength());
        }
        else
          _preparedStatement.setBinaryStream(index, inputStream, (int) length);
      }
    }
    catch (SQLException ex) {
      _error.error(ex);
      return false;
    }

    return true;
  }

  /**
   * @param index 1-based position number
   * @param value the value for the parameter
   *
   * @return true for success, false for failure
   */
  private boolean setParameter(int index, Value value, long length)
  {
    try {
      if (value instanceof DoubleValue) {
        _preparedStatement.setDouble(index, value.toDouble());
      }
      else if (value instanceof LongValue) {
        _preparedStatement.setLong(index, value.toLong());
      }
      else if (value instanceof StringValue) {
        String string = value.toString();

        if (length >= 0)
          string = string.substring(0, (int) length);

        _preparedStatement.setString(index, string);
      }
      else if (value instanceof NullValue) {
        _preparedStatement.setObject(index, null);
      }
      else {
        _error.warning(L.l("unknown type {0} ({1}) for parameter index {2}", value.getType(), value.getClass(), index));
        return false;
      }
    }
    catch (SQLException ex) {
      _error.error(ex);
      return false;
    }

    return true;
  }

  public String toString()
  {
    return "PDOStatement[" + _query + "]";
  }

  /**
   * Bind a value from a resultSet to a variable.
   */
  private class BindColumn {
    private final String _columnAsName;
    private final Value _var;
    private final int _type;

    private int _column;
    private int _jdbcType;

    private boolean _isInit;
    private boolean _isValid;

    /**
     * @param column 1-based column index
     * @param var reference that receives the value
     * @param type a PARM_* type, -1 for default
     */
    private BindColumn(Value column, Value var, int type)
      throws SQLException
    {
      assert column != null;
      assert var != null;

      if (column.isNumberConvertible()) {
        _column = column.toInt();
        _columnAsName = null;
      }
      else {
        _columnAsName = column.toString();
      }

      _var = var;
      _type = type;

      if (_resultSet != null)
        init();
    }

    private boolean init()
      throws SQLException
    {
      if (_isInit)
        return true;

      ResultSetMetaData resultSetMetaData = getResultSetMetaData();

      int columnCount = resultSetMetaData.getColumnCount();

      if (_columnAsName != null) {

        for (int i = 1; i <= columnCount; i++) {
          String name = resultSetMetaData.getColumnName(i);
          if (name.equals(_columnAsName)) {
            _column = i;
            break;
          }
        }
      }

      _isValid = _column > 0 && _column <= columnCount;

      if (_isValid) {
        _jdbcType = resultSetMetaData.getColumnType(_column);
      }

      _isInit = true;

      return true;
    }

    public boolean bind()
      throws SQLException
    {
      if (!init())
        return false;

      if (!_isValid) {
        // this matches php behaviour
        _var.set(StringValueImpl.EMPTY);
      }
      else {
        Value value = getColumnValue(_column, _jdbcType, _type);

        _var.set(value);
      }

      return true;
    }
  }

  /**
   * Bind a value to a parameter when the statement is executed.
   */
  private class BindParam {
    private final int _index;
    private final Value _value;
    private final int _dataType;
    private final int _length;
    private final Value _driverOptions;

    public BindParam(Value parameter, Value value, int dataType, int length, Value driverOptions)
    {
      int index = resolveParameter(parameter);

      _index = index;
      _value = value;
      _dataType = dataType;
      _length = length;
      _driverOptions = driverOptions;
    }

    public boolean apply()
      throws SQLException
    {
      switch (_dataType) {
        case PDO.PARAM_BOOL:
        case PDO.PARAM_INT:
        case PDO.PARAM_STR:
          return setParameter(_index, _value.toValue(), _length);
        case PDO.PARAM_LOB:
          return setLobParameter(_index, _value.toValue(), _length);
        case PDO.PARAM_NULL:
          return setParameter(_index, NullValue.NULL, _length);
        case PDO.PARAM_STMT:
          throw new UnimplementedException("PDO.PARAM_STMT");
        default:
          throw new AssertionError();
      }
    }
  }
}
