/*
 * Copyright (c) 1998-2012 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 Scott Ferguson
 */

package com.caucho.quercus.lib.db;

import com.caucho.quercus.annotation.NotNull;
import com.caucho.quercus.annotation.Optional;
import com.caucho.quercus.env.BooleanValue;
import com.caucho.quercus.env.ConstStringValue;
import com.caucho.quercus.env.Env;
import com.caucho.quercus.env.LongValue;
import com.caucho.quercus.env.NullValue;
import com.caucho.quercus.env.ObjectValue;
import com.caucho.quercus.env.StringValue;
import com.caucho.quercus.env.Value;
import com.caucho.quercus.module.AbstractQuercusModule;
import com.caucho.util.L10N;
import com.caucho.util.Log;

import java.sql.Connection;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * PHP mysql routines.
 */
public class MysqlModule extends AbstractQuercusModule {

  private static final Logger log = Log.open(MysqlModule.class);
  private static final L10N L = new L10N(MysqlModule.class);

  public static final int MYSQL_ASSOC = JdbcResultResource.FETCH_ASSOC;
  public static final int MYSQL_NUM = JdbcResultResource.FETCH_NUM;
  public static final int MYSQL_BOTH = JdbcResultResource.FETCH_BOTH;

  public static final int MYSQL_USE_RESULT = 0x0;
  public static final int MYSQL_STORE_RESULT = 0x1;

  private static final StringValue SV_NAME
    = new ConstStringValue("name");
  private static final StringValue SV_TABLE
    = new ConstStringValue("table");
  private static final StringValue SV_DEF
    = new ConstStringValue("def");
  private static final StringValue SV_MAX_LENGTH
    = new ConstStringValue("max_length");
  private static final StringValue SV_NOT_NULL
    = new ConstStringValue("not_null");
  private static final StringValue SV_PRIMARY_KEY
    = new ConstStringValue("primary_key");
  private static final StringValue SV_MULTIPLE_KEY
    = new ConstStringValue("multiple_key");
  private static final StringValue SV_UNIQUE_KEY
    = new ConstStringValue("unique_key");
  private static final StringValue SV_NUMERIC
    = new ConstStringValue("numeric");
  private static final StringValue SV_BLOB
    = new ConstStringValue("blob");
  private static final StringValue SV_TYPE
    = new ConstStringValue("type");
  private static final StringValue SV_UNSIGNED
    = new ConstStringValue("unsigned");
  private static final StringValue SV_ZEROFILL
    = new ConstStringValue("zerofill");

  public MysqlModule()
  {
  }

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

  /**
   * Returns the number of affected rows.
   */
  public static int mysql_affected_rows(Env env, @Optional Mysqli conn)
  {
    if (conn == null)
      conn = getConnection(env);

    return conn.affected_rows(env);
  }

  /**
   * Get information about the most recent query.
   */
  public static Value mysql_info(Env env, @Optional Mysqli conn)
  {
    if (conn == null)
      conn = getConnection(env);

    return conn.info(env);
  }

  /**
   * Change the logged in user of the current active connection.
   * This function is deprecated and was removed from PHP in PHP 3.0.14.
   */

  public static boolean mysql_change_user(Env env,
                                          StringValue user,
                                          StringValue pass,
                                          @Optional StringValue database,
                                          @Optional Mysqli conn)
  {
    return false;
  }

  /**
   * Returns the client encoding
   */
  public static StringValue mysql_client_encoding(
      Env env, @Optional Mysqli conn)
  {
    if (conn == null)
      conn = getConnection(env);

    return conn.client_encoding(env);
  }

  /**
   * Closes a mysql connection.
   */
  public static boolean mysql_close(Env env, @Optional Mysqli conn)
  {
    boolean isEnvConn = false;

    if (conn == null) {
      conn = (Mysqli) env.getSpecialValue("caucho.mysql");

      isEnvConn = true;
    }

    if (conn == null) {
      // php/1435

      env.warning(L.l("no MySQL-Link resource supplied"));
      return false;
    }

    if (isEnvConn || env.getSpecialValue("caucho.mysql") != null)
      env.removeSpecialValue("caucho.mysql");

    if (conn.isConnected()) {
      conn.close();

      return true;
    }
    else {
      env.warning(
          L.l("connection is either not connected or is already closed"));

      return false;
    }
  }

  /**
   * Creates a database.
   */
  public static boolean mysql_create_db(Env env,
                                        @NotNull StringValue name,
                                        @Optional Mysqli conn)
  {
    if (name.length() == 0)
      return false;

    if (conn == null)
      conn = getConnection(env);

    Statement stmt = null;

    // XXX: move implementation
    try {
      try {
        Connection sqlConn = conn.validateConnection(env).getConnection(env);

        if (sqlConn == null)
          return false;

        stmt = sqlConn.createStatement();
        stmt.setEscapeProcessing(false);
        stmt.executeUpdate("CREATE DATABASE " + name.toString());
      } finally {
        if (stmt != null)
          stmt.close();
      }
    } catch (SQLException e) {
      log.log(Level.FINE, e.toString(), e);
      return false;
    }

    return true;
  }

  /**
   * Moves the intenal row pointer of the MySQL result to the
   * specified row number, 0 based.
   */
  public static boolean mysql_data_seek(Env env,
                                        @NotNull MysqliResult result,
                                        int rowNumber)
  {
    if (result == null)
      return false;

    if (result.seek(env, rowNumber)) {
      return true;
    } else {
      env.warning(
          L.l("Offset {0} is invalid for MySQL "
              + "(or the query data is unbuffered)",
                      rowNumber));
      return false;
    }
  }

  /**
   * Retrieves the database name after a call to mysql_list_dbs()
   */
  public static Value mysql_db_name(Env env,
                                    @NotNull MysqliResult result,
                                    int row,
                                    @Optional("0") Value field)
  {
    if (result == null)
      return BooleanValue.FALSE;

    return mysql_result(env, result, row, field);
  }

  /**
   * Deprecated alias for mysql_db_name
   */
  public static Value mysql_dbname(Env env,
                                   @NotNull MysqliResult result,
                                   int row)
  {
    return mysql_db_name(env, result, row,
                         env.createString("0"));
  }

  /**
   * Returns the value of one field in the result set. FALSE on failure.
   */
  public static Value mysql_result(Env env,
                                   @NotNull MysqliResult result,
                                   int row,
                                   @Optional("0") Value field)
  {
    if (result == null)
      return BooleanValue.FALSE;

    return result.getResultField(env, row, field);
  }

  /**
   * Drops a database.
   */
  public static boolean mysql_drop_db(Env env,
                                      @NotNull StringValue databaseName,
                                      @Optional Mysqli conn)
  {
    if (databaseName.length() == 0)
      return false;

    StringValue query = env.createString("DROP DATABASE " + databaseName);
    Value value = mysql_query(env, query, conn);

    return (value != null && value.toBoolean());
  }

  /**
   * Deprecated alias for mysql_drop_db.
   */
  public static boolean mysql_dropdb(Env env,
                                     @NotNull StringValue databaseName,
                                     @Optional Mysqli conn)
  {
    return mysql_drop_db(env, databaseName, conn);
  }

  /**
   * Returns the error number of the most recent error
   */
  public static int mysql_errno(Env env, @Optional Mysqli conn)
  {
    if (conn == null)
      conn = getConnection(env);

    StringValue error = conn.error(env);

    int errno = conn.errno();

    if (errno != 0)
      return errno;
    else if (error.length() != 0)
      return 2006; // mysql has gone away
    else
      return 0;
  }

  /**
   * Returns the most recent error.
   */
  public static StringValue mysql_error(Env env, @Optional Mysqli conn)
  {
    if (conn == null)
      conn = getConnection(env);

    return conn.error(env);
  }

  /**
   * Deprecated, mysql_real_escape_string() should be used instead.
   *
   * @see StringValue MysqlModule.mysql_real_escape_string(String, Mysqli)
   *
   * @return the escaped string
   */

  public static StringValue mysql_escape_string(Env env, Value val)
  {
    StringValue unescapedString = val.toStringValue();

    StringValue sb = unescapedString.createStringBuilder();

    int len = unescapedString.length();

    for (int i = 0; i < len; i++) {
      char ch = unescapedString.charAt(i);

      switch(ch) {
        case 0:
          sb.append('\\');
          sb.append(0);
          break;
        case '\n':
          sb.append('\\');
          sb.append('n');
          break;
        case '\r':
          sb.append('\\');
          sb.append('r');
          break;
        case '\\':
          sb.append('\\');
          sb.append('\\');
          break;
        case '\'':
          sb.append('\\');
          sb.append('\'');
          break;
        case '"':
          sb.append('\\');
          sb.append('"');
          break;
        case 0x1A:
          sb.append('\\');
          sb.append('Z');
          break;
        default:
          sb.append(ch);
      }
    }

    return sb;
  }

  /**
   * Escapes special characters.
   *
   * @see StringValue MysqliModule.mysqli_real_escape_string(
   * JdbcConnectionResource, String)
   *
   * @return the escaped string
   */

  public static StringValue mysql_real_escape_string(Env env,
                                                     Value val,
                                                     @Optional Mysqli conn)
  {
    StringValue unescapedString = val.toStringValue();

    if (conn == null)
      conn = getConnection(env);

    return conn.real_escape_string(env, unescapedString);
  }

  /**
   * Returns a row from the connection
   */
  public static Value mysql_fetch_array(Env env,
                                        @NotNull MysqliResult result,
                                        @Optional("MYSQL_BOTH") int type)
  {
    if (result == null) {
      return BooleanValue.FALSE;
    }

    Value value = result.fetch_array(env, type);

    if (value != null) {
      return value;
    }
    else {
      return BooleanValue.FALSE;
    }
  }

  /**
   * Returns a row from the connection
   */
  public static Value mysql_fetch_assoc(Env env,
                                        @NotNull MysqliResult result)
  {
    if (result == null) {
      return BooleanValue.FALSE;
    }

    return result.fetch_array(env, MYSQL_ASSOC);
  }

  /**
   * Returns an object containing field information.
   * On success, this method increments the field offset
   * (see {@link #mysql_field_seek}).
   *
   * <h3>ERRATA</h3>
   * <ul>
   *   <li>quercus returns "int" for BIT type, php returns "unknown"
   *   <li>quercus always returns int(0) for unique_key
   *   <li>quercus always returns int(0) for zerofill
   *   <li>quercus always returns int(0) for multiple_key
   * </ul>
   *
   */
  public static Value mysql_fetch_field(Env env,
                                        @NotNull MysqliResult result,
                                        @Optional("-1") int fieldOffset)
  {
    /**
     * ERRATA is also documented in php/142s.qa
     * There is probably a mysql specific query or API that would be better
     * for getting this information
     */

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

    // php/142v.qa - call must succeed even if some info not available

    try {
      if (fieldOffset == -1) {
        fieldOffset = result.field_tell(env);
        result.setFieldOffset(fieldOffset + 1);
      }

      ResultSetMetaData md = result.getMetaData();

      if (md.getColumnCount() <= fieldOffset || fieldOffset < 0) {
        return BooleanValue.FALSE;
      }

      QuercusResultSetMetaData qMd = null;

      if (md instanceof QuercusResultSetMetaData)
        qMd = (QuercusResultSetMetaData) md;

      int jdbcField = fieldOffset + 1;
      int jdbcColumnType = md.getColumnType(jdbcField);

      String catalogName = md.getCatalogName(jdbcField);
      String tableName = md.getTableName(jdbcField);

      String schemaName = md.getSchemaName(jdbcField);

      String columnName = md.getColumnName(jdbcField);
      String columnLabel = md.getColumnLabel(jdbcField);

      if (schemaName == null || "".equals(schemaName))
        schemaName = tableName;

      if ((tableName == null || "".equals(tableName))
          && result.isLastSqlDescribe())
        tableName = "COLUMNS";

      // some information is not available from the ResultSetMetaData
      JdbcColumnMetaData columnMd = null;

      if (qMd == null) {
        JdbcConnectionResource conn = getConnection(env).validateConnection(env);

        // php/141p
        JdbcTableMetaData tableMd
          = conn.getTableMetaData(env, catalogName, null, tableName);

        if (tableMd != null)
          columnMd = tableMd.getColumn(columnName);
      }

      // XXX: maxlen note from PHP comments:
      // the length of the longest value for that field in the returned dataset,
      // NOT the maximum length of data that column is designed to hold.

      int maxLength = 0;
      int notNull = md
          .isNullable(jdbcField) == ResultSetMetaData.columnNullable ? 0 : 1;
      int numeric = JdbcColumnMetaData.isNumeric(jdbcColumnType) ? 1 : 0;
      int blob = JdbcColumnMetaData.isBlob(jdbcColumnType) ? 1 : 0;
      String type = result.getFieldType(fieldOffset, jdbcColumnType);
      int unsigned = md.isSigned(jdbcField) ? 0 : numeric;

      if (jdbcColumnType == Types.BOOLEAN || jdbcColumnType == Types.BIT)
        unsigned = 0;
      else if (jdbcColumnType == Types.DECIMAL)
        numeric = 1;

      int zerofill = 0;
      int primaryKey = 0;
      int multipleKey = 0;
      int uniqueKey = 0;

      if (qMd != null) {
        zerofill = qMd.isZeroFill(jdbcField) ? 1 : 0;
        primaryKey = qMd.isPrimaryKey(jdbcField) ? 1 : 0;
        multipleKey = qMd.isMultipleKey(jdbcField) ? 1 : 0;
        uniqueKey = qMd.isUniqueKey(jdbcField) ? 1 : 0;
        notNull = qMd.isNotNull(jdbcField) ? 1 : 0;
        // maxLength = qMd.getLength(jdbcField);
      }
      else if (columnMd != null) {
        zerofill = columnMd.isZeroFill() ? 1 : 0;
        primaryKey = columnMd.isPrimaryKey() ? 1 : 0;
        // XXX: not sure what multipleKey is supposed to be
        // multipleKey = columnMd.isIndex() && !columnMd.isPrimaryKey() ? 1 : 0;
        uniqueKey = columnMd.isUnique() ? 1 : 0;
      }
      else
        notNull = 1;

      ObjectValue fieldResult = env.createObject();

      fieldResult.putThisField(env, SV_NAME, env.createString(columnLabel));
      fieldResult.putThisField(env, SV_TABLE, env.createString(tableName));
      fieldResult.putThisField(env, SV_DEF, env.getEmptyString());
      fieldResult.putThisField(env, SV_MAX_LENGTH,
                               LongValue.create(maxLength));
      fieldResult.putThisField(env, SV_NOT_NULL,
                               LongValue.create(notNull));
      fieldResult.putThisField(env, SV_PRIMARY_KEY,
                               LongValue.create(primaryKey));
      fieldResult.putThisField(env, SV_MULTIPLE_KEY,
                               LongValue.create(multipleKey));
      fieldResult.putThisField(env, SV_UNIQUE_KEY,
                               LongValue.create(uniqueKey));
      fieldResult.putThisField(env, SV_NUMERIC,
                               LongValue.create(numeric));
      fieldResult.putThisField(env, SV_BLOB,
                               LongValue.create(blob));
      fieldResult.putThisField(env, SV_TYPE, env.createString(type));
      fieldResult.putThisField(env, SV_UNSIGNED,
                               LongValue.create(unsigned));
      fieldResult.putThisField(env, SV_ZEROFILL, LongValue.create(zerofill));

      return fieldResult;
    } catch (SQLException e) {
      log.log(Level.FINE, e.toString(), e);
      return BooleanValue.FALSE;
    }
  }

  /**
   * Executes a query and returns a result set.
   *
   * Returns true on update success, false on failure, and a result set
   * for a successful select
   */
  public static Value mysql_query(Env env,
                                  StringValue sql,
                                  @Optional Mysqli conn)
  {
    if (conn == null)
      conn = getConnection(env);

    return conn.query(env, sql, MYSQL_STORE_RESULT);
  }

  /**
   * Returns an array of lengths.
   */
  public static Value mysql_fetch_lengths(Env env, @NotNull MysqliResult result)
  {
    if (result == null) {
      return BooleanValue.FALSE;
    }

    return result.fetch_lengths();
  }

  /**
   * Returns an object with properties that correspond to the fetched row
   * and moves the data pointer ahead.
   */
  public static Value mysql_fetch_object(Env env,
                                         @NotNull MysqliResult result,
                                         @Optional String className,
                                         @Optional Value[] args)
  {
    if (result == null) {
      return BooleanValue.FALSE;
    }

    Value value = result.fetch_object(env, className, args);

    // php/142t
    // must return FALSE for mediawiki
    if (value.isNull())
      value = BooleanValue.FALSE;

    return value;
  }

  /**
   * Returns a numerical row from the result, FALSE if no more rows.
   */
  public static Value mysql_fetch_row(Env env,
                                      @NotNull MysqliResult result)
  {
    if (result == null) {
      return BooleanValue.FALSE;
    }

    return result.fetch_row(env);
  }

  /**
   * Returns the field flags of the specified field.  The flags
   * are reported as a space separated list of words, the returned
   * value can be split using explode().
   *
   * The following flages are reported, older version of MySQL
   * may not report all flags:
   * <ul>
   * <li> not_null
   * <li> primary_key
   * <li> multiple_key
   * <li> blob
   * <li> unsigned
   * <li> zerofill
   * <li> binary
   * <li> enum
   * <li> auto_increment
   * <li> timestamp
   * </ul>
   */
  public static Value mysql_field_flags(Env env,
                                        @NotNull MysqliResult result,
                                        int fieldOffset)
  {
    if (result == null)
      return BooleanValue.FALSE;

    Value fieldName = result.getFieldName(env, fieldOffset);

    if (fieldName == BooleanValue.FALSE)
      return BooleanValue.FALSE;

    Value fieldTable = result.getFieldTable(env, fieldOffset);
    Value fieldJdbcType = result.getJdbcType(fieldOffset);
    String fieldMysqlType = result.getMysqlType(fieldOffset);

    if ((fieldTable == BooleanValue.FALSE)
        || (fieldJdbcType == BooleanValue.FALSE)
        || (fieldMysqlType == null))
      return BooleanValue.FALSE;

    String sql = "SHOW FULL COLUMNS FROM "
        + fieldTable.toString() + " LIKE \'" + fieldName.toString() + "\'";

    Mysqli conn = getConnection(env);

    Value resultV = conn.validateConnection(env).realQuery(env, sql);

    Object metaResult = resultV.toJavaObject();

    if (metaResult instanceof MysqliResult)
      return ((MysqliResult) metaResult).getFieldFlagsImproved(
        env,
        fieldJdbcType.toInt(),
        fieldMysqlType);

    return BooleanValue.FALSE;
  }

  /**
   * Returns field name at given offset.
   */
  public static Value mysql_field_name(Env env,
                                       @NotNull MysqliResult result,
                                       int fieldOffset)
  {
    if (result == null)
      return BooleanValue.FALSE;

    // XXX : This method can't detect when mysql_field_name()
    // is invoked with just 2 arguments instead of 3. The
    // value 0 is passed for fieldOffsetValue when only
    // two arguments are found. It is not possible to change
    // to a Value argument since a NullValue is passed for
    // both the missing argument and NULL literal argument
    // cases. Also, Vlaue.isset() can't be used since it
    // returns false for the default NULL and the literal.

    return result.getFieldName(env, fieldOffset);
  }

  /**
   * Deprecated alias for mysql_field_name.
   */

  public static Value mysql_fieldname(Env env,
                                      @NotNull MysqliResult result,
                                      int fieldOffset)
  {
    return mysql_field_name(env, result, fieldOffset);
  }

  /**
   * Seeks to the specified field offset, the field offset is
   * is used as the default for the next call to {@link #mysql_fetch_field}.
   */
  public static boolean mysql_field_seek(Env env,
                                         @NotNull MysqliResult result,
                                         int fieldOffset)
  {
    if (result == null)
      return false;

    return result.field_seek(env, fieldOffset);
  }

  /**
   * Returns the table corresponding to the field.
   */
  public static Value mysql_field_table(Env env,
                                        @NotNull MysqliResult result,
                                        int fieldOffset)
  {
    if (result == null)
      return BooleanValue.FALSE;

    return result.getFieldTable(env, fieldOffset);
  }

  /**
   * Deprecated alias for mysql_field_table.
   */
  public static Value mysql_fieldtable(Env env,
                                       @NotNull MysqliResult result,
                                       int fieldOffset)
  {
    return mysql_field_table(env, result, fieldOffset);
  }

  /**
   * Returns the field type.
   */
  public static Value mysql_field_type(Env env,
                                       @NotNull MysqliResult result,
                                       Value fieldOffset)
  {
    if (result == null) {
      return NullValue.NULL;
    }

    if (! fieldOffset.isset())
      return NullValue.NULL;

    return result.getFieldType(env, fieldOffset.toInt());
  }

  /**
   * Deprecated alias for mysql_field_type.
   */
  public static Value mysql_fieldtype(Env env,
                                      @NotNull MysqliResult result,
                                      Value fieldOffset)
  {
    return mysql_field_type(env, result, fieldOffset);
  }

  /**
   * Returns the length of the specified field
   */
  public static Value mysql_field_len(Env env,
                                      @NotNull MysqliResult result,
                                      @Optional("0") int fieldOffset)
  {
    // gallery2 calls this function with 1 arg, so fieldOffset is optional

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

    // ERRATUM: Returns 10 for datatypes DEC and NUMERIC instead of 11

    return result.getFieldLength(env, fieldOffset);
  }

  /**
   * Frees a mysql result.
   */
  public static boolean mysql_free_result(@NotNull MysqliResult result)
  {
    if (result == null)
      return false;

    result.close();

    return true;
  }

  /**
   * Alias for mysql_free_result.
   */
  public static boolean mysql_freeresult(@NotNull MysqliResult result)
  {
    return mysql_free_result(result);
  }

  /**
   * Returns the MySQL client version.
   */
  public static String mysql_get_client_info(Env env)
  {
    return Mysqli.getClientInfoStatic(env);
  }

  /**
   * Returns a string describing the host.
   */
  public static StringValue mysql_get_host_info(Env env, @Optional Mysqli conn)
  {
    if (conn == null)
      conn = getConnection(env);

    return conn.get_host_info(env);
  }

  /**
   * Returns an integer respresenting the MySQL protocol
   * version.
   */
  public static int mysql_get_proto_info(Env env, @Optional Mysqli conn)
  {
    if (conn == null)
      conn = getConnection(env);

    return conn.get_proto_info();
  }

  /**
   * Returns the MySQL server version.
   */

  public static Value mysql_get_server_info(Env env, @Optional Mysqli conn)
  {
    if (conn == null) {
      conn = getConnection(env);
    }

    if (conn != null && conn.isConnected())
      return conn.get_server_info(env);
    else
      return NullValue.NULL;
  }

  /**
   * returns ID generated for an AUTO_INCREMENT column by the previous
   * INSERT query on success, 0 if the previous query does not generate
   * an AUTO_INCREMENT value, or FALSE if no MySQL connection was established
   */
  public static Value mysql_insert_id(Env env, @Optional Mysqli conn)
  {
    if (conn == null)
      conn = getConnection(env);

    return conn.insert_id(env);
  }

  /**
   * Returns a result pointer containing the
   * databases available from the current mysql daemon.
   */
  public static Value mysql_list_dbs(Env env,
                                     @Optional Mysqli conn)
  {
    if (conn == null)
      conn = getConnection(env);

    // php/1451
    // need to return results having column name 'Database' for typo3
    //
    // MySQL Connector/J 5.x returns 'SCHEME_NAME' as the column name
    // for "SHOW DATABASES", while 3.x returns 'Database'
    return mysql_query(env,
                       env.createString(
                           "SELECT SCHEMA_NAME AS 'Database' "
                               + "FROM information_schema.SCHEMATA"),
                       conn);
  }

  /**
   * Retrieves information about the given table name.
   * A Result on success, FALSE on failure.
   */
  public static Value mysql_list_fields(Env env,
                                        String database,
                                        StringValue tableName,
                                        @Optional Mysqli conn)
  {
    // php/141c
    // php gives warnings when the table doesn't exist or is an
    // empty string/null, but not when the database doesn't exist

    if (database == null || database.length() == 0)
      return BooleanValue.FALSE;

    if (tableName.length() == 0) {
      env.warning(L.l("Tablename cannot be empty"));

      return BooleanValue.FALSE;
    }

    if (conn == null)
      conn = getConnection(env);

    if (! conn.select_db(env, database))
      return BooleanValue.FALSE;

    Value result = conn.query(env,
                              env.createString(
                                  "SELECT * FROM " + tableName + " WHERE NULL"),
                              1);

    if (result == BooleanValue.FALSE)
      env.warning(L.l("Table '{0}' does not exist", tableName));

    return result;
  }

  /**
   * Deprecated alias for mysql_list_fields
   */

  public static Value mysql_listfields(Env env,
                                       String databaseName,
                                       StringValue tableName,
                                       @Optional Mysqli conn)
  {
    return mysql_list_fields(env, databaseName, tableName, conn);
  }

  /**
   * Returns result set or false on error
   */
  public static Value mysql_db_query(Env env,
                                     String databaseName,
                                     StringValue query,
                                     @Optional Mysqli conn)
  {
    if (conn == null)
      conn = getConnection(env);

    if (! conn.select_db(env, databaseName))
      return BooleanValue.FALSE;

    return conn.query(env, query, 1);
  }

  /**
   * Selects the database
   */
  public static boolean mysql_select_db(Env env,
                                        String dbName,
                                        @Optional Mysqli conn)
  {
    if (dbName == null || dbName.length() == 0)
      return false;

    if (conn == null)
      conn = getConnection(env, dbName);

    return conn.select_db(env, dbName);
  }

  /**
   * Retrieves a list of table names from a MySQL database.
   */
  public static Object mysql_list_tables(Env env,
                                         StringValue databaseName,
                                         @Optional Mysqli conn)
  {
    return mysql_query(env,
                       env.createString("SHOW TABLES FROM " + databaseName),
                       conn);
  }

  /**
   * Get number of fields in result
   */

  public static Value mysql_num_fields(Env env, @NotNull MysqliResult result)
  {
    if (result == null)
      return BooleanValue.FALSE;

    return LongValue.create(result.num_fields());
  }

  /**
   * Deprecated alias for mysql_num_fields
   */
  public static Value mysql_numfields(Env env, @NotNull MysqliResult result)
  {
    return mysql_num_fields(env, result);
  }

  /**
   * Retrieves the number of rows in a result set.
   */
  public static Value mysql_num_rows(Env env, @NotNull MysqliResult result)
  {
    if (result == null)
      return BooleanValue.FALSE;

    return LongValue.create(result.num_rows());
  }

  /**
   * Undocumented alias for {#link #mysql_num_rows}.
   */
  public static Value mysql_numrows(Env env, @NotNull MysqliResult result)
  {
    return mysql_num_rows(env, result);
  }

  /**
   * Returns a new persistent mysql connection.  JDBC has its own pooling
   * so don't need to do anything different from regular mysql_connect().
   */
  public static Value mysql_pconnect(Env env,
                                     @Optional StringValue server,
                                     @Optional StringValue user,
                                     @Optional StringValue password,
                                     @Optional boolean newLink,
                                     @Optional int flags)
  {
    Value value = mysql_connect(env, server, user, password, newLink, flags);

    Mysqli conn = (Mysqli) env.getSpecialValue("caucho.mysql");

    if (conn != null && conn.isConnected())
      conn.setPersistent();

    return value;
  }

  /**
   * Returns a new mysql connection.
   */
  public static Value mysql_connect(Env env,
                                    @Optional StringValue host,
                                    @Optional StringValue userName,
                                    @Optional StringValue password,
                                    @Optional boolean isNewLink,
                                    @Optional int flags) {
    int port = -1;
    String socketStr = "";

    String hostStr;
    String url = null;

    if (host.length() == 0) {
      hostStr = env.getIniString("mysql.default_host");
    }
    else {
      hostStr = host.toString();
    }

    if (hostStr == null || hostStr.length() == 0) {
      hostStr = "localhost";
    }
    else if (hostStr.startsWith("jdbc:")) {
    }
    else {
      // host string could contain just a host name,
      // or it could contain a host name
      // and a port number, or it could contain
      // a host name and local socket name, or all 3.
      //
      // "localhost"
      // "localhost:3306"
      // ":3306"
      // ":/tmp/mysql.sock"
      // "localhost:/tmp/mysql.sock"
      // "localhost:3306:/tmp/mysql.sock"

      int sepIndex = hostStr.indexOf(':');

      if (sepIndex > -1) {
        String tmp;
        String portStr;

        tmp = hostStr;
        hostStr = tmp.substring(0, sepIndex);
        if (hostStr.length() == 0) {
          hostStr = "localhost";
        }
        sepIndex++;
        tmp = tmp.substring(sepIndex);

        if ((tmp.length() > 0) && (tmp.charAt(0) != '/')) {
          sepIndex = tmp.indexOf(':');

          if (sepIndex > -1) {
            portStr = tmp.substring(0, sepIndex);
            sepIndex++;
            socketStr = tmp.substring(sepIndex);
          } else {
            portStr = tmp;
          }

          try {
            port = Integer.parseInt(portStr);
          } catch (NumberFormatException ex) {
            // Use default port in case of invalid port number
          }
        } else {
          socketStr = tmp;
        }
      }

      /*
      String catalog = (String) env.getQuercus().getSpecial("mysql.catalog");

      if (catalog == null)
        catalog = "";
      */
    }

    Mysqli mysqli = new MysqliResource(env, hostStr, userName.toString(),
                                       password.toString(), "",
                                       port, socketStr, flags,
                                       null, url, isNewLink);

    if (! mysqli.isConnected())
      return BooleanValue.FALSE;

    Value value = env.wrapJava(mysqli);

    env.setSpecialValue("caucho.mysql", mysqli);

    return value;
  }

  /**
   * Checks if the connection is still valid.
   */
  public static boolean mysql_ping(Env env, @Optional Mysqli conn)
  {
    if (conn == null)
      conn = getConnection(env);

    return conn.ping(env);
  }

  /**
   * Returns a string with the status of the connection
   * or NULL if error.
   */
  public static Value mysql_stat(Env env, @Optional Mysqli conn)
  {
    if (conn == null)
      conn = getConnection(env);

    Value result = conn.stat(env);

    return result == BooleanValue.FALSE ? NullValue.NULL : result;
  }

  /**
   * Retrieves the table name corresponding to a field, using
   * a result return by {@link #mysql_list_tables}.
   */
  public static Value mysql_tablename(Env env,
                               @NotNull MysqliResult result,
                               int i)
  {
    if (result == null)
      return BooleanValue.FALSE;

    return result.getResultField(env, i, LongValue.ZERO);
  }

  /**
   * Queries the database.
   */
  public static Object mysql_unbuffered_query(Env env,
                                       @NotNull StringValue name,
                                       @Optional Mysqli conn)
  {
    // An "unbuffered" query is a performance optimization
    // for large data sets. Mysql will lock the table in
    // question until all rows are read by the client.
    // It is unclear how this would be implemented on top
    // of Connector/J.

    return mysql_query(env, name, conn);
  }

  /**
   * Query an identifier that corresponds to this specific
   * connection. Mysql calls this integer identifier a
   * thread, but it is really a connection identifier.
   */
  public static Value mysql_thread_id(Env env,
                                      @Optional Mysqli conn)
  {
    if (conn == null)
      conn = getConnection(env);

    return conn.thread_id(env);
  }

  //@todo mysql_list_processes()

  public static Value mysql_get_charset(Env env, @Optional Mysqli conn)
  {
    if (conn == null) {
      conn = getConnection(env);
    }

    return conn.get_charset(env);
  }

  public static boolean mysql_set_charset(Env env,
                                          StringValue charset,
                                          @Optional Mysqli conn)
  {
    if (conn == null) {
      conn = getConnection(env);
    }

    return conn.set_charset(env, charset);
  }

  private static Mysqli getConnection(Env env)
  {
    return getConnection(env, "");
  }

  private static Mysqli getConnection(Env env, String db)
  {
    Mysqli conn = (Mysqli) env.getSpecialValue("caucho.mysql");

    // php/1436
    if (conn != null)
      return conn;

    conn = new MysqliResource(env,
                              env.getEmptyString(),
                              env.getEmptyString(), env.getEmptyString(),
                              db, 3306,
                              env.getEmptyString());

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

    return conn;
  }
}

