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

import com.caucho.es.parser.Parser;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.Vfs;

import java.io.IOException;
import java.util.ArrayList;

/**
 * JavaScript object
 */
class NativeFunction extends Native {
  static final int NEW = 1;
  static final int TO_STRING = NEW + 1;
  static final int CALL = TO_STRING + 1;
  static final int APPLY = CALL + 1;

  static ESId LENGTH = ESId.intern("length");

  /**
   * Create a new object based on a prototype
   */
  private NativeFunction(String name, int n, int len)
  {
    super(name, len);

    this.n = n;
  }

  /**
   * Creates the native Function object
   *
   * XXX: incomplete
   */
  static NativeWrapper create(Global resin)
  {
    ESBase []scope = new ESBase[] { resin.getGlobalProto() };
    ESClosure funProto = new ESClosure(scope, 1);
    funProto.name = ESId.intern("Function");

    Native natFunction = new NativeFunction("Function", NEW, 1);
    NativeWrapper function = new NativeWrapper(resin, natFunction,
					       funProto, ESThunk.FUN_THUNK);
    resin.funProto = funProto;
    
    put(funProto, "toString", TO_STRING, 0);
    put(funProto, "call", CALL, 1);
    put(funProto, "apply", APPLY, 2);
    funProto.prototype = resin.objProto;

    funProto.setClean();
    function.setClean();

    return function;
  }

  private static void put(ESObject proto, String name, int n, int len)
  {
    ESId id = ESId.intern(name);

    proto.put(id, new NativeFunction(name, n, len), DONT_ENUM);
  }

  public ESBase call(Call eval, int length) throws Throwable
  {
    switch (n) {
    case NEW:
      return createAnonymous(eval, length);

      // Object prototype stuff
    case TO_STRING:
      // XXX: Is this correct?  Test.
      if (eval.getThis() instanceof ESClosure) {
	ESClosure closure = (ESClosure) eval.getThis();

	return ESString.create(closure.decompile());
      } else if (eval.getThis() instanceof NativeWrapper) {
	NativeWrapper wrapper = (NativeWrapper) eval.getThis();

	return wrapper.fun.toStr();
      } else
      throw new ESException("to string bound to function: " +
			  eval.getThis().getClass());

    case CALL:
      int oldTop = eval.top;
      ESBase fun = eval.getArg(-1);
      ESBase callThis = null;

      try {
	if (length > 0) {
	  callThis = eval.getArg(0);
	} else
	  callThis = esNull;

	if (callThis == esNull || callThis == esUndefined ||
	    callThis == esEmpty)
	  eval.setArg(0, eval.getGlobal());
	else
	  eval.setArg(0, callThis.toObject());
	eval.top++;

	return fun.call(eval, length > 0 ? length - 1 : 0);
      } finally {
	eval.top = oldTop;
      }

    case APPLY:
      return apply(eval, length);

    default:
      throw new ESException("Unknown object function");
    }
  }

  /**
   * Create a new parser instance.
   */
  private ESClosure parseFunction(Call eval, int length) throws Throwable
  {
    StringBuffer sbuf = new StringBuffer();

    sbuf.append("function anonymous(");
    ArrayList argList = new ArrayList();
    for (int i = 0; i < length - 1; i++) {
      if (i != 0)
	sbuf.append(",");
      String str = eval.getArg(i).toString();
      int j = 0;
      int p = 0;
      
      while ((p = str.indexOf(',', j)) >= 0 ||
             (p = str.indexOf(' ', j)) >= 0) {
        if (j < p)
          argList.add(ESId.intern(str.substring(j, p)));
        j = p + 1;
      }
      if (j < str.length())
        argList.add(ESId.intern(str.substring(j)));
      
      sbuf.append(str);
    }
    ESId []args = new ESId[argList.size()];
    argList.toArray(args);
    sbuf.append("){");
    if (length > 0)
      sbuf.append(eval.getArg(length - 1).toString());
    sbuf.append("}\n");
    sbuf.append("return anonymous();");

    Global resin = Global.getGlobalProto();
    Script script = null;
    try {
      Parser parser = new Parser();
      ReadStream is = Vfs.openString(sbuf.toString());
      script = parser.parse(is, "anonymous", 1);
      is.close();
    } catch (IOException e) {
      e.printStackTrace();
    }

    ESCallable jsClass = script.initClass(resin, eval.getGlobal());

    // It's known that the function will be #2.
    ESClosure fun = new ESClosure(ESId.intern("anonymous"), jsClass,
                                  null, 2, args, eval.getGlobal());
    
    return fun;
  }

  private ESBase createAnonymous(Call eval, int length) throws Throwable
  {
    return parseFunction(eval, length);
  }

  private ESBase apply(Call eval, int length) throws Throwable
  {
    Global resin = Global.getGlobalProto();
    Call call = eval.getCall();

    call.top = 1;
    call.global = eval.global;
    call.caller = eval;

    ESBase fun = eval.getArg(-1);
    ESBase callThis = null;

    if (length > 0) {
      callThis = eval.getArg(0);
    } else
      callThis = esNull;

    if (callThis == esNull || callThis == esUndefined ||
	callThis == esEmpty)
      call.setArg(-1, eval.getGlobal());
    else
      call.setArg(-1, callThis.toObject());

    int j = 0;
    for (int i = 1; i < length; i++) {
      ESBase arg = eval.getArg(i);

      if (arg == esNull || arg == esUndefined || arg == esEmpty)
	continue;

      ESBase arglen = arg.hasProperty(LENGTH);

      if (arglen == null)
	call.setArg(j++, arg);
      else {
	int len = arglen.toInt32();

	if (j + len > call.stack.length - 2)
	  throw new ESException("stack overflow");

	for (int k = 0; k < len; k++)
	  call.setArg(j++, arg.getProperty(ESString.create(k)));

	if (len < 0)
	  call.setArg(j++, arg);
      }
    }

    ESBase value = fun.call(call, j);

    resin.freeCall(call);

    return value;
  }
}
