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

package com.caucho.es;

import java.util.Enumeration;
import java.util.Iterator;

/**
 * Implementation class representing a call context.
 */
public final class Call extends ESBase {
  static ESId ARGUMENTS = ESId.intern("arguments");
  static ESId ARRAY = ESId.intern("Array");

  Call caller;
  ESObject callThis;
  int callLength;
  ESBase callee;

  public ESBase []stack;
  int top;

  public ESGlobal global;
  ESBase []scope;
  int scopeLength;

  public ESBase []values;

  ESObject aux;

  Call child; // just a cache

  /**
   * Create a new context object
   */
  Call()
  {
    prototype = esBase;

    stack = new ESBase[64];
    scope = new ESBase[16];
    values = new ESBase[64];
    top = 0;
  }

  void clear()
  {
    aux = null;
    child = null;
    top = 0;
  }

  public Call getCall()
  {
    Call child = this.child;

    if (child == null)
      child = this.child = new Call();

    child.caller = this;

    child.global = global;

    return child;
  }

  Global getGlobalProto() { return Global.getGlobalProto(); }

  public ESBase wrap(Object o) throws Throwable { return Global.wrap(o); }
  
  public ESBase wrap(long n) throws Throwable
  {
    return ESNumber.create(n);
  }
  
  public ESBase wrapClass(Class cl) throws Throwable
  {
    return Global.getGlobalProto().classWrap(cl);
  }

  // public ESGlobal getGlobal() { return Global.getGlobal().global; }

  final ESBase getArg(int i)
  {
    return stack[top + i];
  }

  public final ESBase getArg(int i, int len) 
  { 
    return i < len ? stack[top + i] : esUndefined; 
  }

  public final int getArgInt32(int i, int len) throws Throwable
  {
    return i < len ? stack[top + i].toInt32() : 0;
  }

  public final double getArgNum(int i, int len) throws Throwable
  {
    return i < len ? stack[top + i].toNum() : (0.0/0.0);
  }

  public final String getArgString(int i, int len) throws Throwable
  {
    return i < len ? stack[top + i].toJavaString() : null;
  }

  public final Object getArgObject(int i, int len) throws Throwable
  {
    return i < len ? stack[top + i].toJavaObject() : null;
  }

  public ESObject createObject()
  {
    return new ESObject("Object", getGlobalProto().objProto);
  }

  public ESBase createDate(long time)
  {
    return new ESDate(time, getGlobalProto().dateProto);
  }

  public String printf(int length)
    throws Throwable
  {
    return Printf.sprintf(this, length);
  }

  void setArg(int i, ESBase obj)
  {
    stack[top + i] = obj;
  }

  public final ESObject getThis() throws Throwable
  {
    return stack[top - 1].toObject();
  }
  
  public final Object getThisWrapper() throws Throwable
  { 
    return stack[top - 1].toJavaObject();
  }
  
  void setThis(ESBase obj)
  {
    stack[top - 1] = obj;
  }

  public ESGlobal getGlobal()
  {
    return global;
  }

  public final ESObject getCallThis() throws Throwable
  { 
    ESBase obj = caller.stack[caller.top - 1];

    return obj.toObject();
  }

  ESBase getContext()
  {
    return scope[scopeLength - 1];
  }

  ESBase getFunctionContext()
  {
    if (caller == null || caller.scopeLength == 0)
      return global;
    else
      return caller.scope[caller.scopeLength - 1];
  }

  public void pushScope(ESBase value)
  {
    scope[scopeLength++] = value;
  }

  public void popScope()
  {
    scopeLength--;
  }

  public ESObject getEval()
  {
    return (ESObject) scope[scopeLength - 1];
  }
  
  public ESObject createArg(ESId []args, int length)
    throws Throwable
  {
    ESObject arg = ESArguments.create(args, this, length);

    scope[scopeLength++] = arg;

    return arg;
  }

  public void setProperty(ESString name, ESBase value) throws Throwable
  {
    //setProperty(name, value);
  }

  public ESBase delete(ESString key) throws Throwable
  {
    return aux == null ? ESBoolean.FALSE : aux.delete(key); 
  } 

  public ESBase findScopeProperty(ESString id) throws Throwable
  {
    for (int i = scopeLength - 1; i > 0; i--) {
      if (scope[i].getProperty(id) != esEmpty)
        return scope[i];
    }

    return global;
  }

  public ESBase scopeTypeof(ESString id) throws Throwable
  {
    for (int i = scopeLength - 1; i >= 0; i--) {
      ESBase value;
      if ((value = scope[i].getProperty(id)) != esEmpty)
        return value.typeof();
    }

    return esEmpty.typeof();
  }

  public static ESBase setProperty(ESBase base, ESString field, ESBase value)
    throws Throwable
  {
    base.setProperty(field, value);
    
    return value;
  }

  public static ESBase doVoid(ESBase value)
  {
    return ESBase.esUndefined;
  }

  public ESBase array(ESBase value)
    throws Throwable
  {
    ESBase array = call(global, ARRAY, 0);
    array.setProperty(0, value);
    return array;
  }

  public static ESBase comma(ESBase left, ESBase right)
  {
    return right;
  }

  public static ESBase _first(ESBase left, ESBase right)
    throws Throwable
  {
    // This is only used for postfix
    if (! (left instanceof ESNumber))
      return ESNumber.create(left.toNum());
    else
      return left;
  }

  public static double _first(double left, double right)
    throws Throwable
  {
    return left;
  }

  /**
   * Returns the first value in a tuple.  Used
   *
   * @param left the first value
   * @param right the right value
   *
   * @return the first value.
   */
  public static int _first(int left, int right)
    throws Throwable
  {
    return left;
  }

  public static double _pre(ESBase expr, ESString field, int inc)
    throws Throwable
  {
    double oldVal = expr.getProperty(field).toNum();
    ESNumber newVal = ESNumber.create(oldVal + inc);

    expr.setProperty(field, newVal);
    
    return oldVal + inc;
  }

  public static double _post(ESBase expr, ESString field, int inc)
    throws Throwable
  {
    double oldVal = expr.getProperty(field).toNum();
    ESNumber newVal = ESNumber.create(oldVal + inc);

    expr.setProperty(field, newVal);
    
    return oldVal;
  }

  public double _pre(ESString field, int inc)
    throws Throwable
  {
    double oldVal = getScopeProperty(field).toNum();
    ESNumber newVal = ESNumber.create(oldVal + inc);

    setScopeProperty(field, newVal);
    
    return oldVal + inc;
  }

  public double _post(ESString field, int inc)
    throws Throwable
  {
    double oldVal = getScopeProperty(field).toNum();
    ESNumber newVal = ESNumber.create(oldVal + inc);

    setScopeProperty(field, newVal);
    
    return oldVal;
  }

  public ESBase setGlobalProperty(ESString id, ESBase value) throws Throwable
  {
    global.setProperty(id, value);
    
    return value;
  }

  /**
   * Returns the global variable of the id, throwing an exception if
   * the it's undefined.
   */
  public ESBase getGlobalVariable(ESString id) throws Throwable
  {
    ESBase value = global.getProperty(id);
    if (value == ESBase.esEmpty)
      throw new ESUndefinedException("undefined variable `" + id + "'");

    return value;
  }

  public ESBase getScopeProperty(ESString id) throws Throwable
  {
    for (int i = scopeLength - 1; i >= 0; i--) {
      ESBase value;
      if ((value = scope[i].getProperty(id)) != esEmpty) {
        return value;
      }
    }

    throw new ESUndefinedException("undefined variable `" + id + "'");
  }

  public void fillScope()
  {
    if (callee instanceof ESClosure) {
      ESClosure closure = (ESClosure) callee;

      if (closure.scopeLength == 0) {
        scope[0] = caller.global;
        scopeLength = 1;
        return;
      }
      for (int i = 0; i < closure.scopeLength; i++) {
        scope[i] = closure.scope[i];
      }
      scopeLength = closure.scopeLength;
    }
    else {
      scope[0] = caller.global;
      scopeLength = 1;
    }
  }

  public ESBase hasScopeProperty(ESString id) throws Throwable
  {
    for (int i = scopeLength - 1; i >= 0; i--) {
      ESBase value;
      if ((value = scope[i].getProperty(id)) != esEmpty)
        return value;
    }

    return esEmpty;
  }

  public ESBase setScopeProperty(ESString id, ESBase value) throws Throwable
  {
    if (value == esEmpty)
      value = esUndefined;

    for (int i = scopeLength - 1; i > 0; i--) {
      if (scope[i].getProperty(id) != esEmpty) {
        scope[i].setProperty(id, value);
        return value;
      }
    }

    global.setProperty(id, value);

    return value;
  }

  public ESBase deleteScopeProperty(ESString id) throws Throwable
  {
    for (int i = scopeLength - 1; i > 0; i--) {
      if (scope[i].getProperty(id) != esEmpty)
        return scope[i].delete(id);
    }

    return global.delete(id);
  }
  /*
  public ESBase startCallScopeProperty(ESString id, int i) throws Throwable
  {
    top = i + 1; 

    for (int j = scopeLength - 1; j >= 0; j--) {
      ESBase value; 

      if ((value = scope[j].getProperty(id)) != esEmpty) {
        stack[i] = global;
        return value;
      }
    }

    throw new ESUndefinedException("undefined call `" + id + "'");
  }
  */

  public int arg(int i, ESBase arg)
  {
    stack[i + 1] = arg;
    
    return 1;
  }

  public ESBase callScope(ESString id, int i) throws Throwable
  {
    top = i + 1;

    int scopeLength = caller.scopeLength;
    ESBase []scope = caller.scope;
    for (int j = scopeLength - 1; j >= 0; j--) {
      ESBase value; 

      if ((value = scope[j].getProperty(id)) != esEmpty) {
        callee = value;
        stack[i] = scope[j];
        return value.call(this, 0);
      }
    }

    throw new ESUndefinedException("undefined call `" + id + "'");
  }

  public ESBase callScope(ESString id, int i, ESBase a) throws Throwable
  {
    top = i + 1;

    int scopeLength = caller.scopeLength;
    ESBase []scope = caller.scope;
    for (int j = scopeLength - 1; j >= 0; j--) {
      ESBase value; 

      if ((value = scope[j].getProperty(id)) != esEmpty) {
        callee = value;
        stack[i] = scope[j];
        stack[i + 1] = a;
        return value.call(this, 1);
      }
    }

    throw new ESUndefinedException("undefined call `" + id + "'");
  }

  public ESBase callScope(ESString id, int i, ESBase a, ESBase b) 
    throws Throwable
  {
    top = i + 1; 

    int scopeLength = caller.scopeLength;
    ESBase []scope = caller.scope;
    for (int j = scopeLength - 1; j >= 0; j--) {
      ESBase value; 

      if ((value = scope[j].getProperty(id)) != esEmpty) {
        callee = value;
        stack[i] = scope[j];
        stack[i + 1] = a;
        stack[i + 2] = b;
        return value.call(this, 2);
      }
    }

    throw new ESUndefinedException("undefined call `" + id + "'");
  }

  public ESBase callScope(ESString id, int i, ESBase a, ESBase b, ESBase c,
                          int length)
    throws Throwable
  {
    top = i + 1; 

    int scopeLength = caller.scopeLength;
    ESBase []scope = caller.scope;
    for (int j = scopeLength - 1; j >= 0; j--) {
      ESBase value;
      if ((value = scope[j].getProperty(id)) != esEmpty) {
        callee = value;
        stack[i] = scope[j];
        stack[i + 1] = a;
        stack[i + 2] = b;
        stack[i + 3] = c;
        return value.call(this, length);
      }
    }

    throw new ESUndefinedException("undefined call `" + id + "'");
  }

  public ESBase call(ESBase base, ESString name, int i) 
    throws Throwable
  {
    top = i + 1; 

    stack[i] = base;
    
    return base.call(this, 0, name);
  }

  public ESBase call(ESBase base, ESString name, int i, ESBase a) 
    throws Throwable
  {
    top = i + 1;

    stack[i] = base;
    stack[i + 1] = a;

    return base.call(this, 1, name);
  }

  public ESBase call(ESBase base, ESString name, int i, ESBase a, ESBase b) 
    throws Throwable
  {
    top = i + 1; 

    stack[i] = base;
    stack[i + 1] = a;
    stack[i + 2] = b;

    return base.call(this, 2, name);
  }

  public ESBase call(ESBase base, ESString name, int i, 
                     ESBase a, ESBase b, ESBase c, int length)
    throws Throwable
  {
    top = i + 1; 

    stack[i] = base;
    stack[i + 1] = a;
    stack[i + 2] = b;
    stack[i + 3] = c;

    return base.call(this, length, name);
  }

  public ESBase call(ESBase base, int i) 
    throws Throwable
  {
    top = i + 1; 

    stack[i] = global;
    callee = base;
    
    return base.call(this, 0);
  }

  public ESBase call(ESBase base, int i, ESBase a) 
    throws Throwable
  {
    top = i + 1;

    stack[i + 1] = a;
    stack[i] = global;
    callee = base;
    return base.call(this, 1);
  }

  public ESBase call(ESBase base, int i, ESBase a, ESBase b) 
    throws Throwable
  {
    top = i + 1; 

    stack[i] = base;
    stack[i + 1] = a;
    stack[i + 2] = b;
    stack[i] = global;
    callee = base;
    return base.call(this, 2);
  }

  public ESBase call(ESBase base, int i,
                     ESBase a, ESBase b, ESBase c, int length)
    throws Throwable
  {
    top = i + 1; 

    stack[i] = base;
    stack[i + 1] = a;
    stack[i + 2] = b;
    stack[i + 3] = c;
    stack[i] = global;
    callee = base;
    return base.call(this, length);
  }

  public ESBase newScope(ESString id, int i) throws Throwable
  {
    top = i + 1;

    int scopeLength = caller.scopeLength;
    ESBase []scope = caller.scope;
    for (int j = scopeLength - 1; j >= 0; j--) {
      ESBase value; 

      if ((value = scope[j].getProperty(id)) != esEmpty) {
        callee = value;
        stack[i] = global;
        return value.construct(this, 0);
      }
    }

    throw new ESUndefinedException("undefined constructor `" + id + "'");
  }

  public ESBase newScope(ESString id, int i, ESBase a) throws Throwable
  {
    top = i + 1;

    int scopeLength = caller.scopeLength;
    ESBase []scope = caller.scope;
    for (int j = scopeLength - 1; j >= 0; j--) {
      ESBase value; 

      if ((value = scope[j].getProperty(id)) != esEmpty) {
        callee = value;
        stack[i] = global;
        stack[i + 1] = a;
        return value.construct(this, 1);
      }
    }

    throw new ESUndefinedException("undefined constructor `" + id + "'");
  }

  public ESBase newScope(ESString id, int i, ESBase a, ESBase b)
    throws Throwable
  {
    top = i + 1;

    int scopeLength = caller.scopeLength;
    ESBase []scope = caller.scope;
    for (int j = scopeLength - 1; j >= 0; j--) {
      ESBase value; 

      if ((value = scope[j].getProperty(id)) != esEmpty) {
        callee = value;
        stack[i] = global;
        stack[i + 1] = a;
        stack[i + 2] = b;
        return value.construct(this, 2);
      }
    }

    throw new ESUndefinedException("undefined constructor `" + id + "'");
  }

  public ESBase newScope(ESString id, int i, ESBase a, ESBase b, ESBase c,
                          int length)
    throws Throwable
  {
    top = i + 1; 

    int scopeLength = caller.scopeLength;
    ESBase []scope = caller.scope;
    for (int j = scopeLength - 1; j >= 0; j--) {
      ESBase value; 

      if ((value = scope[j].getProperty(id)) != esEmpty) {
        callee = value;
        stack[i] = global;
        stack[i + 1] = a;
        stack[i + 2] = b;
        stack[i + 3] = c;
        return value.construct(this, length);
      }
    }

    throw new ESUndefinedException("undefined constructor `" + id + "'");
  }

  public ESBase doNew(ESBase base, ESString name, int i) 
    throws Throwable
  {
    top = i + 1; 

    ESBase obj = base.getProperty(name);
    stack[i] = base;
    callee = obj;
    if (obj != esEmpty)
      return obj.construct(this, 0);
    else
      throw new ESUndefinedException("undefined constructor `" + name + "'");
  }

  public ESBase doNew(ESBase base, ESString name, int i, ESBase a) 
    throws Throwable
  {
    top = i + 1;

    stack[i] = base;
    stack[i + 1] = a;

    ESBase obj = base.getProperty(name);
    callee = obj;
    if (obj != esEmpty)
      return obj.construct(this, 1);
    else
      throw new ESUndefinedException("undefined constructor `" + name + "'");
  }

  public ESBase doNew(ESBase base, ESString name, int i, ESBase a, ESBase b) 
    throws Throwable
  {
    top = i + 1; 

    stack[i] = base;
    stack[i + 1] = a;
    stack[i + 2] = b;

    ESBase obj = base.getProperty(name);
    callee = obj;
    if (obj != esEmpty)
      return obj.construct(this, 2);
    else
      throw new ESUndefinedException("undefined constructor `" + name + "'");
  }

  public ESBase doNew(ESBase base, ESString name, int i, 
                      ESBase a, ESBase b, ESBase c, int length)
    throws Throwable
  {
    top = i + 1; 

    stack[i] = base;
    stack[i + 1] = a;
    stack[i + 2] = b;
    stack[i + 3] = c;

    ESBase obj = base.getProperty(name);
    callee = obj;
    if (obj != esEmpty)
      return obj.construct(this, length);
    else
      throw new ESUndefinedException("undefined constructor `" + name + "'");
  }

  public ESBase doNew(ESBase base, int i) 
    throws Throwable
  {
    top = i + 1; 

    stack[i] = global;
    callee = base;
    return base.construct(this, 0);
  }

  public ESBase doNew(ESBase base, int i, ESBase a) 
    throws Throwable
  {
    top = i + 1;

    stack[i] = global;
    stack[i + 1] = a;
    callee = base;
    
    return base.construct(this, 1);
  }

  public ESBase doNew(ESBase base, int i, ESBase a, ESBase b) 
    throws Throwable
  {
    top = i + 1; 

    stack[i] = global;
    stack[i + 1] = a;
    stack[i + 2] = b;
    callee = base;
    
    return base.construct(this, 2);
  }

  public ESBase doNew(ESBase base, int i, 
                      ESBase a, ESBase b, ESBase c, int length)
    throws Throwable
  {
    top = i + 1; 

    stack[i] = global;
    stack[i + 1] = a;
    stack[i + 2] = b;
    stack[i + 3] = c;
    callee = base;
    
    return base.construct(this, length);
  }

  public void free()
  {
    clear();
    
    for (int i = stack.length - 1; i >= 0; i--)
      stack[i] = null;
    for (int i = scope.length - 1; i >= 0; i--)
      scope[i] = null;
    for (int i = values.length - 1; i >= 0; i--)
      values[i] = null;
    global = null;
  }

  public static Iterator toESIterator(Iterator i)
  {
    return new ESIterator(i);
  }

  public static Iterator toESIterator(Enumeration e)
  {
    return new ESEnumIterator(e);
  }

  public static boolean matchException(ESBase test, Exception e)
  {
    String testString;
    try {
      testString = test.toStr().toString();
    } catch (Throwable foo) {
      testString = "undefined";
    }

    Class eClass = e.getClass();
    String dotted = "." + test;

    for (; eClass != null; eClass = eClass.getSuperclass()) {
      String eString = eClass.getName();

      if (testString.equals(eString) || eString.endsWith(dotted))
        return true;
    }

    return false;
  }

  static class ESIterator implements Iterator {
    Global resin;
    Iterator i;

    public boolean hasNext() 
    {
      return i != null && i.hasNext();
    }

    public Object next()
    {
      Object value = i == null ? null : i.next();

      Object result;
      try {
        if (value == null)
          result = ESBase.esNull;
        else
          result = resin.objectWrap(value);
      } catch (Throwable e) {
        result = ESBase.esNull;
      }

      return result;
    }

    public void remove() { throw new RuntimeException(); }

    ESIterator(Iterator i)
    {
      this.i = i;
      this.resin = Global.getGlobalProto();
    }
  }

  static class ESEnumIterator implements Iterator {
    Global resin;
    Enumeration e;

    public boolean hasNext() 
    {
      return e != null && e.hasMoreElements();
    }

    public Object next()
    {
      Object value = e != null ? e.nextElement() : null;;

      try {
        if (value == null)
          return ESBase.esNull;
        else
          return resin.objectWrap(value);
      } catch (Throwable e) {
        return ESBase.esNull;
      }
    }

    public void remove() { throw new RuntimeException(); }

    ESEnumIterator(Enumeration e)
    {
      this.e = e;
      this.resin = Global.getGlobalProto();
    }
  }
}
