#include <node.h>
#include <v8.h>
#include <math.h>
#include <nlopt.h>
#include <stdio.h>

using namespace v8;


Local<Array> cArrayToV8Array(unsigned n, const double* array)
{
  Local<Array> ret = Local<Array>::New(v8::Isolate::GetCurrent(), Array::New(v8::Isolate::GetCurrent(), n));
  for (unsigned i = 0; i < n; ++i)
  {
    ret->Set(i, Number::New(v8::Isolate::GetCurrent(), array[i]));
  }
  return ret;
}

double* v8ArrayToCArray(Local<Array>& array)
{
  double* ret = new double[array->Length()];
  for (unsigned i = 0; i < array->Length(); ++i)
  {
    ret[i] = array->Get(i)->NumberValue();
  }
  return ret;
}

void checkNloptErrorCode(Local<Object>& errors, Local<String>& operation, nlopt_result errorCode)
{
  const char* str;
  switch (errorCode)
  {
    case NLOPT_SUCCESS: 
      str = "Success";
      break;
    case NLOPT_STOPVAL_REACHED:
      str = "Success: Optimization stopped because stopValue was reached";
      break;
    case NLOPT_FTOL_REACHED:
      str = "Success: Optimization stopped because fToleranceRelative or fToleranceAbsolute was reached";
      break;
    case NLOPT_XTOL_REACHED:
      str = "Success: Optimization stopped because xToleranceRelative or xToleranceAbsolute was reached";
      break;
    case NLOPT_MAXEVAL_REACHED:
      str = "Success: Optimization stopped because maxEval was reached";
      break;
    case NLOPT_MAXTIME_REACHED:
      str = "Success: Optimization stopped because maxTime was reached";
      break;
    case NLOPT_FAILURE:
      str = "Failure";
      break;
    case NLOPT_INVALID_ARGS:
      str = "Failure: Invalid arguments";
      break;
    case NLOPT_OUT_OF_MEMORY:
      str = "Failure: Ran out of memory";
      break;
    case NLOPT_ROUNDOFF_LIMITED:
      str = "Failure: Halted because roundoff errors limited progress";
      break;
    case NLOPT_FORCED_STOP:
      str = "Failure: Halted because of a forced termination";
      break;
    default:
      str = "Failure: Unknown Error Code";
      break;
  }
  //set error message for operation
  errors->Set(operation, String::NewFromUtf8(v8::Isolate::GetCurrent(), str));
}

#define GET_VALUE(TYPE, NAME, OBJ) \
  Local<String> key_##NAME = String::NewFromUtf8(v8::Isolate::GetCurrent(), #NAME); \
  Local<TYPE> val_##NAME; \
  if(OBJ->Has(key_##NAME)){ \
    val_##NAME = Local<TYPE>::Cast(OBJ->Get(key_##NAME)); \
  }

#define CHECK_CODE(NAME) \
  checkNloptErrorCode(ret, key_##NAME, code);

#define SIMPLE_CONFIG_OPTION(NAME, CONFIG_METHOD) \
  GET_VALUE(Number, NAME, options) \
  if(!val_##NAME.IsEmpty()){ \
    code = CONFIG_METHOD(opt, val_##NAME->Value()); \
    CHECK_CODE(NAME) \
  }

double optimizationFunc(unsigned n, const double* x, double* grad, void* ptrCallback)
{
  EscapableHandleScope scope(v8::Isolate::GetCurrent());
  Local<Value> undefined;
  Function* callback = (Function*)ptrCallback;
  double returnValue = -1;

  //prepare parms to callback
  Local<Value> argv[3];
  argv[0] = Local<Value>::New(v8::Isolate::GetCurrent(), Number::New(v8::Isolate::GetCurrent(), n));
  argv[1] = cArrayToV8Array(n, x);
  //gradient
  Local<Array> v8Grad;
  if(grad){
    v8Grad = cArrayToV8Array(n, grad);
    argv[2] = v8Grad;
  }
  else {
    argv[2] = Local<Value>::New(v8::Isolate::GetCurrent(), v8::Null(v8::Isolate::GetCurrent()));
  }
  //call callback
  Local<Value> ret = callback->Call(v8::Isolate::GetCurrent()->GetCurrentContext()->Global(), 3, argv);
  //validate return results
  if(!ret->IsNumber()){
    v8::Isolate::GetCurrent()->ThrowException(Exception::TypeError(String::NewFromUtf8(v8::Isolate::GetCurrent(), "Objective or constraint function must return a number.")));
  }
  else if(grad && v8Grad->Length() != n){
    v8::Isolate::GetCurrent()->ThrowException(Exception::TypeError(String::NewFromUtf8(v8::Isolate::GetCurrent(), "Length of gradient array must be the same as the number of parameters.")));
  }
  else { //success
    if(grad){
      for (unsigned i = 0; i < n; ++i) {
        grad[i] = v8Grad->Get(i)->NumberValue();
      }
    }
    returnValue = ret->NumberValue();
  }
  scope.Escape(undefined);
  return returnValue;
}

void Optimize(const FunctionCallbackInfo<Value>& args) {
  EscapableHandleScope scope(v8::Isolate::GetCurrent());
  Local<Object> ret = Local<Object>::New(v8::Isolate::GetCurrent(), Object::New(v8::Isolate::GetCurrent()));
  nlopt_result code = NLOPT_SUCCESS;
  Local<String> key;

  //There is not much validation in this function... should be done in js.
  Local<Object> options = Local<Object>::Cast(args[0]);

  //basic nlopt config
  GET_VALUE(Number, algorithm, options)
  GET_VALUE(Number, numberOfParameters, options)
  unsigned n = val_numberOfParameters->Uint32Value();
  nlopt_opt opt;
  opt = nlopt_create((nlopt_algorithm)val_algorithm->Uint32Value(), n);

  //objective function
  GET_VALUE(Function, minObjectiveFunction, options)
  GET_VALUE(Function, maxObjectiveFunction, options)
  if(!val_minObjectiveFunction.IsEmpty()){
    code = nlopt_set_min_objective(opt, optimizationFunc, *val_minObjectiveFunction);
    CHECK_CODE(minObjectiveFunction)
  }
  else if(!val_maxObjectiveFunction.IsEmpty()){
    code = nlopt_set_max_objective(opt, optimizationFunc, *val_maxObjectiveFunction);
    CHECK_CODE(maxObjectiveFunction)
  }
  else{
    v8::Isolate::GetCurrent()->ThrowException(Exception::TypeError(String::NewFromUtf8(v8::Isolate::GetCurrent(), "minObjectiveFunction or maxObjectiveFunction must be specified")));
    //return scope.Escape(ret);
    args.GetReturnValue().Set(scope.Escape(ret));
    return;
  }

  //optional parms
  GET_VALUE(Array, lowerBounds, options)
  if(!val_lowerBounds.IsEmpty()){
    double* lowerBounds = v8ArrayToCArray(val_lowerBounds);
    code = nlopt_set_lower_bounds(opt, lowerBounds);
    CHECK_CODE(lowerBounds)
    delete[] lowerBounds;
  }

  GET_VALUE(Array, upperBounds, options)
  if(!val_upperBounds.IsEmpty()){
    double* upperBounds = v8ArrayToCArray(val_upperBounds);
    code = nlopt_set_upper_bounds(opt, upperBounds);
    CHECK_CODE(upperBounds)
    delete[] upperBounds;
  }

  SIMPLE_CONFIG_OPTION(stopValue, nlopt_set_stopval)
  SIMPLE_CONFIG_OPTION(fToleranceRelative, nlopt_set_ftol_rel)
  SIMPLE_CONFIG_OPTION(fToleranceAbsolute, nlopt_set_ftol_abs)
  SIMPLE_CONFIG_OPTION(xToleranceRelative, nlopt_set_xtol_rel)
  SIMPLE_CONFIG_OPTION(xToleranceAbsolute, nlopt_set_xtol_abs1)
  SIMPLE_CONFIG_OPTION(maxEval, nlopt_set_maxeval)
  SIMPLE_CONFIG_OPTION(maxTime, nlopt_set_maxtime)

  GET_VALUE(Array, inequalityConstraints, options)
  if(!val_inequalityConstraints.IsEmpty()){
    for (unsigned i = 0; i < val_inequalityConstraints->Length(); ++i)
    {
      Local<Object> obj = Local<Object>::Cast(val_inequalityConstraints->Get(i));
      GET_VALUE(Function, callback, obj)
      GET_VALUE(Number, tolerance, obj)
      code = nlopt_add_inequality_constraint(opt, optimizationFunc, *val_callback, val_tolerance->NumberValue());
      CHECK_CODE(inequalityConstraints)
    }
  }

  
  GET_VALUE(Array, equalityConstraints, options)
  if(!val_equalityConstraints.IsEmpty()){
    for (unsigned i = 0; i < val_equalityConstraints->Length(); ++i)
    {
      Local<Object> obj = Local<Object>::Cast(val_equalityConstraints->Get(i));
      GET_VALUE(Function, callback, obj)
      GET_VALUE(Number, tolerance, obj)
      code = nlopt_add_equality_constraint(opt, optimizationFunc, *val_callback, val_tolerance->NumberValue());
      CHECK_CODE(equalityConstraints)
    }
  }

  //setup parms for optimization
  double* input;
  input = new double[n];
  for (unsigned i = 0; i < n; ++i) {
    input[i] = 0;
  }
  //initalGuess
  GET_VALUE(Array, initalGuess, options)
  if(!val_initalGuess.IsEmpty()){
    ret->Set(key_initalGuess, String::NewFromUtf8(v8::Isolate::GetCurrent(), "Success"));
    for (unsigned i = 0; i < val_initalGuess->Length(); ++i) {
      input[i] = val_initalGuess->Get(i)->NumberValue();
    }
  }
  //do the optimization!
  key = String::NewFromUtf8(v8::Isolate::GetCurrent(), "status");
  double output[1] = {0};
  checkNloptErrorCode(ret, key, nlopt_optimize(opt, input, output));
  ret->Set(String::NewFromUtf8(v8::Isolate::GetCurrent(), "parameterValues"), cArrayToV8Array(n, input));
  delete[] input;
  ret->Set(String::NewFromUtf8(v8::Isolate::GetCurrent(), "outputValue"), Number::New(v8::Isolate::GetCurrent(), output[0]));
  nlopt_destroy(opt);//cleanup
  args.GetReturnValue().Set(scope.Escape(ret));
}

void init(Handle<Object> exports) {
  exports->Set(String::NewFromUtf8(v8::Isolate::GetCurrent(), "optimize"),
      FunctionTemplate::New(v8::Isolate::GetCurrent(), FunctionCallback(Optimize))->GetFunction());
}


NODE_MODULE(nlopt, init)