/*
 * 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 SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.es;

/**
 * Implementation class representing a JavaScript function.
 */
public class ESClosure extends ESObject {
  static ESString LENGTH = ESId.intern("length");
  static ESString CALLEE = ESId.intern("callee");
  static ESString ARGUMENTS = ESId.intern("arguments");
  static ESString OBJECT = ESId.intern("Object");
  static ESString PROTOTYPE = ESId.intern("prototype");
  static ESString CONSTRUCTOR = ESId.intern("constructor");

  public ESString name;
  private ESCallable esClass;
  public int n;

  ESId []formals;
  int nFormals;

  ESBase proto;

  ESGlobal global;
  int stackRequired; // how much argument stack space this function requires
  int scopeRequired; // how much scope space
  ESBase []scope;
  int scopeLength;

  boolean hasFields;

  public ESClosure(ESString name, ESCallable esClass, ESObject proto,
                   int n, ESId []formals, ESObject global)
  {
    this.className = "Function";
    this.prototype = Global.getGlobalProto().funProto;
    this.name = name;
    this.esClass = esClass;
    this.proto = proto;
    this.n = n;
    this.formals = formals;
    this.nFormals = formals.length;

    if (global instanceof ESGlobal)
      this.global = (ESGlobal) global;

    if (global != null) {
      this.scopeLength = 1;
      this.scope = new ESBase[1];
      this.scope[0] = global;
    }
  }

  protected ESClosure(ESBase []scope, int scopeLength)
  {
    super("Function", null);

    hasFields = true;
    this.scope = scope;
    this.scopeLength = scopeLength;
  }
  /**
   * Create a new object based on a prototype
   */
  protected ESClosure()  {}

  public void closure(Call env)
  {
    if (scope == null) {
      scope = new ESBase[16];
    }

    for (; scopeLength < env.scopeLength; scopeLength++)
      scope[scopeLength] = env.scope[scopeLength];

    if (scopeLength == 0)
      scope[scopeLength++] = env.global;

    global = (ESGlobal) scope[0];
  }

  /**
   * Create a new object based on a prototype
   */
  void setScope(ESBase []scope, int scopeLength)
  {
    this.scope = scope;
    this.scopeLength = scopeLength;
  }

  public ESBase hasProperty(ESString id)
    throws Throwable
  {
    if (id.equals(PROTOTYPE)) {
      if (proto == null) {
        ESObject obj = Global.getGlobalProto().createObject();
        proto = obj;
        obj.put(CONSTRUCTOR, this, DONT_ENUM);
      }

      return proto;
    } else if (id.equals(LENGTH)) {
      return ESNumber.create(nFormals);
    } else if (hasFields) {
      return super.hasProperty(id);
    } else if (prototype != null)
      return prototype.hasProperty(id);
    else
      return ESBase.esEmpty;
  }

  public ESBase getProperty(ESString id)
    throws Throwable
  {
    if (id.equals(PROTOTYPE)) {
      if (proto == null) {
        ESObject obj = Global.getGlobalProto().createObject();
        proto = obj;
        obj.put(CONSTRUCTOR, this, DONT_ENUM);
      }

      return proto;
    } else if (id.equals(LENGTH)) {
      return ESNumber.create(nFormals);
    } else if (hasFields) {
      return super.getProperty(id);
    } else
      return prototype.getProperty(id);
  }

  public boolean canPut(ESString id)
  {
    if (id.equals(PROTOTYPE)) {
      return true;
    } else if (id.equals(LENGTH)) {
      return false;
    } else if (hasFields) {
      return super.canPut(id);
    } else {
      return true;
    }
  }

  public void setProperty(ESString id, ESBase value)
    throws Throwable
  {
    if (id.equals(PROTOTYPE)) {
      proto = value;
    } else if (id.equals(LENGTH)) {
    } else if (hasFields) {
      super.setProperty(id, value);
    } else {
      init(className, prototype);
      hasFields = true;
      super.setProperty(id, value);
    }
  }

  public void put(ESString id, ESBase value, int flags)
  {
    if (id.equals(PROTOTYPE)) {
      proto = value;
    } else if (id.equals(LENGTH)) {
    } else if (hasFields) {
      super.put(id, value, flags);
    } else {
      init(className, prototype);
      hasFields = true;
      super.put(id, value, flags);
    }
  }

  public ESBase delete(ESString id)
    throws Throwable
  {
    if (id.equals(PROTOTYPE)) {
      proto = ESBase.esEmpty;
      return ESBoolean.TRUE;
    } else if (id.equals(LENGTH)) {
      return ESBoolean.FALSE;
    } else if (hasFields) {
      return super.delete(id);
    } else
      return ESBoolean.TRUE;
  }

  public ESString toStr()
  {
    return ESString.create(decompile());
  }

  String decompile()
  {
    StringBuffer sbuf = new StringBuffer();

    sbuf.append("function ");
    if (name != null && ! name.toString().startsWith("$lambda"))
        sbuf.append(name);
    sbuf.append("(");
    for (int i = 0; formals != null && i < nFormals; i++) {
      if (i != 0)
        sbuf.append(", ");
      sbuf.append(formals[i]);
    }

    sbuf.append(") ");

    sbuf.append("{ ");
    sbuf.append("[compiled code]");
    sbuf.append(" }");

    return sbuf.toString();
  }

  protected ESBase dispatch() throws ESException
  {
    throw new ESException("dispatch not specialized");
  }

  public ESBase call(Call call, int length) throws Throwable
  {
    if (global != null)
      call.global = global;

    if (esClass != null)
      return esClass.call(n, call, length);
    else
      return ESBase.esUndefined;
  }

  public ESBase construct(Call eval, int length) throws Throwable
  {
    Global resin = Global.getGlobalProto();
    ESObject obj = Global.getGlobalProto().createObject();
    ESBase proto = this.proto;

    if (! (proto instanceof ESObject))
      proto = resin.object.getProperty(PROTOTYPE);

    if (proto instanceof ESObject)
      obj.prototype = proto;

    eval.setThis(obj);

    ESBase value = call(eval, length);

    return value instanceof ESObject ? (ESObject) value : obj;
  }

  public ESBase typeof() throws ESException
  {
    return ESString.create("function");
  }

  protected void copy(Object newObj)
  {
    ESClosure closure = (ESClosure) newObj;

    super.copy(newObj);

    closure.name = name;
    closure.esClass = esClass;
    closure.n = n;
    closure.formals = formals;
    closure.nFormals = nFormals;

    closure.stackRequired = stackRequired;
    closure.scopeRequired = scopeRequired;
    closure.scopeLength = scopeLength;
    closure.scope = new ESBase[scopeLength];
    closure.hasFields = hasFields;

    for (int i = 0; i < scopeLength; i++) {
      if (scope[i] != null) {
        closure.scope[i] = (ESBase) scope[i];
      }
    }

    closure.proto = proto; // XXX: should be copied?
  }

  public ESObject dup() { return new ESClosure(); }

  ESObject resinCopy()
  {
    ESObject obj = dup();

    copy(obj);

    return obj;
  }

  ESObject getClassPrototype()
    throws Throwable
  {
    ESBase proto = hasProperty(PROTOTYPE);

    return (ESObject) proto;
  }

  void setClassPrototype(ESObject proto)
    throws ESException
  {
    this.proto = proto;
    proto.put(CONSTRUCTOR, this, DONT_ENUM);
  }
}
