#include <v8.h>
#include <node.h>
#include <node_buffer.h>
#include <node_version.h>

#include <cstring>
#include <cstdlib>

#include "v8u.hpp"

extern "C" {
  #include "deflate.h"
  #include "zlib_container.h"
  #include "gzip_container.h"
}

using namespace v8;
using namespace node;

inline Handle<Value> Zf_error(const char* msg = NULL) {
  return ThrowException(Exception::Error(
    String::New(msg ? msg : "Unknown Error")));
}

#define ZF_ASYNC_CSTR(OBJ, VAR)                                            \
  int VAR##_len = OBJ->length();                                               \
  char* VAR = new char[VAR##_len+1];                                           \
  memcpy(VAR, **OBJ, VAR##_len);                                               \
  delete OBJ;                                                                  \
  VAR[VAR##_len] = 0
#define ZF_SYNC_CSTR(OBJ, VAR)                                             \
  int VAR##_len = OBJ.length();                                                \
  char* VAR = new char[VAR##_len+1];                                           \
  memcpy(VAR, *OBJ, VAR##_len);                                                \
  VAR[VAR##_len] = 0


#define ZF_WORK_UNWRAP(IDENTIFIER)                                         \
  IDENTIFIER##_req* r = (IDENTIFIER##_req*)req->data
#define ZF_WORK_QUEUE(IDENTIFIER)                                          \
  r->req.data = r;                                                             \
  return v8::Integer::New(uv_queue_work(uv_default_loop(), &r->req,            \
                                        IDENTIFIER##_work, (uv_after_work_cb)IDENTIFIER##_after))

#define ZF_WORK_PRE(IDENTIFIER)                                            \
  void IDENTIFIER##_work(uv_work_t *req);                                      \
  void IDENTIFIER##_after(uv_work_t *req);                                     \
  struct IDENTIFIER##_req

#define ZF_WORK(IDENTIFIER)                                                \
  void IDENTIFIER##_work(uv_work_t *req) {                                     \
    ZF_WORK_UNWRAP(IDENTIFIER);

#define ZF_WORK_AFTER(IDENTIFIER) }                                        \
  void IDENTIFIER##_after(uv_work_t *req) {                                    \
    v8::HandleScope scope;                                                     \
    ZF_WORK_UNWRAP(IDENTIFIER);

#define ZF_END }

#define ZF_WORK_CALL(ARGC)                                                 \
  v8::TryCatch try_catch;                                                      \
  r->cb->Call(v8::Context::GetCurrent()->Global(), ARGC, argv);                \
  r->cb.Dispose();                                                             \
  delete r;                                                                    \
  if (try_catch.HasCaught()) node::FatalException(try_catch)

ZF_WORK_PRE(deflate) {
  Options options;
  unsigned char bp;
  unsigned char* in;
  size_t insize;
  const char* err;

  Persistent<Object> buf;
  Persistent<Buffer> out;
  Persistent<Function> cb;
  uv_work_t req;
};

//TODO: make an exists(...) pair
V8_SCB(deflate) {
  int len = args.Length()-1; // don't count the callback
  if (len < 1)
    V8_STHROW(v8u::RangeErr("Not enough arguments!"));
  if (!Buffer::HasInstance(args[0]))
    V8_STHROW(v8u::TypeErr("Expected Buffer as first argument"));
  if (len > 3) len = 3;
  if (!args[len]->IsFunction()) V8_STHROW(v8u::TypeErr("A Function is needed as callback!"));
  Local<Object> opts;
  if(len > 1 && args[len-1]->IsObject()) opts = args[len-1]->ToObject();

  deflate_req* r = new deflate_req;
  InitOptions(&r->options);
  Local<Value> opt;
  opt = opts->Get(v8u::Symbol("verbose"));
  r->options.verbose = opt->ToBoolean()->IsTrue() ? 1 : 0;
  opt = opts->Get(v8u::Symbol("numiterations"));
  r->options.numiterations = opt->IsNumber() ? opt->Int32Value() : 20;
  opt = opts->Get(v8u::Symbol("blocksplitting"));
  r->options.blocksplitting = opt->ToBoolean()->IsFalse() ? 0 : 1;
  opt = opts->Get(v8u::Symbol("blocksplittinglast"));
  r->options.blocksplittinglast = opt->ToBoolean()->IsTrue() ? 1 : 0;
  opt = opts->Get(v8u::Symbol("blocksplittingmax"));
  r->options.blocksplittingmax = opt->IsNumber() ? opt->Int32Value() : 15;

  r->buf = v8u::Persist<Object>(args[0]->ToObject());
  r->cb = v8u::Persist<Function>(v8u::Cast<Function>(args[len]));
  ZF_WORK_QUEUE(deflate);
} ZF_WORK(deflate) {
  const unsigned char* in = (unsigned char*)Buffer::Data(r->buf);
  //int length = r->insize = Buffer::Length(r->buf);
  int length = r->insize = Buffer::Length(r->buf);
  unsigned char* out = NULL;
  size_t outsize;
  out = (unsigned char*)realloc(out, length);
  //printf("allocating %d\n", length);
  if(out) {
    //printf("takin care of business %.*s''\n", length, in);
    Deflate(&r->options, 2, 1, in, length, &r->bp, &out, &outsize);
    //printf("took care of business %ld\n", outsize);
    Local<Buffer> buf = Buffer::New((char*)out, outsize);
    r->out = v8u::Persist<Buffer>(buf);
    free(out);
  } else {
    r->err = "could not allocate memory";
  }

  r->buf.Dispose();
  //delete r->options;
} ZF_WORK_AFTER(deflate) {
  v8::Handle<v8::Value> argv [2];
  if (r->err) {
    argv[0] = v8u::Err(r->err);
    argv[1] = v8::Null();
  } else {
    argv[0] = v8::Null();
    argv[1] = r->buf;
  }
  ZF_WORK_CALL(2);
} ZF_END

//require('./build/Debug/zopfli').deflateSync(require('fs').readFileSync('binding.gyp'))
V8_SCB(deflateSync) {
  v8::HandleScope scope;
  Options options;
  unsigned char bp;
  if (args.Length() > 0) {
    Local<Object> obj = v8u::Obj(args[0]);
    const unsigned char* in = (unsigned char*)Buffer::Data(obj);
    int length = Buffer::Length(obj);
    unsigned char* out = NULL;
    if(!Buffer::HasInstance(obj))
      V8_STHROW(v8u::TypeErr("Expected Buffer as first argument"));
    Local<Object> opts;
    if(args[1]->IsObject()) opts = args[1]->ToObject();

    InitOptions(&options);
    Local<Value> opt;
    opt = opts->Get(v8u::Symbol("verbose"));
    options.verbose = opt->ToBoolean()->IsTrue() ? 1 : 0;
    opt = opts->Get(v8u::Symbol("numiterations"));
    options.numiterations = opt->IsNumber() ? opt->Int32Value() : 20;
    opt = opts->Get(v8u::Symbol("blocksplitting"));
    options.blocksplitting = opt->ToBoolean()->IsFalse() ? 0 : 1;
    opt = opts->Get(v8u::Symbol("blocksplittinglast"));
    options.blocksplittinglast = opt->ToBoolean()->IsTrue() ? 1 : 0;
    opt = opts->Get(v8u::Symbol("blocksplittingmax"));
    options.blocksplittingmax = opt->IsNumber() ? opt->Int32Value() : 15;

    size_t outsize = 0;
    //printf("allocating %d\n", length);
    out = (unsigned char*)realloc(out, length);
    if(out) {
      //printf("takin care of business %d\n", length);
      if(opts->Get(v8u::Symbol("gzip"))->ToBoolean()->IsTrue())
        GzipCompress(&options, in, length, &out, &outsize);
      else if(opts->Get(v8u::Symbol("zip"))->ToBoolean()->IsTrue())
        ZlibCompress(&options, in, length, &out, &outsize);
      else
        Deflate(&options, 2, 1, in, length, &bp, &out, &outsize);

      //printf("Gzip: took care of business %ld\n", outsize);
      Buffer* buf = Buffer::New((char*)out, outsize);
      free(out);
      return scope.Close(Local<Value>::New(buf->handle_));
    } else {
      V8_STHROW(v8u::Err("could not allocate memory"));
    }
  }

  return Undefined();
}


extern "C" void init (Handle<Object> target) {
    NODE_SET_METHOD(target, "deflate", deflate);
    NODE_SET_METHOD(target, "deflateSync", deflateSync);
    //NODE_SET_METHOD(target, "zip", zip);
    //NODE_SET_METHOD(target, "gzip", gzip);
}
