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

import com.caucho.quercus.QuercusModuleException;
import com.caucho.quercus.annotation.Optional;
import com.caucho.quercus.annotation.PassThru;
import com.caucho.quercus.annotation.ReadOnly;
import com.caucho.quercus.annotation.Reference;
import com.caucho.quercus.annotation.ReturnNullAsFalse;
import com.caucho.quercus.annotation.UsesSymbolTable;
import com.caucho.quercus.env.ArrayValue;
import com.caucho.quercus.env.ArrayValueImpl;
import com.caucho.quercus.env.BooleanValue;
import com.caucho.quercus.env.DoubleValue;
import com.caucho.quercus.env.Env;
import com.caucho.quercus.env.EnvVar;
import com.caucho.quercus.env.LongValue;
import com.caucho.quercus.env.NullValue;
import com.caucho.quercus.env.QuercusClass;
import com.caucho.quercus.env.SerializeMap;
import com.caucho.quercus.env.StringValue;
import com.caucho.quercus.env.UnserializeCacheEntry;
import com.caucho.quercus.env.Value;
import com.caucho.quercus.env.Var;
import com.caucho.quercus.module.AbstractQuercusModule;
import com.caucho.util.L10N;
import com.caucho.util.LruCache;
import com.caucho.vfs.StderrStream;
import com.caucho.vfs.StringWriter;
import com.caucho.vfs.WriteStream;

import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.logging.Logger;

/**
 * Information about PHP variables.
 */
public class VariableModule extends AbstractQuercusModule {
  private static final Logger log
    = Logger.getLogger(VariableModule.class.getName());
  private static final L10N L = new L10N(VariableModule.class);

  private static final
    LruCache<UnserializeKey,UnserializeCacheEntry> _unserializeCache
    = new LruCache<UnserializeKey,UnserializeCacheEntry>(256);

  /**
   * Returns a constant
   *
   * @param env the quercus calling environment
   * @param name the constant name
   */
  public static Value constant(Env env, String name)
  {
    if (name == null) {
      env.warning(L.l("null passed as constant name"));
      return NullValue.NULL;
    }

    int i = name.indexOf("::");

    if (i > 0) {
      String cls = name.substring(0, i);

      name = name.substring(i + 2);

      return env.getClass(cls).getConstant(env, env.createString(name));
    }
    else {
      Value constant = env.getConstant(name, false);

      if (constant != null)
        return constant;
      else {
        env.warning(L.l("cannot find constant '{0}'", name));
        return NullValue.NULL;
      }
    }
  }

  /**
   * Prints a debug version of the variable
   *
   * @param env the quercus calling environment
   * @param v the variable to print
   * @return the escaped stringPhp
   */
  public static Value debug_zval_dump(Env env, @ReadOnly Value v)
  {
    try {
      debug_impl(env, v, 0);

      return NullValue.NULL;
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  /**
   * Defines a constant
   *
   * @param env the quercus calling environment
   * @param name the constant name
   * @param value the constant value
   */
  public static Value define(Env env,
                             StringValue name,
                             Value value,
                             @Optional boolean isCaseInsensitive)
  {
    return env.addConstant(name, value, isCaseInsensitive);
  }

  /**
   * Returns true if the constant is defined.
   *
   * @param env the quercus calling environment
   * @param name the constant name
   */
  public static boolean defined(Env env, String name)
  {
    if (name == null)
      return false;

    int i = name.indexOf("::");

    if (i > 0) {
      String clsName = name.substring(0, i);
      name = name.substring(i + 2);

      QuercusClass cls = env.getClass(clsName);

      return cls.hasConstant(env.createString(name));
    }
    else
      return env.isDefined(name);
  }

  /**
   * Converts to a double
   *
   * @param v the variable to convert
   * @return the double value
   */
  public static Value doubleval(@ReadOnly Value v)
  {
    return floatval(v);
  }

  /**
   * Returns true for an empty variable.
   *
   * @param v the value to test
   *
   * @return true if the value is empty
   */
  public static boolean empty(@ReadOnly Value v)
  {
    return v.isEmpty();
  }

  /**
   * Converts to a double
   *
   * @param v the variable to convert
   * @return the double value
   */
  public static Value floatval(@ReadOnly Value v)
  {
    return new DoubleValue(v.toDouble());
  }

  /**
   * Returns the defined variables in the current scope.
   */
  @UsesSymbolTable
  public static Value get_defined_vars(Env env)
  {
    ArrayValue result = new ArrayValueImpl();

    Map<StringValue,EnvVar> map = env.getEnv();

    for (Map.Entry<StringValue,EnvVar> entry : map.entrySet()) {
      result.append(entry.getKey(),
                    entry.getValue().get());
    }

    Map<StringValue,EnvVar> globalMap = env.getGlobalEnv();
    if (map != globalMap) {
      for (Map.Entry<StringValue,EnvVar> entry : globalMap.entrySet()) {
        result.append(entry.getKey(),
                      entry.getValue().get());
      }
    }

    return result;
  }

  /*
   * Returns the type of this resource.
   */
  @ReturnNullAsFalse
  public static String get_resource_type(Env env, Value v)
  {
    return v.getResourceType();
  }

  /**
   * Returns the type string for the variable
   */
  public static String gettype(@ReadOnly Value v)
  {
    return v.getType();
  }

  /**
   * Imports request variables
   *
   * @param types the variables to import
   * @param prefix the prefix
   */
  public static boolean import_request_variables(Env env,
                                                 String types,
                                                 @Optional String prefix)
  {
    if ("".equals(prefix))
      env.notice(L.l("import_request_variables should use a prefix argument"));

    for (int i = 0; i < types.length(); i++) {
      char ch = types.charAt(i);

      Value value = null;

      if (ch == 'c' || ch == 'C')
        value = env.getGlobalValue("_COOKIE");
      else if (ch == 'g' || ch == 'G')
        value = env.getGlobalValue("_GET");
      else if (ch == 'p' || ch == 'P')
        value = env.getGlobalValue("_POST");

      if (! (value instanceof ArrayValue))
        continue;

      ArrayValue array = (ArrayValue) value;

      for (Map.Entry<Value,Value> entry : array.entrySet()) {
        String key = entry.getKey().toString();

        env.setGlobalValue(prefix + key,
                         array.getVar(entry.getKey()));
      }
    }

    return true;
  }

  /**
   * Converts to a long
   *
   * @param v the variable to convert
   * @return the double value
   */
  public static Value intval(@ReadOnly Value v)
  {
    return v.toLongValue();
  }

  /**
   * Converts to a long
   *
   * @param v the variable to convert
   * @return the double value
   */
  public static long intval(@ReadOnly Value v, int base)
  {
    if (! v.isString())
      return v.toLong();

    StringValue s = v.toStringValue();

    int len = s.length();
    long value = 0;

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

      int digit;

      if ('0' <= ch && ch <= '9')
        digit = ch - '0';
      else if ('a' <= ch && ch <= 'z')
        digit = ch - 'a' + 10;
      else if ('A' <= ch && ch <= 'Z')
        digit = ch - 'A' + 10;
      else
        digit = 0;

      value = value * base + digit;
    }

    return value;
  }

  /**
   * Returns true for an array.
   *
   * @param v the value to test
   *
   * @return true for an array
   */
  public static boolean is_array(@ReadOnly Value v)
  {
    return v.isArray();
  }

  // XXX: is_binary

  /**
   * Returns true for a boolean
   *
   * @param v the value to test
   *
   * @return true for a boolean
   */
  public static Value is_bool(@ReadOnly Value v)
  {
    return (v.toValue() instanceof BooleanValue
            ? BooleanValue.TRUE
            : BooleanValue.FALSE);
  }

  // XXX: is_buffer

  /**
   * Returns the type string for the variable
   */
  public static boolean is_callable(Env env,
                                    @ReadOnly Value v,
                                    @Optional boolean isCheckSyntaxOnly,
                                    @Optional @Reference Value nameRef)
  {
    if (nameRef.isDefault()) {
      nameRef = null;
    }

    return v.isCallable(env, isCheckSyntaxOnly, nameRef);
  }

  /**
   * Returns true for a double
   *
   * @param v the value to test
   *
   * @return true for a double
   */
  public static boolean is_double(@ReadOnly Value v)
  {
    return is_float(v);
  }

  /**
   * Returns true for a double
   *
   * @param v the value to test
   *
   * @return true for a double
   */
  public static boolean is_float(@ReadOnly Value v)
  {
    return (v.toValue() instanceof DoubleValue);
  }

  /**
   * Returns true for an integer
   *
   * @param v the value to test
   *
   * @return true for a double
   */
  public static Value is_int(@ReadOnly Value v)
  {
    return (v.toValue() instanceof LongValue
            ? BooleanValue.TRUE
            : BooleanValue.FALSE);
  }

  /**
   * Returns true for an integer
   *
   * @param v the value to test
   *
   * @return true for a double
   */
  public static Value is_integer(@ReadOnly Value v)
  {
    return is_int(v);
  }

  /**
   * Returns true for an integer
   *
   * @param v the value to test
   *
   * @return true for a double
   */
  public static Value is_long(@ReadOnly Value v)
  {
    return is_int(v);
  }

  /**
   * Returns true for null
   *
   * @param v the value to test
   *
   * @return true for null
   */
  public static boolean is_null(@ReadOnly Value v)
  {
    return v.isNull();
  }

  /**
   * Returns true for numeric
   *
   * @param env the calling environment
   * @param v the value to test
   *
   * @return true for numeric
   */
  public static boolean is_numeric(Env env, @ReadOnly Value v)
  {
    return v.isNumeric();
  }

  /**
   * Returns true for an object
   *
   * @param env the calling environment
   * @param v the value to test
   *
   * @return true for object
   */
  public static boolean is_object(Env env, @ReadOnly Value v)
  {
    return v.isObject();
  }

  /**
   * Returns true for a real
   *
   * @param v the value to test
   *
   * @return true for a real
   */
  public static boolean is_real(@ReadOnly Value v)
  {
    return is_float(v);
  }

  /**
   * Returns true if the value is a resource
   */
  public boolean is_resource(@ReadOnly Value value)
  {
    return value.isResource();
  }

  /**
   * Returns true for a scalar
   *
   * @param v the value to test
   *
   * @return true for a scalar
   */
  public static boolean is_scalar(@ReadOnly Value v)
  {
    if (v == null) {
      return false;
    }

    Value value = v.toValue();

    return (value instanceof DoubleValue
            || value instanceof StringValue
            || value instanceof LongValue
            || value instanceof BooleanValue);
  }

  /**
   * Returns true if the value is a string
   */
  public boolean is_string(@ReadOnly Value value)
  {
    return value.isString();
  }

  // XXX: is_unicode

  /**
   * Returns the type string for the variable
   */
  public static boolean isset(@ReadOnly Value ... values)
  {
    for (Value v : values) {
      if (! v.isset()) {
        return false;
      }
    }

    return true;
  }

  /**
   * Prints a value.  If isReturn is true, then returns what was supposed
   * to be printed as a string instead.
   *
   * @param env the quercus calling environment
   * @param v the variable to print
   * @param isReturn set to true if returning instead of printing value
   * @return the string that was supposed to be printed, or true
   */
  public static Value print_r(Env env,
                              @ReadOnly Value v,
                              @Optional boolean isReturn)
  {
    try {
      WriteStream out;

      if (isReturn) {
        StringWriter writer = new StringWriter();
        out = writer.openWrite();

        out.setNewlineString("\n");

        v.printR(env, out, 0, new IdentityHashMap<Value, String>());

        return env.createString(writer.getString());
      }
      else {
        out = env.getOut();

        v.printR(env, out, 0, new IdentityHashMap<Value, String>());

        return BooleanValue.TRUE;
      }
    }
    catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  private static void printDepth(WriteStream out, int depth)
    throws IOException
  {
    for (int i = 0; i < depth; i++) {
      out.print(' ');
    }
  }

  /**
   * Serializes the value to a string.
   */
  public static String serialize(Env env,
                                 @PassThru @ReadOnly Value v)
  {
    StringBuilder sb = new StringBuilder();

    v.serialize(env, sb, new SerializeMap());

    return sb.toString();
  }

  /**
   * Converts the variable to a specified tyep.
   */
  public static boolean settype(Env env,
                                @Reference Value var,
                                String type)
  {
    Value value = var.toValue();

    if ("null".equals(type)) {
      var.set(NullValue.NULL);
      return true;
    }
    else if ("boolean".equals(type) || "bool".equals(type)) {
      var.set(value.toBoolean() ? BooleanValue.TRUE : BooleanValue.FALSE);
      return true;
    }
    else if ("string".equals(type)) {
      var.set(value.toStringValue());
      return true;
    }
    else if ("int".equals(type) || "integer".equals(type)) {
      var.set(LongValue.create(value.toLong()));
      return true;
    }
    else if ("float".equals(type) || "double".equals(type)) {
      var.set(new DoubleValue(value.toDouble()));
      return true;
    }
    else if ("object".equals(type)) {
      var.set(value.toObject(env));
      return true;
    }
    else if ("array".equals(type)) {
      if (value.isArray())
        var.set(value);
      else {
        ArrayValueImpl array = new ArrayValueImpl();
        var.set(array);

        if (! value.isNull()) {
          array.append(value);
        }
      }

      return true;
    }
    else {
      return false;
    }
  }

  /**
   * Converts to a string
   *
   * @param env the quercus calling environment
   * @param v the variable to convert
   * @return the double value
   */
  public static Value strval(Env env, @ReadOnly Value v)
  {
    return v.toString(env);
  }

  /**
   * Unserializes the value from a string.
   */
  public static Value unserialize(Env env, StringValue s)
  {
    if (s.length() == 0) {
      return BooleanValue.FALSE;
    }

    Value v = null;

    UnserializeKey key = new UnserializeKey(s);

    UnserializeCacheEntry entry = _unserializeCache.get(key);

    if (entry != null) {
      v = entry.getValue(env);

      if (v != null) {
        return v;
      }
    }

    UnserializeReader is = null;

    try {
      is = new UnserializeReader(s);

      v = is.unserialize(env);
    }
    catch (IOException e) {
      System.err.println("VariableModule.unserialize0: " + s);

      env.notice(e);

      v = BooleanValue.FALSE;
    }

    if (is != null && ! is.useReference()) {
      entry = new UnserializeCacheEntry(v);

      _unserializeCache.put(key, entry);

      return entry.getValue(env);
    }

    return v;
  }

  // XXX: unset

  /**
   * Prints a debug version of the variable
   *
   * @param env the quercus calling environment
   * @param v the variable to print
   * @return the escaped stringPhp
   */
  public static Value var_dump(Env env,
                               @PassThru @ReadOnly Value v,
                               Value []args)
  {
    try {
      if (v == null)
        env.getOut().print("NULL#java");
      else {
        v.varDump(env, env.getOut(), 0,  new IdentityHashMap<Value,String>());

        env.getOut().println();
      }

      if (args != null) {
        for (Value value : args) {
          if (value == null)
            env.getOut().print("NULL#java");
          else {
            value.varDump(env, env.getOut(), 0,
                          new IdentityHashMap<Value,String>());

            env.getOut().println();
          }
        }
      }

      return NullValue.NULL;
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  public static Value stderr_var_dump(Env env,
                                      @PassThru @ReadOnly Value v,
                                      Value []args)
  {
    WriteStream out = new WriteStream(StderrStream.create());

    try {
      if (v == null)
        out.print("NULL#java");
      else {
        v.varDump(env, out, 0,  new IdentityHashMap<Value,String>());

        out.println();
      }

      if (args != null) {
        for (Value value : args) {
          if (value == null)
            out.print("NULL#java");
          else {
            value.varDump(env, out, 0,
                          new IdentityHashMap<Value,String>());

            out.println();
          }
        }
      }

      out.flush();

      return NullValue.NULL;
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  /**
   * Serializes the value to a string.
   */
  public static Value var_export(Env env,
                                 @ReadOnly Value v,
                                 @Optional boolean isReturn)
  {
    StringValue s = v.varExport(env);

    if (isReturn) {
      return s;
    }
    else {
      env.print(s);

      return NullValue.NULL;
    }
  }

  private static void debug_impl(Env env, Value v, int depth)
    throws IOException
  {
    WriteStream out = env.getOut();

    if (v instanceof Var)
      out.print("&");

    v = v.toValue();

    if (v instanceof ArrayValue) {
      ArrayValue array = (ArrayValue) v;

      out.println("Array");
      printDepth(out, 2 * depth);
      out.println("(");

      for (Map.Entry<Value,Value> entry : array.entrySet()) {
        printDepth(out, 2 * depth);
        out.print("    [");
        out.print(entry.getKey());
        out.print("] => ");
        debug_impl(env, entry.getValue(), depth + 1); // XXX: recursion
      }
      printDepth(out, 2 * depth);
      out.println(")");
    }
    else if (v instanceof BooleanValue) {
      if (v.toBoolean())
        out.print("bool(true)");
      else
        out.print("bool(false)");
    }
    else if (v instanceof LongValue) {
      out.print("int(" + v.toLong() + ")");
    }
    else if (v instanceof DoubleValue) {
      out.print("float(" + v.toDouble() + ")");
    }
    else if (v instanceof StringValue) {
      out.print("string(" + v.toString() + ")");
    }
    else if (v instanceof NullValue) {
      out.print("NULL");
    }
    else {
      v.print(env);
    }
  }

  static class UnserializeKey {
    private final SoftReference<StringValue> _stringRef;
    private int _hash;

    UnserializeKey(StringValue string)
    {
      _hash = string.hashCode();

      _stringRef = new SoftReference<StringValue>(string);
    }

    public int hashCode()
    {
      return _hash;
    }

    public boolean equals(Object o)
    {
      if (this == o)
        return true;
      else if (! (o instanceof UnserializeKey))
        return false;

      UnserializeKey key = (UnserializeKey) o;

      StringValue a = _stringRef.get();
      StringValue b = key._stringRef.get();

      if (a == null || b == null)
        return false;

      return a.equals(b);
    }
  }
}

