#include <napi.h>
#include "clHCA.h"
#include <stdio.h>
#include "HCAAsyncWorker.h"

void BufferFinalizer(Napi::Env env, void* data, void* hint);
void BufferFinalizer(Napi::Env env, void* data, void* hint) {
  std::vector<uint8_t>* buf = static_cast<std::vector<uint8_t>*>(hint);
  delete buf;
}

class HCADecoder : public Napi::ObjectWrap<HCADecoder> {
public:
  static Napi::Object init(Napi::Env env, Napi::Object exports);
  static Napi::Value _getInfo(const Napi::CallbackInfo &info);
  HCADecoder(const Napi::CallbackInfo &info);
private:
  clHCA _hca;

  Napi::Value _decodeToWaveFileSync(const Napi::CallbackInfo &info);
  // Napi::Value _decrypt(const Napi::CallbackInfo &info);
  Napi::Value _printInfo(const Napi::CallbackInfo &info);
  Napi::Value _decodeToWaveFile(const Napi::CallbackInfo &info);
  Napi::Value _decodeToMemory(const Napi::CallbackInfo &info);
  Napi::Value _decodeToMemorySync(const Napi::CallbackInfo &info);
};

Napi::Object HCADecoder::init(Napi::Env env, Napi::Object exports) {
  // This method is used to hook the accessor and method callbacks
  Napi::Function classConstructor = DefineClass(env, "HCADecoder", {
    StaticMethod("getInfo", HCADecoder::_getInfo),
    InstanceMethod("printInfo", &HCADecoder::_printInfo),
    // InstanceMethod("decrypt", &HCADecoder::_decrypt),
    InstanceMethod("decodeToWaveFile", &HCADecoder::_decodeToWaveFile),
    InstanceMethod("decodeToWaveFileSync", &HCADecoder::_decodeToWaveFileSync),
    InstanceMethod("decodeToMemory", &HCADecoder::_decodeToMemory),
    InstanceMethod("decodeToMemorySync", &HCADecoder::_decodeToMemorySync)
  });

  Napi::FunctionReference* constructor = new Napi::FunctionReference();

  *constructor = Napi::Persistent(classConstructor);
  exports.Set("HCADecoder", classConstructor);
  env.SetInstanceData<Napi::FunctionReference>(constructor);
  return exports;
}

HCADecoder::HCADecoder(const Napi::CallbackInfo &info)
  : Napi::ObjectWrap<HCADecoder>(info), _hca(0xF27E3B22, 0x00003657)
{
  Napi::Env env = info.Env();
  size_t argc = info.Length();

  if (argc < 1) {
    return;
  }
  if (argc < 2) {
    if (info[0].IsNumber()) {
      _hca.SetKey1(info[0].As<Napi::Number>().Uint32Value());
    }
  } else {
    if (info[0].IsNumber()) {
      _hca.SetKey1(info[0].As<Napi::Number>().Uint32Value());
    }

    if (info[1].IsNumber()) {
      _hca.SetKey2(info[1].As<Napi::Number>().Uint32Value());
    }
  }
}

Napi::Value HCADecoder::_printInfo(const Napi::CallbackInfo &info){
  Napi::Env env = info.Env();
  if (info.Length() < 1) {
    Napi::Error::New(env, "HCADecoder::printInfo(): arguments.length < 1").ThrowAsJavaScriptException();
    return Napi::Boolean::New(env, false);
  }

  if (info[0].IsString()) {
    return Napi::Boolean::New(env, _hca.PrintInfo(info[0].As<Napi::String>().Utf8Value().c_str()));
  }

  return Napi::Boolean::New(env, false);
}

// Napi::Value HCADecoder::_decrypt(const Napi::CallbackInfo &info){
//   Napi::Env env = info.Env();
//   if (info.Length() < 1) {
//     Napi::Error::New(env, "HCADecoder::decrypt(): arguments.length < 1").ThrowAsJavaScriptException();
//     return Napi::Boolean::New(env, false);
//   }

//   if (info[0].IsString() && _hca) {
//     const char* hcaPath = info[0].As<Napi::String>().Utf8Value().c_str();
//     bool res = clHCA(ciphKey1, ciphKey2).Decrypt(hcaPath);
//     return Napi::Boolean::New(env, res);
//   }

//   return Napi::Boolean::New(env, false);
// }

static Napi::Value noop(const Napi::CallbackInfo &info) { return info.Env().Undefined(); }

Napi::Value HCADecoder::_decodeToWaveFile(const Napi::CallbackInfo &info){
  Napi::Env env = info.Env();
  size_t argc = info.Length();
  if (argc < 1) {
    Napi::TypeError::New(env, "HCADecoder::decodeToWaveFile(): arguments.length < 1").ThrowAsJavaScriptException();
    return env.Undefined();
  }

  if (!info[0].IsString() && !(info[0].IsArrayBuffer() || info[0].IsTypedArray() || info[0].IsBuffer())) {
    Napi::TypeError::New(env, "HCADecoder::decodeToWaveFile(): typeof arguments[0] should be string | Buffer").ThrowAsJavaScriptException();
    return env.Undefined();
  }

  std::string hca = "";
  Napi::ArrayBuffer hcaArrayBuffer;
  std::string wav = "";
  double volumn = 1;
  int mode = 16;
  int loop = 0;
  Napi::Function callback;

  int i = 0;

  for (i = 0; i < argc; i++) {
    if (info[i].IsFunction()) {
      callback = info[i].As<Napi::Function>();
      break;
    } else {
      switch (i) {
        case 0: {
          if (info[0].IsString()) {
            hca = info[i].As<Napi::String>().Utf8Value();
            wav = hca.find_last_of(".") == std::string::npos ? (hca + ".wav") : (hca.substr(0, hca.find_last_of(".")) + ".wav");
          } else {
            if (argc < 2 || !info[1].IsString()) {
              Napi::TypeError::New(env, "HCADecoder::decodeToWaveFile(): typeof arguments[1] !== 'string'").ThrowAsJavaScriptException();
              return Napi::Value();
            }
            if (info[0].IsArrayBuffer()) {
              hcaArrayBuffer = info[0].As<Napi::ArrayBuffer>();
            } else {
              hcaArrayBuffer = info[0].As<Napi::Object>().Get("buffer").As<Napi::ArrayBuffer>();
            }
          }
          break;
        }
        case 1: {
          if (info[i].IsString()) {
            std::string path = info[i].As<Napi::String>().Utf8Value();
            if (path == "") {
              Napi::TypeError::New(env, "HCADecoder::decodeToWaveFile(): arguments[1] === ''").ThrowAsJavaScriptException();
              return Napi::Value();
            }
            wav = path;
          } else {
            if (!info[0].IsString()) {
              Napi::TypeError::New(env, "HCADecoder::decodeToWaveFile(): typeof arguments[1] !== 'string'").ThrowAsJavaScriptException();
              return Napi::Value();
            }
          }
          break;
        }
        case 2:
          if (info[i].IsNumber()) volumn = info[i].As<Napi::Number>().DoubleValue();
          break;
        case 3:
          if (info[i].IsNumber()) mode = info[i].As<Napi::Number>().Int32Value();
          break;
        case 4:
          if (info[i].IsNumber()) loop = info[i].As<Napi::Number>().Int32Value();
          break;
        default:
          break;
      }
    }
  }
  if (callback.IsEmpty()) {
    callback = Napi::Function::New(env, noop);
  }

  HCAAsyncWorker* worker = hca == ""
    ? new HCAAsyncWorker(_hca.GetKey1(), _hca.GetKey2(), hcaArrayBuffer, wav, volumn, mode, loop, callback)
    : new HCAAsyncWorker(_hca.GetKey1(), _hca.GetKey2(), hca, wav, volumn, mode, loop, callback);
  worker->Queue();

  return env.Undefined();
}

Napi::Value HCADecoder::_decodeToWaveFileSync(const Napi::CallbackInfo &info){
  Napi::Env env = info.Env();
  size_t argc = info.Length();
  if (argc < 1) {
    Napi::TypeError::New(env, "HCADecoder::decodeToWaveFileSync(): arguments.length < 1").ThrowAsJavaScriptException();
    return Napi::String::New(env, "");
  }

  if (!info[0].IsString() && !(info[0].IsArrayBuffer() || info[0].IsTypedArray() || info[0].IsBuffer())) {
    Napi::TypeError::New(env, "HCADecoder::decodeToWaveFileSync(): typeof arguments[0] should be string | Buffer").ThrowAsJavaScriptException();
    return Napi::String::New(env, "");
  }

  std::string hca = "";
  Napi::ArrayBuffer hcaArrayBuffer;
  std::string wav = "";
  double volumn = 1;
  int mode = 16;
  int loop = 0;

  int i = 0;

  for (i = 0; i < argc; i++) {
    switch (i) {
      case 0: {
        if (info[0].IsString()) {
          hca = info[i].As<Napi::String>().Utf8Value();
          wav = hca.find_last_of(".") == std::string::npos ? (hca + ".wav") : (hca.substr(0, hca.find_last_of(".")) + ".wav");
        } else {
          if (argc < 2 || !info[1].IsString()) {
            Napi::TypeError::New(env, "HCADecoder::decodeToWaveFileSync(): typeof arguments[1] !== 'string'").ThrowAsJavaScriptException();
            return Napi::Value();
          }
          if (info[0].IsArrayBuffer()) {
            hcaArrayBuffer = info[0].As<Napi::ArrayBuffer>();
          } else {
            hcaArrayBuffer = info[0].As<Napi::Object>().Get("buffer").As<Napi::ArrayBuffer>();
          }
        }
        break;
      }
      case 1: {
        if (info[i].IsString()) {
          std::string path = info[i].As<Napi::String>().Utf8Value();
          if (path == "") {
            Napi::TypeError::New(env, "HCADecoder::decodeToWaveFileSync(): arguments[1] === ''").ThrowAsJavaScriptException();
            return Napi::Value();
          }
          wav = path;
        } else {
          if (!info[0].IsString()) {
            Napi::TypeError::New(env, "HCADecoder::decodeToWaveFileSync(): typeof arguments[1] !== 'string'").ThrowAsJavaScriptException();
            return Napi::Value();
          }
        }
        break;
      }
      case 2:
        if (info[i].IsNumber()) volumn = info[i].As<Napi::Number>().DoubleValue();
        break;
      case 3:
        if (info[i].IsNumber()) mode = info[i].As<Napi::Number>().Int32Value();
        break;
      case 4:
        if (info[i].IsNumber()) loop = info[i].As<Napi::Number>().Int32Value();
        break;
      default:
        break;
    }
  }

  bool res;
  if (hca == "") {
    HCAFileStream stream = HCAFileStream::Open(hcaArrayBuffer.Data(), hcaArrayBuffer.ByteLength());
    res = _hca.DecodeToWavefile(stream, wav.c_str(), volumn, mode, loop);
  } else {
    res = _hca.DecodeToWavefile(hca.c_str(), wav.c_str(), volumn, mode, loop);
  }

  if (!res) {
    Napi::Error::New(env, hca + " decode failed.").ThrowAsJavaScriptException();
    return Napi::String::New(env, "");
  }

  return Napi::String::New(env, wav);
}

Napi::Value HCADecoder::_decodeToMemory(const Napi::CallbackInfo &info) {
  Napi::Env env = info.Env();
  size_t argc = info.Length();
  if (argc < 1) {
    Napi::TypeError::New(env, "HCADecoder::decodeToMemory(): arguments.length < 1").ThrowAsJavaScriptException();
    return env.Undefined();
  }

  if (!info[0].IsString() && !(info[0].IsArrayBuffer() || info[0].IsTypedArray() || info[0].IsBuffer())) {
    Napi::TypeError::New(env, "HCADecoder::decodeToMemory(): typeof arguments[0] should be string | Buffer").ThrowAsJavaScriptException();
    return env.Undefined();
  }

  std::string hca = "";
  Napi::ArrayBuffer hcaArrayBuffer;
  double volumn = 1;
  int mode = 16;
  int loop = 0;
  Napi::Function callback;

  int i = 0;

  for (i = 0; i < argc; i++) {
    if (info[i].IsFunction()) {
      callback = info[i].As<Napi::Function>();
      break;
    } else {
      switch (i) {
        case 0: {
          if (info[0].IsString()) {
            hca = info[i].As<Napi::String>().Utf8Value();
          } else {
            if (info[0].IsArrayBuffer()) {
              hcaArrayBuffer = info[0].As<Napi::ArrayBuffer>();
            } else {
              hcaArrayBuffer = info[0].As<Napi::Object>().Get("buffer").As<Napi::ArrayBuffer>();
            }
          }
          break;
        }
        case 1:
          if (info[i].IsNumber()) volumn = info[i].As<Napi::Number>().DoubleValue();
          break;
        case 2:
          if (info[i].IsNumber()) mode = info[i].As<Napi::Number>().Int32Value();
          break;
        case 3:
          if (info[i].IsNumber()) loop = info[i].As<Napi::Number>().Int32Value();
          break;
        default:
          break;
      }
    }
  }
  if (callback.IsEmpty()) {
    callback = Napi::Function::New(env, noop);
  }

  HCAAsyncWorker* worker = hca == ""
    ? new HCAAsyncWorker(_hca.GetKey1(), _hca.GetKey2(), hcaArrayBuffer, "", volumn, mode, loop, callback)
    : new HCAAsyncWorker(_hca.GetKey1(), _hca.GetKey2(), hca, "", volumn, mode, loop, callback);
  worker->Queue();

  return env.Undefined();
}

Napi::Value HCADecoder::_decodeToMemorySync(const Napi::CallbackInfo &info){
  Napi::Env env = info.Env();
  size_t argc = info.Length();
  if (argc < 1) {
    Napi::TypeError::New(env, "HCADecoder::decodeToMemorySync(): arguments.length < 1").ThrowAsJavaScriptException();
    return Napi::String::New(env, "");
  }

  if (!info[0].IsString() && !(info[0].IsArrayBuffer() || info[0].IsTypedArray() || info[0].IsBuffer())) {
    Napi::TypeError::New(env, "HCADecoder::decodeToMemorySync(): typeof arguments[0] should be string | Buffer").ThrowAsJavaScriptException();
    return Napi::String::New(env, "");
  }

  std::string hca = "";
  Napi::ArrayBuffer hcaArrayBuffer;
  double volumn = 1;
  int mode = 16;
  int loop = 0;

  int i = 0;

  for (i = 0; i < argc; i++) {
    switch (i) {
      case 0: {
        if (info[0].IsString()) {
          hca = info[i].As<Napi::String>().Utf8Value();
        } else {
          if (info[0].IsArrayBuffer()) {
            hcaArrayBuffer = info[0].As<Napi::ArrayBuffer>();
          } else {
            hcaArrayBuffer = info[0].As<Napi::Object>().Get("buffer").As<Napi::ArrayBuffer>();
          }
        }
        break;
      }
      case 1:
        if (info[i].IsNumber()) volumn = info[i].As<Napi::Number>().DoubleValue();
        break;
      case 2:
        if (info[i].IsNumber()) mode = info[i].As<Napi::Number>().Int32Value();
        break;
      case 3:
        if (info[i].IsNumber()) loop = info[i].As<Napi::Number>().Int32Value();
        break;
      default:
        break;
    }
  }
  HCAFileStream stream = HCAFileStream::Open();
  bool res;
  if (hca == "") {
    HCAFileStream hcaStream = HCAFileStream::Open(hcaArrayBuffer.Data(), hcaArrayBuffer.ByteLength());
    res = _hca.DecodeToMemory(hcaStream, stream, volumn, mode, loop);
  } else {
    res = _hca.DecodeToMemory(hca.c_str(), stream, volumn, mode, loop);
  }
  if (!res) {
    Napi::Error::New(env, hca + " decode failed.").ThrowAsJavaScriptException();
    return Napi::String::New(env, "");
  }

  std::vector<uint8_t>* buf = stream.Release();

  return Napi::Buffer<uint8_t>::NewOrCopy(env, buf->data(), buf->size(), BufferFinalizer, buf);
}

Napi::Value HCADecoder::_getInfo(const Napi::CallbackInfo &info) {
  Napi::Env env = info.Env();
  if (info.Length() < 1) {
    Napi::TypeError::New(env, "HCADecoder::getInfo(): arguments.length < 1").ThrowAsJavaScriptException();
    return Napi::Value();
  }

  if (!info[0].IsString() && !(info[0].IsArrayBuffer() || info[0].IsTypedArray() || info[0].IsBuffer())) {
    Napi::TypeError::New(env, "HCADecoder::getInfo(): typeof arguments[0] should be string | Buffer").ThrowAsJavaScriptException();
    return Napi::Value();
  }

  int r;
  HCAInfo hcainfo;
  if (info[0].IsString()) {
    std::string hcafile = info[0].As<Napi::String>().Utf8Value();
    HCAFileStream hcaStream = HCAFileStream::Open(hcafile.c_str(), "rb");
    r = clHCA::GetInfo(hcaStream, &hcainfo);
    hcaStream.Close();
  } else {
    Napi::ArrayBuffer hcaArrayBuffer = info[0].IsArrayBuffer()
      ? info[0].As<Napi::ArrayBuffer>()
      : info[0].As<Napi::Object>().Get("buffer").As<Napi::ArrayBuffer>();
    HCAFileStream hcaStream = HCAFileStream::Open(hcaArrayBuffer.Data(), hcaArrayBuffer.ByteLength());
    r = clHCA::GetInfo(hcaStream, &hcainfo);
  }
  if (r != 0) {
    Napi::Error::New(env, getInfoErrorMessage(r)).ThrowAsJavaScriptException();
    return Napi::Value();
  }

  Napi::Object ret = Napi::Object::New(env);
  std::string ver = std::to_string(hcainfo.versionMajor);
  ver += ".";
  ver += std::to_string(hcainfo.versionMinor);
  ret["version"] = Napi::String::New(env, ver);
  ret["channelCount"] = Napi::Number::New(env, hcainfo.channelCount);
  ret["samplingRate"] = Napi::Number::New(env, hcainfo.samplingRate);
  ret["blockCount"] = Napi::Number::New(env, hcainfo.blockCount);
  ret["muteHeader"] = Napi::Number::New(env, hcainfo.muteHeader);
  ret["muteFooter"] = Napi::Number::New(env, hcainfo.muteFooter);

  if (hcainfo.comp) {
    Napi::Object comp = Napi::Object::New(env);
    ret["bitRate"] = Napi::Number::New(env, hcainfo.bitRate);
    ret["blockSize"] = Napi::Number::New(env, hcainfo.blockSize);
    comp["comp1"] = Napi::Number::New(env, hcainfo.comp1);
    comp["comp2"] = Napi::Number::New(env, hcainfo.comp2);
    comp["comp3"] = Napi::Number::New(env, hcainfo.comp3);
    comp["comp4"] = Napi::Number::New(env, hcainfo.comp4);
    comp["comp5"] = Napi::Number::New(env, hcainfo.comp5);
    comp["comp6"] = Napi::Number::New(env, hcainfo.comp6);
    comp["comp7"] = Napi::Number::New(env, hcainfo.comp7);
    comp["comp8"] = Napi::Number::New(env, hcainfo.comp8);
    ret["comp"] = comp;
  }
  if (hcainfo.dec) {
    Napi::Object dec = Napi::Object::New(env);
    ret["bitRate"] = Napi::Number::New(env, hcainfo.bitRate);
    ret["blockSize"] = Napi::Number::New(env, hcainfo.blockSize);
    dec["dec1"] = Napi::Number::New(env, hcainfo.dec1);
    dec["dec2"] = Napi::Number::New(env, hcainfo.dec2);
    dec["dec3"] = Napi::Number::New(env, hcainfo.dec3);
    dec["dec4"] = Napi::Number::New(env, hcainfo.dec4);
    dec["dec5"] = Napi::Number::New(env, hcainfo.dec5);
    dec["dec6"] = Napi::Number::New(env, hcainfo.dec6);
    dec["dec7"] = Napi::Number::New(env, hcainfo.dec7);
    ret["dec"] = dec;
  }
  if (hcainfo.vbr) {
    Napi::Object vbr = Napi::Object::New(env);
    vbr["vbr1"] = Napi::Number::New(env, hcainfo.vbr1);
    vbr["vbr2"] = Napi::Number::New(env, hcainfo.vbr2);
    ret["vbr"] = vbr;
  }
  ret["athType"] = Napi::Number::New(env, hcainfo.athType);
  if (hcainfo.loop) {
    Napi::Object loop = Napi::Object::New(env);
    loop["start"] = Napi::Number::New(env, hcainfo.loopStart);
    loop["end"] = Napi::Number::New(env, hcainfo.loopEnd);
    loop["count"] = Napi::Number::New(env, hcainfo.loopCount);
    loop["loop1"] = Napi::Number::New(env, hcainfo.loop1);
    loop["infinite"] = Napi::Boolean::New(env, hcainfo.loopCount == 0x80);
    ret["loop"] = loop;
  }
  ret["ciphType"] = Napi::Number::New(env, hcainfo.ciphType);
  ret["rvaVolume"] = Napi::Number::New(env, hcainfo.rvaVolume);

  return ret;
}

static Napi::Object _index (Napi::Env env, Napi::Object exports) {
  HCADecoder::init(env, exports);
  return exports;
}

NODE_API_MODULE(NODE_GYP_MODULE_NAME, _index)
