import { randomBytes } from 'crypto'
import { readFileSync } from 'fs';
import { Fr } from '@aztec/bb.js/dest/node/types/index.js';
import { MerkleTree } from './MerkleTree.mjs';
import { keccak256 } from "@ethersproject/keccak256/lib/index.js";
import fs from 'fs'
const ZERO_VALUE = "0xf35fcb490b7ea67c3ac26ed530fa5d8dfe8be344e7177ebb63fe02723fb6f725"; 
const MAX_VALUE = Fr.MAX_VALUE; 
export const treeConfig = {
  utxoDepth: 4,
  txDepth: 4,
  stateDepth: 9
}

export const circuitConfig = {
  main: "./src/main.nr"
}
const treeSum = [treeConfig.utxoDepth, treeConfig.txDepth, treeConfig.stateDepth].reduce((a, b) => a + b)
function evalWithScopePrefix(js) {
  let public_inputs = {};
  js.split('\n').forEach(line => {
    const trimmedLine = line.trim();
    if (trimmedLine.length > 0) {
      eval(`public_inputs.` + trimmedLine);
    }
  });
  return public_inputs;
}
export function getInputsObject() {
  
  
  
  
  let regex = /(.*)\: (pub )?(\[.*\; (.*)?\],)?.*/;
  let circuit = fs.readFileSync(circuitConfig.main).toString();
  let inputs = {}
  let input_ordering = []
  
  let structLine = circuit.split('\n').find(line => line.includes('fn main'));
  let circuitSplit = circuit.split('\n');
  
  let structLines = circuitSplit.slice(circuitSplit.indexOf(structLine) + 1, circuitSplit.indexOf(circuitSplit.find(line => line.includes(') {'))));
  let line_maps = structLines.map(line => line.match(regex)).filter(x => x !== null);
  line_maps.forEach(line_map => {
    inputs[line_map[1].trim()] = [(line_map[2] ? true : false), (line_map[4] ? line_map[4] : 1)];
    input_ordering.push(line_map[1].trim());
  });
  return [inputs, input_ordering];
}
export function getInputs() {
  let inputs = getInputsObject()[0];
  return inputs;
}
export function getInputsOrdered(pub) {
  let inputsObj = getInputsObject();
  let inputs = inputsObj[1];
  let input_ordering = inputs;
  if (pub) input_ordering = input_ordering.map(input => inputsObj[0][input][0] ? input : null).filter(x => x !== null);
  return input_ordering;
}
export function getPublicInputsOrdered() {
  return getInputsOrdered(true);
}
export function readToml(path) {
  let public_inputs = getPublicInputsOrdered();
  let unordered_inputs = evalWithScopePrefix(fs.readFileSync(path).toString());
  let ordered_inputs = public_inputs.map(input => unordered_inputs[input]);
  return ordered_inputs;
}
export function randomBytesFr(numBytes) {
  const bytes = randomBytes(numBytes)
  const bytesFr = Fr.fromBufferReduce(bytes)
  return bytesFr
}
export const format = (data) => {
  if (typeof data === "string") return `"${data}"`;
  if (data.length === 0) return "[]";
  return `[\n    "${data.join('",\n    "')}"\n]`;
}
export const dumpToml = (data) => {
  let public_inputs = getInputsOrdered();
  let toml = [];
  public_inputs.forEach((input) => {
    toml.push(`            ${input} = ${format(data[input])}`);
  });
  
  toml = toml.join('\n');
  fs.writeFileSync('./Prover.toml', toml);
}
export const dumpSolidity = () => {
  
  let contract = fs.readFileSync("./contracts/TechnicalPreview.sol").toString();
  
  let structLine = contract.split('\n').find(line => line.includes('BatchPublicInputs'));
  
  let public_inputs = getInputsOrdered(true);
  
  let public_inputs_sizes = public_inputs.map(input => getInputs()[input][1]);
  
  
  let new_struct = `struct BatchPublicInputs {`;
  public_inputs.forEach((input, index) => {
    new_struct += `\n   bytes32${public_inputs_sizes[index] !== 1 ? `[${public_inputs_sizes[index]}]` : ''} ${input};`;
  });
  
  contract = contract.replace(structLine, new_struct);
  
  
  let flattenLine = contract.split('\n').find(line => line.includes('function flattenBatchPublicInputs(BatchPublicInputs memory input) public pure returns (bytes32[] memory) {'));
  let flattenLineIndex = contract.split('\n').indexOf(flattenLine);
  let flatArrayLine = `        bytes32[] memory flatArray = new bytes32[](${public_inputs_sizes.reduce((a, b) => parseInt(a) + parseInt(b))});`;
  contract = contract.split('\n').slice(0, flattenLineIndex + 1).join('\n') + '\n' + flatArrayLine + '\n' + contract.split('\n').slice(flattenLineIndex + 1).join('\n');
  
  let idxLine = `        uint256 idx = 0;\n`;
  contract = contract.split('\n').slice(0, flattenLineIndex + 2).join('\n') + '\n' + idxLine + '\n' + contract.split('\n').slice(flattenLineIndex + 2).join('\n');
  
  let forLoopLines = [];
  public_inputs.forEach((input, index) => {
    if (public_inputs_sizes[index] === 1) {
      forLoopLines.push(`        flatArray[idx++] = input.${input};`);
    } else {
      forLoopLines.push(`        for (uint i = 0; i < ${public_inputs_sizes[index]}; i++) flatArray[idx++] = input.${input}[i];`);
    }
  });
  contract = contract.split('\n').slice(0, flattenLineIndex + 3).join('\n') + '\n' + forLoopLines.join('\n') + '\n' + contract.split('\n').slice(flattenLineIndex + 3).join('\n');
  
  fs.writeFileSync('./contracts/TechnicalPreview.sol', contract);
}
export function path_to_uint8array(path) {
  let buffer = readFileSync(path);
  return new Uint8Array(buffer);
}
const toFixedHex = (number, pad0x, length = 32) => {
  let hexString = number.toString(16).padStart(length * 2, '0');
  return (pad0x ? `0x` + hexString : hexString);
}
export function getSolidityHash(asset) {
  return keccak256(asset);
}
export function generateHashPathInput(hash_path) {
  let hash_path_input = [];
  for (var i = 0; i < hash_path.length; i++) {
    hash_path_input.push(`0x` + hash_path[i]);
  }
  return hash_path_input;
}
export function generateUTXO(batchSize, amounts, _secrets, BarretenbergApi) {
  let utxos = []
  for (let i = 0; i < batchSize; i++) {
    let amountBN = amounts[i]
    let utxo = {
      secret: _secrets[i],
      owner: BarretenbergApi.pedersenPlookupCompress([_secrets[i]]),
      amountBN: amountBN,
      amount: Fr.fromString(toFixedHex(Number(amountBN.toString()), true)),
      assetType: Fr.fromBufferReduce(Buffer.from(getSolidityHash(0), 'hex')),
    }
    utxos.push(utxo)
  }
  return utxos
}
export async function generateTreeProof(utxoIn, BarretenbergApi, contract) {
  let trees = {
    utxo_tree: new MerkleTree(treeConfig.utxoDepth, BarretenbergApi),
    tx_tree: new MerkleTree(treeConfig.txDepth, BarretenbergApi),
    historic_tree: new MerkleTree(treeConfig.stateDepth, BarretenbergApi),
  }
  let commitment = BarretenbergApi.pedersenPlookupCompress(
    [utxoIn.owner, utxoIn.amount, utxoIn.assetType]
  ).toString()
  let old_root = await contract.getRootFromUtxo(commitment)
  let utxo_list = await contract.getUtxoFromRoot(old_root)
  let historic_roots = await contract.getValidRoots()
  let oracle = BarretenbergApi.pedersenPlookupCompress([new Fr(0n)])
  
  for (let i = 0; i < utxo_list.length; i++) {
    trees.utxo_tree.insert(utxo_list[i]);
  }
  
  
  let utxo_root = trees.utxo_tree.root()
  trees.tx_tree.insert(utxo_root);
  let tx_root_Fr = Fr.fromString(trees.tx_tree.root())
  let batch = BarretenbergApi.pedersenPlookupCompress([tx_root_Fr, oracle])
  let new_root = BarretenbergApi.pedersenPlookupCompress([batch, Fr.fromString(old_root)]).toString()
  
  for (let i = 0; i < historic_roots.length; i++) {
    trees.historic_tree.insert(historic_roots[i]);
  }
  let proofs = {
    utxo: {
      leaf: commitment,
      index: trees.utxo_tree.getIndex(commitment),
      root: utxo_root,
      hash_path: trees.utxo_tree.proof(
        trees.utxo_tree.getIndex(commitment)
      ).pathElements
    },
    tx: {
      leaf: utxo_root,
      index: trees.tx_tree.getIndex(utxo_root),
      root: trees.tx_tree.root(),
      hash_path: trees.tx_tree.proof(
        trees.tx_tree.getIndex(utxo_root)
      ).pathElements
    },
    historic: {
      leaf: new_root,
      index: trees.historic_tree.getIndex(new_root),
      root: trees.historic_tree.root(),
      hash_path: trees.historic_tree.proof(
        trees.historic_tree.getIndex(new_root)
      ).pathElements
    }
  }
  return proofs
}
export function generateDataToml(oldRoot, newRoot, trees, api) {
  let zeroHash = api.pedersenPlookupCompress([Fr.fromString(toFixedHex(0, true))])
  const data = {
    tx_in: new Array(16).fill(ZERO_VALUE),
    secrets: new Array(16).fill('0'),
    utxo_in: new Array(48).fill('0'),
    utxo_out: new Array(48).fill('0'),
    oracle: zeroHash.toString(),
    old_root_proof: new Array(16).fill('0'),
    old_root: ZERO_VALUE,
    new_root: ZERO_VALUE,
    current_root: trees.historic_tree.root(),
    indexes: new Array(48).fill('0'),
    hash_path: new Array(16 * treeSum).fill('0'),
    commitment_out: new Array(16).fill(ZERO_VALUE),
    amount_public_in: "0",
    amount_public_out: "0",
    nullifier_hashes: new Array(16).fill('0'),
    recipient: Fr.fromString(toFixedHex(0, true)).toString()
  }
  if (oldRoot !== "0") data.old_root = oldRoot;
  if (newRoot !== "0") data.new_root = newRoot;
  return data
}
export async function generateTestTransaction(utxoIn, utxoOut, trees, treeProof, amountPublic, data, recipient, BarretenbergApi, contract) {
  let utxoInLen = utxoIn.length
  let utxoOutLen = utxoOut.length
  
  for (let i = 0; i < utxoInLen; i++) {
    let ownerHex = utxoIn[i].owner.toString();
    let amountHex = utxoIn[i].amount.toString();
    let assetTypeHex = utxoIn[i].assetType.toString();
    let note_commitment = BarretenbergApi.pedersenPlookupCompress([utxoIn[i].owner, utxoIn[i].amount, utxoIn[i].assetType]);
    let utxoLeaf = note_commitment.toString()
    data.secrets[i] = utxoIn[i].secret
    data.nullifier_hashes[i] = BarretenbergApi.pedersenPlookupCompressFields(utxoIn[i].secret, utxoIn[i].secret)
    data.old_root_proof[i] = await contract.getRootFromUtxo(utxoLeaf)
    data.utxo_in[i * 3 + 0] = ownerHex
    data.utxo_in[i * 3 + 1] = amountHex
    data.utxo_in[i * 3 + 2] = assetTypeHex
    data.indexes[i * 4 + 0] = treeProof[i].utxo.index
    data.indexes[i * 4 + 1] = treeProof[i].tx.index
    data.indexes[i * 4 + 2] = treeProof[i].historic.index
    let utxoPath = treeProof[i].utxo.hash_path
    let txPath = treeProof[i].tx.hash_path
    let historicPath = treeProof[i].historic.hash_path
    for (let j = 0; j < utxoPath.length; j++) {
      data.hash_path[i * treeSum + 0 + j] = utxoPath[j]
      data.hash_path[i * treeSum + treeConfig.utxoDepth + j] = txPath[j]
    }
    for (let k = 0; k < historicPath.length; k++) {
      data.hash_path[i * treeSum + treeConfig.utxoDepth + treeConfig.txDepth + k] = historicPath[k]
    }
  }
  
  for (let i = 0; i < utxoOutLen; i++) {
    let ownerHex = utxoOut[i].owner.toString();
    let amountHex = utxoOut[i].amount.toString();
    let assetTypeHex = utxoOut[i].assetType.toString();
    let note_commitment = BarretenbergApi.pedersenPlookupCompress([utxoOut[i].owner, utxoOut[i].amount, utxoOut[i].assetType]);
    let utxoLeaf = note_commitment.toString()
    trees.utxo_tree.insert(utxoLeaf)
    data.utxo_out[i * 3 + 0] = ownerHex
    data.utxo_out[i * 3 + 1] = amountHex
    data.utxo_out[i * 3 + 2] = assetTypeHex
    data.commitment_out[i] = utxoLeaf
  }
  data.tx_in[0] = trees.utxo_tree.root()
  data.amount_public_in = toFixedHex(Number(amountPublic.amountIn.toString()), true)
  data.amount_public_out = toFixedHex(Number(amountPublic.amountOut.toString()), true)
  data.recipient = recipient
  data.current_root = trees.historic_tree.root()
  dumpToml(data)
}
export function generateTestPublish(trees, data, api) {
  
  let utxoTree = trees.utxo_tree
  let txTree = trees.tx_tree
  let historicTree = trees.historic_tree
  let utxoRoot = utxoTree.root()
  txTree.insert(utxoRoot)
  let txRoot = txTree.root()
  let txRootFr = Fr.fromBufferReduce(Buffer.from(txRoot.slice(2), 'hex'))
  let oracleFr = Fr.fromBufferReduce(toFixedHex(0, true))
  let oracleHash = api.pedersenPlookupCompress([oracleFr])
  let batch = api.pedersenPlookupCompressFields(txRootFr, oracleHash)
  let oldHistoricRoot = Fr.fromBufferReduce(Buffer.from(data.new_root.slice(2), 'hex'))
  let newHistoricRoot = api.pedersenPlookupCompress([batch, oldHistoricRoot])
  let newHistoricRootHex = newHistoricRoot.toString()
  historicTree.insert(newHistoricRootHex)
  data.old_root = oldHistoricRoot.toString()
  data.new_root = newHistoricRootHex
  
  
  trees.utxoTreeOld = trees.utxo_tree
  trees.txTreeOld = trees.tx_tree
  trees.newHistoricRoot = newHistoricRootHex
  trees.utxo_tree = new MerkleTree(4, api)
  trees.tx_tree = new MerkleTree(4, api)
  dumpToml(data)
}