#include "v8_ref.h"
#include "v8_cast.h"
#include "v8_object.h"
#include "v8_function.h"
#include "v8_external.h"
#include "v8_errors.h"
#include "v8_macros.h"

using namespace v8;

VALUE rb_cV8Function;
UNWRAPPER(Function);

/* Typecasting */

static Handle<Value> proc_caller(const Arguments &args)
{
  HandleScope scope;

  if (!args.Data().IsEmpty() && args.Data()->IsExternal()) {
    VALUE proc = (VALUE)External::Cast(*args.Data())->Value();
    VALUE proc_args[args.Length()];

    // We have to invoke `arity` on given proc instead of calling rb_proc_arity,
    // because we can have instance of Method instead of Proc...
    int proc_arity = FIX2INT(rb_funcall2(proc, rb_intern("arity"), 0, NULL));
    
    if (proc_arity < 0 || proc_arity == args.Length()) {
      for (int i = 0; i < args.Length(); i++) {
	proc_args[i] = to_ruby(args[i]);
      }

      // Again, we have to invoke `call` method like this instead of rb_proc_call...
      return to_v8(rb_funcall2(proc, rb_intern("call"), args.Length(), proc_args));
    } else {
      ThrowException(Exception::Error(String::New("wrong number of arguments")));
    }
  }

  return Null();
}

Handle<Value> to_v8_function(VALUE value)
{
  HandleScope scope;
  Handle<FunctionTemplate> func = FunctionTemplate::New(proc_caller, to_v8_external(value));
  return func->GetFunction();
}

/* V8::Function methods */

/*
 * call-seq:
 *   V8::Function.new(proc)    => new_func
 *   V8::Function.new { ... }  => new_func
 *
 * When block given then creates new function based on it, otherwise
 * creates function based on passed parameter (proc/lambda/method).
 *
 */
static VALUE rb_v8_function_new(int argc, VALUE *argv, VALUE klass)
{
  HandleScope scope;
  PREVENT_CREATION_WITHOUT_CONTEXT();

  VALUE orig;
  
  if (rb_block_given_p()) {
    orig = rb_block_proc();
  } else {
    if (argc == 1) {
      orig = argv[0];
    } else {
      rb_raise(rb_eArgError, "wrong number of arguments (%d for 1)", argc);
      return Qnil;
    }
  }

  VALUE self = v8_ref_new(klass, to_v8_function(orig), orig);
  rb_iv_set(self, "@origin", orig);
  rb_iv_set(self, "@receiver", Qnil);
  v8_set_peer(self);

  return self;
}

/*
 * call-seq:
 *   func.call_on(recv, *args)  => result
 *
 * Executes function with given arguments on specified receiver. When function code is
 * broken then proper JavaScript error will be returned. 
 *
 */
static VALUE rb_v8_function_call_on(int argc, VALUE *argv, VALUE self)
{
  HandleScope scope;
  TryCatch try_catch;

  if (argc < 1) {
    rb_raise(rb_eArgError, "wrong number of arguments (%d for 1)", FIX2INT(argc));
    return Qnil;
  }
  
  VALUE recv = argv[0];
  
  Handle<Object> this_obj =
    NIL_P(recv) ? Context::GetEntered()->Global() :
    unwrap(recv)->ToObject();
  
  Handle<Value> args[argc-1];
  
  for (int i = 1; i < argc; i++) {
    args[i-1] = to_v8(argv[i]);
  }
  
  Handle<Value> result = unwrap(self)->Call(this_obj, argc-1, args);

  if (try_catch.HasCaught()) {
    return rb_v8_error_new3(try_catch);
  } else {
    return to_ruby(result);
  }
}

/*
 * call-seq:
 *   func.name  => str
 *
 * Returns function's internal name.
 *
 */
static VALUE rb_v8_function_get_name(VALUE self)
{
  HandleScope scope;
  return to_ruby(unwrap(self)->GetName());
}

/*
 * call-seq:
 *   func.name = str  => str
 *
 * Sets function's internal name.
 *
 */
static VALUE rb_v8_function_set_name(VALUE self, VALUE name)
{
  HandleScope scope;
  unwrap(self)->SetName(String::New(StringValuePtr(name)));
  return name;
}

/*
 * call-seq:
 *   func.bind(obj)  => obj
 *
 * Binds given object as function's receiver. 
 *
 */
static VALUE rb_v8_function_bind(VALUE self, VALUE recv)
{
  return rb_iv_set(self, "@receiver", recv);
}


/* Public constructors */

VALUE rb_v8_function_new2(VALUE data)
{
  return rb_v8_function_new(1, &data, rb_cV8Function);
}


/* V8::Function initializer. */
void Init_V8_Function()
{
  rb_cV8Function = rb_define_class_under(rb_mV8, "Function", rb_cV8Object);
  rb_define_singleton_method(rb_cV8Function, "new", RUBY_METHOD_FUNC(rb_v8_function_new), -1);
  rb_define_method(rb_cV8Function, "bind", RUBY_METHOD_FUNC(rb_v8_function_bind), 1);
  rb_define_method(rb_cV8Function, "call_on", RUBY_METHOD_FUNC(rb_v8_function_call_on), -1);
  rb_define_method(rb_cV8Function, "name", RUBY_METHOD_FUNC(rb_v8_function_get_name), 0);
  rb_define_method(rb_cV8Function, "name=", RUBY_METHOD_FUNC(rb_v8_function_set_name), 1);
  rb_define_attr(rb_cV8Function, "receiver", 1, 0);
  rb_define_attr(rb_cV8Function, "origin", 1, 0);
}