#include "./node.h"
#include <nan.h>
#include <tree_sitter/runtime.h>
#include <v8.h>
#include "./conversions.h"
#include <cmath>

namespace node_tree_sitter {

using namespace v8;

Nan::Persistent<String> row_key;
Nan::Persistent<String> column_key;
Nan::Persistent<String> start_index_key;
Nan::Persistent<String> start_position_key;
Nan::Persistent<String> end_index_key;
Nan::Persistent<String> end_position_key;

static unsigned BYTES_PER_CHARACTER = 2;
static uint32_t *point_transfer_buffer;

void InitConversions(Local<Object> exports) {
  row_key.Reset(Nan::Persistent<String>(Nan::New("row").ToLocalChecked()));
  column_key.Reset(Nan::Persistent<String>(Nan::New("column").ToLocalChecked()));
  start_index_key.Reset(Nan::Persistent<String>(Nan::New("startIndex").ToLocalChecked()));
  start_position_key.Reset(Nan::Persistent<String>(Nan::New("startPosition").ToLocalChecked()));
  end_index_key.Reset(Nan::Persistent<String>(Nan::New("endIndex").ToLocalChecked()));
  end_position_key.Reset(Nan::Persistent<String>(Nan::New("endPosition").ToLocalChecked()));

  point_transfer_buffer = static_cast<uint32_t *>(malloc(2 * sizeof(uint32_t)));
  auto js_point_transfer_buffer = ArrayBuffer::New(Isolate::GetCurrent(), point_transfer_buffer, 2 * sizeof(uint32_t));
  exports->Set(Nan::New("pointTransferArray").ToLocalChecked(), Uint32Array::New(js_point_transfer_buffer, 0, 2));
}

void TransferPoint(const TSPoint &point) {
  point_transfer_buffer[0] = point.row;
  point_transfer_buffer[1] = point.column / 2;
}

Local<Object> RangeToJS(const TSRange &range) {
  Local<Object> result = Nan::New<Object>();
  result->Set(Nan::New(start_position_key), PointToJS(range.start_point));
  result->Set(Nan::New(start_index_key), ByteCountToJS(range.start_byte));
  result->Set(Nan::New(end_position_key), PointToJS(range.end_point));
  result->Set(Nan::New(end_index_key), ByteCountToJS(range.end_byte));
  return result;
}

Nan::Maybe<TSRange> RangeFromJS(const Local<Value> &arg) {
  if (!arg->IsObject()) {
    Nan::ThrowTypeError("Range must be a {startPosition, endPosition, startIndex, endIndex} object");
    return Nan::Nothing<TSRange>();
  }

  TSRange result;

  Local<Object> js_range = Local<Object>::Cast(arg);

  #define INIT(field, key, Type) { \
    auto field = Type(js_range->Get(Nan::New(key))); \
    if (field.IsJust()) { \
      result.field = field.FromJust(); \
    } else { \
      return Nan::Nothing<TSRange>(); \
    } \
  }

  INIT(start_point, start_position_key, PointFromJS);
  INIT(end_point, end_position_key, PointFromJS);
  INIT(start_byte, start_index_key, ByteCountFromJS);
  INIT(end_byte, end_index_key, ByteCountFromJS);

  #undef INIT

  return Nan::Just(result);
}

Local<Object> PointToJS(const TSPoint &point) {
  Local<Object> result = Nan::New<Object>();
  result->Set(Nan::New(row_key), Nan::New<Number>(point.row));
  result->Set(Nan::New(column_key), ByteCountToJS(point.column));
  return result;
}

Nan::Maybe<TSPoint> PointFromJS(const Local<Value> &arg) {
  if (!arg->IsObject()) {
    Nan::ThrowTypeError("Point must be a {row, column} object");
    return Nan::Nothing<TSPoint>();
  }

  Local<Object> js_point = Local<Object>::Cast(arg);
  Local<Value> js_row = js_point->Get(Nan::New(row_key));
  if (!js_row->IsNumber()) {
    Nan::ThrowTypeError("Point.row must be a number");
    return Nan::Nothing<TSPoint>();
  }

  Local<Value> js_column = js_point->Get(Nan::New(column_key));
  if (!js_column->IsNumber()) {
    Nan::ThrowTypeError("Point.column must be a number");
    return Nan::Nothing<TSPoint>();
  }

  uint32_t row, column;
  if (std::isfinite(js_row->NumberValue())) {
    row = static_cast<uint32_t>(js_row->Int32Value());
  } else {
    row = UINT32_MAX;
  }

  if (std::isfinite(js_column->NumberValue())) {
    column = static_cast<uint32_t>(js_column->Int32Value()) * BYTES_PER_CHARACTER;
  } else {
    column = UINT32_MAX;
  }

  return Nan::Just<TSPoint>({row, column});
}

Local<Number> ByteCountToJS(uint32_t byte_count) {
  return Nan::New<Number>(byte_count / BYTES_PER_CHARACTER);
}

Nan::Maybe<uint32_t> ByteCountFromJS(const v8::Local<v8::Value> &arg) {
  if (!arg->IsUint32()) {
    Nan::ThrowTypeError("Character index must be a number");
    return Nan::Nothing<uint32_t>();
  }

  return Nan::Just<uint32_t>(arg->Uint32Value() * BYTES_PER_CHARACTER);
}

}  // namespace node_tree_sitter
