const { toBN, keccak256, fromWei, toWei, randomHex } = require('web3-utils')
const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
const toFixedHex = (number, length = 32) => '0x' + bigInt(number).toString(16).padStart(length * 2, '0');
const websnarkUtils = require('websnark/src/utils')
const buildGroth16 = require('websnark/src/groth16')
const stringifyBigInts = require('websnark/tools/stringifybigint').stringifyBigInts
const snarkjs = require('snarkjs')
const bigInt = snarkjs.bigInt
const MerkleTree = require('fixed-merkle-tree')
const crypto = require('crypto')
const circomlib = require('circomlib')
const Web3 = require('web3');
const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
const levels = 20;
const fs = require('fs')
const subtle = crypto.subtle;

let web3, storage, depositAccount, groth16, circuit, proving_key;

global.cryptoController = {

  storageABI: require('../deps/Storage.json').abi,
  shifterABI: require('../deps/XFTanon.json').abi,
  tokenABI: require('../deps/Token.json').abi,

  helpers: {
    randomHex: randomHex,
    toFixedHex: toFixedHex,
    toBN: toBN,
    keccak256: keccak256,
    fromWei: fromWei,
    toWei: toWei,
    pedersenHash: pedersenHash,
    rbigint: rbigint,
    stringifyBigInts: stringifyBigInts,
    isAddress: (address) => web3.utils.isAddress(address)
  },

  account: {
    balance: 0,
    tokenBalances: {}
  },

  loadFactories: async () => {

    web3 = new Web3(new Web3.providers.HttpProvider(config.rpc), null, { transactionConfirmationBlocks: 1 })

    storage = await new web3.eth.Contract(cryptoController.storageABI, config.storageContract);

    depositAccount = web3.eth.accounts.privateKeyToAccount(config.walletKey);
    web3.eth.accounts.wallet.add(depositAccount);

    groth16 = await buildGroth16()
    circuit = require('../deps/withdraw.json')
    proving_key = fs.readFileSync('./deps/withdraw_proving_key.bin').buffer

    cryptoController.sender = depositAccount.address

    let relayerConfig = await (await fetch(config.relayUrl + "/config")).json()
    cryptoController.relayer = {};
    cryptoController.relayer.refund = toBN(relayerConfig.refund);
    cryptoController.relayer.fee = toBN(relayerConfig.fee);
    cryptoController.relayer.address = relayerConfig.address;
    cryptoController.relayer.shifters = relayerConfig.shifters;

    cryptoController.shifterList = await cryptoController.getShifters();
    cryptoController.shifters = {};
    cryptoController.tokens = {};
    cryptoController.shifterTokens = {};


    if (!cryptoController.xft) {
      cryptoController.xftAddress = config.xftContract;
      cryptoController.xft = await new web3.eth.Contract(cryptoController.tokenABI, cryptoController.xftAddress);
      cryptoController.account.balance = await cryptoController.xft.methods.balanceOf(cryptoController.sender).call()
    }


    for (let i = 0; i < cryptoController.shifterList.length; i++) {

      let shifter = cryptoController.shifterList[i];
      cryptoController.shifters[shifter] = await new web3.eth.Contract(cryptoController.shifterABI, shifter);

      let shifterToken = await cryptoController.shifters[shifter].methods.token().call();
      cryptoController.tokens[shifterToken] = await new web3.eth.Contract(cryptoController.tokenABI, shifterToken);
      cryptoController.shifterTokens[shifter] = shifterToken;

      let newBalance = await cryptoController.tokens[shifterToken].methods.balanceOf(cryptoController.sender).call();
      cryptoController.account.tokenBalances[shifterToken] = cryptoController.account.tokenBalances[shifterToken] + newBalance;
    }
  },
  getOrSetPasswordLocally: () => {
    if (!config.storagePassword) {
      let password = randomHex(32);
      console.log(`No password found. Creating a new one. This will be the only time it is shown. \n\n>> ${password} <<`);
      configController.setConfig('storagePassword', password);
    };
    return config.storagePassword;
  },
  parseSaveNote: (saveNote) => {

    const nullifier = BigInt(`0x${saveNote.slice(2, 64)}`)
    const secret = BigInt(`0x${saveNote.slice(64, 126)}`)
    const preimage = Buffer.concat([nullifier.leInt2Buff(31), secret.leInt2Buff(31)])

    let hexNote = `0x${preimage.toString('hex')}`
    const buffNote = Buffer.from(hexNote.slice(2), 'hex')
    const commitment = pedersenHash(buffNote)

    const CUT_LENGTH = 31

    const nullifierBuff = buffNote.slice(0, CUT_LENGTH)
    const nullifierHash = BigInt(pedersenHash(nullifierBuff))

    return {
      secret,
      nullifier,
      commitment,
      nullifierBuff,
      nullifierHash,
      commitmentHex: toFixedHex(commitment),
      nullifierHex: toFixedHex(nullifierHash)
    }
  },
  refBalance: async (shifterAddress) => {

    if (shifterAddress) {
      let shifterToken = cryptoController.shifterTokens[shifterAddress]
      let newBalance = await cryptoController.tokens[shifterToken].methods.balanceOf(cryptoController.sender).call();
      cryptoController.account.tokenBalances[cryptoController.shifterTokens[shifterAddress]._address] = newBalance;
      cryptoController.account.balance = await cryptoController.xft.methods.balanceOf(cryptoController.sender).call();
    } else {
      for (let i = 0; i < cryptoController.tokens.length; i++) {
        let token = cryptoController.tokens[i];
        let newBalance = await token.methods.balanceOf(cryptoController.sender).call();
        cryptoController.account.tokenBalances[token._address] = newBalance;
      }
    }

  },
  xcrypt: async (data, password, shifter, address, nonce) => {

    if (!nonce && nonce != 0) nonce = (await storage.methods.getDepositsLength(shifter, address, keccak256(password)).call({ from: address }));

    const iv = (await subtle.digest('SHA-256', new Uint8Array(Buffer.from((password + nonce))))).slice(0, 16);
    const dataBuffer = new Uint8Array(Buffer.from(data.slice(2), 'hex'));
    const keyBuffer = new Uint8Array(Buffer.from(password.slice(2), 'hex'));
    const key = await subtle.importKey('raw', keyBuffer, { name: 'AES-GCM' }, false, ['encrypt']);

    try {
      let encrypted = await subtle.encrypt({ name: 'AES-GCM', iv }, key, dataBuffer);
      return "0x" + Buffer.from(encrypted).toString('hex').slice(0, 124);
    } catch (err) {
      console.log(err)
      throw new Error("The data provided couldn't be encrypted or decrypted, please check the inputs");
    }
  },
  generateProof: async (shifter, recipient, note) => {

    const deposit = cryptoController.parseSaveNote(note);
    let { fee, refund, address } = cryptoController.relayer;
    let relayer = address;
    let leafIndex;
    const leaves = await shifter.methods.commitmentList().call()
    leaves.forEach((e, i) => {
      const index = toBN(i).toNumber();
      if (toBN(e).eq(toBN(toFixedHex(deposit.commitment)))) {
        leafIndex = index;
      }
      leaves[i] = toBN(e).toString(10);
    });

    let nullHash = deposit.nullifierHash;
    tree = new MerkleTree(levels, leaves)
    const { pathElements, pathIndices } = tree.path(leafIndex)
    const input = stringifyBigInts({

      root: tree.root(),
      nullifierHash: nullHash,
      relayer,
      recipient,
      fee,
      refund,


      nullifier: deposit.nullifier,
      secret: deposit.secret,
      pathElements: pathElements,
      pathIndices: pathIndices,
    })
    const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
    const { proof } = websnarkUtils.toSolidityInput(proofData)

    const withdrawArgs = [
      toFixedHex(input.root),
      toFixedHex(input.nullifierHash),
      toFixedHex(input.recipient, 20),
      toFixedHex(input.relayer, 20),
      toFixedHex(input.fee),
      toFixedHex(input.refund),
    ]

    return { proof, withdrawArgs }
  },
  getShifters: async () => {
    const storageShifters = await storage.methods.getAllShifters().call();
    const relayerShifters = cryptoController.relayer.shifters;
    return storageShifters.filter(shifter => relayerShifters.includes(shifter));
  }
}

global.helpers = cryptoController.helpers;