/**
 * FourQ - Node.js Native Addon
 * Copyright 2018-2020 Firestack, all rights reserved.
 * */
#include <cstdint>
#include <napi.h>
#include "../features/serverside_unknowability.cc"
#include "../FourQlib/sha512/sha512.c"
#include "../FourQlib/FourQ_64bit_and_portable/FourQ_api.h"

namespace FourQ {

  /* Helper Function Headers */
  unsigned char* get_dummy_pubkey();


  /*
    Module Exports
  */
  Napi::Value GenerateFromSeed(const Napi::CallbackInfo& info) {
    Napi::Env nenv = info.Env();

    Napi::ArrayBuffer secretAB = info[0].As<Napi::ArrayBuffer>();
    unsigned char* secret = (unsigned char*) secretAB.Data();
    
    Napi::ArrayBuffer pubkeyAB = Napi::ArrayBuffer::New(nenv, 32);
    unsigned char* pubkey = (unsigned char*) pubkeyAB.Data();

    SchnorrQ_KeyGeneration(secret, pubkey);

    return pubkeyAB;
  }

  Napi::Value GenerateKeyPair(const Napi::CallbackInfo& info) {
    Napi::Env nenv = info.Env();

    Napi::ArrayBuffer secretAB = Napi::ArrayBuffer::New(nenv, 32);
    unsigned char* secret = (unsigned char*) secretAB.Data();

    Napi::ArrayBuffer pubkeyAB = Napi::ArrayBuffer::New(nenv, 32);
    unsigned char* pubkey = (unsigned char*) pubkeyAB.Data();

    SchnorrQ_FullKeyGeneration(secret, pubkey);

    Napi::Object obj = Napi::Object::New(nenv);
    obj.Set(Napi::String::New(nenv, "publicKey"), pubkeyAB);
    obj.Set(Napi::String::New(nenv, "secretKey"), secretAB);

    return obj;
  }

  Napi::Value Sign(const Napi::CallbackInfo& info) {
    Napi::Env nenv = info.Env();

    Napi::ArrayBuffer messageAB = info[0].As<Napi::ArrayBuffer>();
    unsigned int messageLength = (unsigned int) messageAB.ByteLength();
    unsigned char* message = (unsigned char*) messageAB.Data();

    Napi::ArrayBuffer secretAB = info[1].As<Napi::ArrayBuffer>();
    unsigned char* secret = (unsigned char*) secretAB.Data();

    unsigned char pubkey[32]; // derivable from secret key
    SchnorrQ_KeyGeneration(secret, pubkey);

    Napi::ArrayBuffer sigAB = Napi::ArrayBuffer::New(nenv, 64);
    unsigned char* sig = (unsigned char*) sigAB.Data();

    SchnorrQ_Sign(secret, pubkey, message, messageLength, sig);

    return sigAB;
  }

  Napi::Value Verify(const Napi::CallbackInfo& info) {
    Napi::Env nenv = info.Env();

    Napi::ArrayBuffer sigAB = info[0].As<Napi::ArrayBuffer>();
    unsigned char* sig = (unsigned char*) sigAB.Data();

    Napi::ArrayBuffer messageAB = info[1].As<Napi::ArrayBuffer>();
    uint64_t messageLength = (uint64_t) messageAB.ByteLength();
    unsigned char* message = (unsigned char*) messageAB.Data();

    Napi::ArrayBuffer pubkeyAB = info[2].As<Napi::ArrayBuffer>();
    uint64_t pubkeyLength = (uint64_t) pubkeyAB.ByteLength();
    unsigned char* pubkey;
    if (pubkeyLength == 32) {
      pubkey = (unsigned char*) pubkeyAB.Data();
    } else {
      // See serverside unknowability feature in FourQ.ts
      pubkey = get_dummy_pubkey(); 
    }

    unsigned int result = 0;
    unsigned int* resultPointer = &result;

    SchnorrQ_Verify(pubkey, message, messageLength, sig, resultPointer);

    Napi::Boolean resultBoolean = Napi::Boolean::New(nenv, (result > 0));

    return resultBoolean;
  }

  /**
   * ECDH, Key Exchange
   */
  Napi::Value GenerateKeyPairECDH(const Napi::CallbackInfo& info) {
    Napi::Env nenv = info.Env();

    Napi::ArrayBuffer secretAB = Napi::ArrayBuffer::New(nenv, 32);
    unsigned char* secret = (unsigned char*) secretAB.Data();

    Napi::ArrayBuffer pubkeyAB = Napi::ArrayBuffer::New(nenv, 32);
    unsigned char* pubkey = (unsigned char*) pubkeyAB.Data();

    CompressedKeyGeneration(secret, pubkey);

    Napi::Object obj = Napi::Object::New(nenv);
    obj.Set(Napi::String::New(nenv, "publicKey"), pubkeyAB);
    obj.Set(Napi::String::New(nenv, "secretKey"), secretAB);

    return obj;
  }

  Napi::Value GenerateFromSeedECDH(const Napi::CallbackInfo& info) {
    Napi::Env nenv = info.Env();

    Napi::ArrayBuffer secretAB = info[0].As<Napi::ArrayBuffer>();
    unsigned char* secret = (unsigned char*) secretAB.Data();

    Napi::ArrayBuffer pubkeyAB = Napi::ArrayBuffer::New(nenv, 32);
    unsigned char* pubkey = (unsigned char*) pubkeyAB.Data();

    CompressedPublicKeyGeneration(secret, pubkey);

    return pubkeyAB;
  }

  Napi::Value GetSharedSecretECDH(const Napi::CallbackInfo& info) {
    Napi::Env nenv = info.Env();

    Napi::ArrayBuffer mySecretAB = info[0].As<Napi::ArrayBuffer>();
    unsigned char* my_secret = (unsigned char*) mySecretAB.Data();

    Napi::ArrayBuffer theirPubkeyAB = info[1].As<Napi::ArrayBuffer>();
    unsigned char* their_pubkey = (unsigned char*) theirPubkeyAB.Data();

    Napi::ArrayBuffer sharedSecretAB = Napi::ArrayBuffer::New(nenv, 32);
    unsigned char* shared_secret = (unsigned char*) sharedSecretAB.Data();

    CompressedSecretAgreement(my_secret, their_pubkey, shared_secret);

    return sharedSecretAB;
  }

  Napi::Value XorCryptSHA512(const Napi::CallbackInfo& info) {
    Napi::ArrayBuffer xorCypherAB = info[0].As<Napi::ArrayBuffer>();
    unsigned char* xor_cypher = (unsigned char*) xorCypherAB.Data();

    Napi::ArrayBuffer contentAB = info[1].As<Napi::ArrayBuffer>();
    uint64_t content_length = (uint64_t) contentAB.ByteLength();
    uint8_t* content = (uint8_t*) contentAB.Data();
    uint64_t* content_u64 = (uint64_t*) content;

    uint64_t batch_count = content_length / 64;
    uint64_t batch_count_remainder = content_length - 64 * batch_count;

    uint8_t* hash_holder1 = xor_cypher;
    uint8_t hash_holder2[64];
    uint8_t* hash_holder_temp;
    uint8_t* hash_input = hash_holder1;
    uint8_t* hash_output = hash_holder2;
    uint64_t* hash_u64;

    uint64_t at = 0;
    for (uint64_t i = 0; i < batch_count; ++i) {
      crypto_sha512(hash_input, 64, hash_output);
      // printf("Hash: ");
      // for (int a = 0; a < 64; a++) { printf("%02X", hash_output[a]); }
      // printf("\n");
      hash_u64 = (uint64_t*) hash_output;
      hash_holder_temp = hash_input;
      hash_input = hash_output;
      hash_output = hash_holder_temp;
      content_u64[at] ^= hash_u64[0]; ++at;
      content_u64[at] ^= hash_u64[1]; ++at;
      content_u64[at] ^= hash_u64[2]; ++at;
      content_u64[at] ^= hash_u64[3]; ++at;
      content_u64[at] ^= hash_u64[4]; ++at;
      content_u64[at] ^= hash_u64[5]; ++at;
      content_u64[at] ^= hash_u64[6]; ++at;
      content_u64[at] ^= hash_u64[7]; ++at;
    }

    if (batch_count_remainder) {
        crypto_sha512(hash_input, 64, hash_output);
        // printf("Hash: ");
        // for (int a = 0; a < 64; a++) { printf("%02X", hash_output[a]); }
        // printf("\n");
        // printf("remainder = %llu\n", batch_count_remainder);
        uint8_t* hash_u8 = (uint8_t*) hash_output;
        at = content_length - batch_count_remainder;
        for (uint64_t i = 0; i < batch_count_remainder; ++i) {
          content[at + i] ^= hash_u8[i];
        }
    }

    return contentAB;
  }

  Napi::Object Init(Napi::Env nenv, Napi::Object exports) {
    // Schnorr
    exports.Set(Napi::String::New(nenv, "generateFromSeed"), Napi::Function::New(nenv, GenerateFromSeed));
    exports.Set(Napi::String::New(nenv, "generateKeyPair"), Napi::Function::New(nenv, GenerateKeyPair));
    exports.Set(Napi::String::New(nenv, "sign"), Napi::Function::New(nenv, Sign));
    exports.Set(Napi::String::New(nenv, "verify"), Napi::Function::New(nenv, Verify));
    // ECDH
    exports.Set(Napi::String::New(nenv, "generateFromSeedECDH"), Napi::Function::New(nenv, GenerateFromSeedECDH));
    exports.Set(Napi::String::New(nenv, "generateKeyPairECDH"), Napi::Function::New(nenv, GenerateKeyPairECDH));
    exports.Set(Napi::String::New(nenv, "getSharedSecretECDH"), Napi::Function::New(nenv, GetSharedSecretECDH));
    // Util
    exports.Set(Napi::String::New(nenv, "xorCryptSHA512"), Napi::Function::New(nenv, XorCryptSHA512));
    return exports;
  }

  NODE_API_MODULE(addon, Init)


  /* Helper Function Definitions */
  unsigned char* dummy_pubkey_ptr = NULL;
  unsigned char* get_dummy_pubkey() {
    if (dummy_pubkey_ptr == NULL) { dummy_pubkey_ptr = (unsigned char*) ssu_pub_key; }
    return dummy_pubkey_ptr;
  }

}