// This is necessary so that tsc can resolve some of the indirect types for
// sc_call, otherwise it errors out - richard@zilliqa.com 2023-03-09
import { Account, Transaction } from "@zilliqa-js/account";
import { Contract, Init } from "@zilliqa-js/contract";
import { BN, bytes, Long, units } from "@zilliqa-js/util";
import { Zilliqa } from "@zilliqa-js/zilliqa";
import fs from "fs";
import { HardhatPluginError } from "hardhat/plugins";
import { HardhatRuntimeEnvironment } from "hardhat/types";

import * as ScillaContractProxy from "../parser/ScillaContractProxy";
import { ContractInfo } from "../parser/ScillaContractsInfoUpdater";
import { Field, Fields, isNumeric } from "../parser/ScillaParser";

export interface Value {
  vname: string;
  type: string;
  value: string;
}

export interface Setup {
  zilliqa: Zilliqa;
  readonly attempts: number;
  readonly timeout: number;
  readonly version: number;
  readonly gasPrice: BN;
  readonly gasLimit: Long;
  accounts: Account[];
}

export let setup: Setup | null = null;

// The optional params are listed in popularity order.
export const initZilliqa = (
  zilliqaNetworkUrl: string,
  chainId: number,
  privateKeys: string[],
  attempts: number = 10,
  timeoutMs: number = 1000,
  gasPriceQa: number = 2000,
  gasLimit: number = 50000
): Setup => {
  const zilliqaObject = new Zilliqa(zilliqaNetworkUrl);
  const accounts: Account[] = [];

  privateKeys.forEach((pk) => {
    zilliqaObject.wallet.addByPrivateKey(pk);
    accounts.push(new Account(pk));
  });

  setup = {
    zilliqa: zilliqaObject,
    version: bytes.pack(chainId, 1),
    gasPrice: units.toQa(gasPriceQa.toString(), units.Units.Li),
    gasLimit: Long.fromNumber(gasLimit),
    attempts,
    timeout: timeoutMs,
    accounts,
  };

  return setup;
};

function read(f: string) {
  const t = fs.readFileSync(f, "utf8");
  return t;
}

/// Allows you to change setup parameters. Available params:
/// gasPrice, gasLimit, attempts, timeout.
export function updateSetup(args: any) {
  if (setup === null) {
    throw new HardhatPluginError(
      "hardhat-scilla-plugin",
      "Please call the initZilliqa function."
    );
  }
  const overrides: any = {};
  if (args.gasPrice) {
    overrides.gasPrice = units.toQa(args.gasPrice.toString(), units.Units.Li);
  }
  if (args.gasLimit) {
    overrides.gasLimit = Long.fromNumber(args.gasLimit);
  }
  if (args.timeout) {
    overrides.timeout = args.timeout;
  }
  if (args.attempts) {
    overrides.attempts = args.attempts;
  }
  const newSetup: Setup = { ...setup, ...overrides };
  setup = newSetup;
}

export function setAccount(account: number | Account) {
  if (setup === null) {
    throw new HardhatPluginError(
      "hardhat-scilla-plugin",
      "Please call initZilliqa function."
    );
  }

  if (account instanceof Account) {
    setup.zilliqa.wallet.defaultAccount = account;
  } else {
    setup.zilliqa.wallet.defaultAccount = setup.accounts[account];
  }
}

export type ContractFunction<T = any> = (...args: any[]) => Promise<T>;

declare module "@zilliqa-js/contract" {
  interface Contract {
    executer?: Account;
    [key: string]: ContractFunction | any;
    connect: (signer: Account) => Contract;
  }
}

export type ScillaContract = Contract;

export interface UserDefinedLibrary {
  name: string;
  address: string;
}

export type OptionalUserDefinedLibraryList = UserDefinedLibrary[] | null;

export async function deploy(
  hre: HardhatRuntimeEnvironment,
  contractName: string,
  compress: boolean,
  userDefinedLibraries: OptionalUserDefinedLibraryList,
  ...args: any[]
): Promise<ScillaContract> {
  const contractInfo: ContractInfo = hre.scillaContracts[contractName];
  if (contractInfo === undefined) {
    throw new Error(`Scilla contract ${contractName} doesn't exist.`);
  }

  let txParamsForContractDeployment = {};
  const constructorParamsLength =
    contractInfo.parsedContract.constructorParams?.length || 0;
  if (args.length === constructorParamsLength + 1) {
    // The last param is Tx info such as amount, nonce, gasPrice
    txParamsForContractDeployment = args.pop();
  }

  const init: Init = fillInit(
    contractName,
    userDefinedLibraries,
    contractInfo.parsedContract.constructorParams,
    ...args
  );

  const [tx, sc] = await deployFromFile(
    contractInfo.path,
    init,
    compress,
    txParamsForContractDeployment
  );
  sc.deployed_by = tx;

  ScillaContractProxy.injectProxies(setup!, contractInfo, sc);

  return sc;
}

export const deployLibrary = async (
  hre: HardhatRuntimeEnvironment,
  libraryName: string,
  compress: boolean
): Promise<ScillaContract> => {
  const contractInfo: ContractInfo = hre.scillaContracts[libraryName];
  if (contractInfo === undefined) {
    throw new Error(`Scilla contract ${libraryName} doesn't exist.`);
  }

  const init: Init = fillLibraryInit();

  const [tx, sc] = await deployFromFile(contractInfo.path, init, compress, {}); // FIXME: In  #45
  sc.deployed_by = tx;

  return sc;
};

const fillLibraryInit = (): Init => {
  const init = [
    {
      vname: "_scilla_version",
      type: "Uint32",
      value: "0",
    },
    {
      vname: "_library",
      type: "Bool",
      value: {
        constructor: "True",
        argtypes: [],
        arguments: [],
      },
    },
  ];
  return init;
};

const fillInit = (
  contractName: string,
  userDefinedLibraries: OptionalUserDefinedLibraryList,
  contractParams: Fields | null,
  ...userSpecifiedArgs: any[]
): Init => {
  const init: Init = [{ vname: "_scilla_version", type: "Uint32", value: "0" }];

  if (userDefinedLibraries) {
    // Underlying zilliqa-js doesn't support push such an object to Init
    (init as any).push({
      vname: "_extlibs",
      type: "List(Pair String ByStr20)",
      value: userDefinedLibraries.map((lib) => ({
        constructor: "Pair",
        argtypes: ["String", "ByStr20"],
        arguments: [lib.name, lib.address],
      })),
    });
  }

  if (contractParams) {
    if (userSpecifiedArgs.length !== contractParams.length) {
      throw new Error(
        `Expected to receive ${contractParams.length} parameters for ${contractName} deployment but got ${userSpecifiedArgs.length}`
      );
    }

    contractParams.forEach((param: Field, index: number) => {
      if (isNumeric(param.type)) {
        init.push({
          vname: param.name,
          type: param.type,
          value: userSpecifiedArgs[index].toString(),
        });
      } else {
        // It's an ADT or string
        init.push({
          vname: param.name,
          type: param.type,
          value: userSpecifiedArgs[index] as any,
        });
      }
    });
  } else {
    if (userSpecifiedArgs.length > 0) {
      throw new Error(
        `Expected to receive 0 parameters for ${contractName} deployment but got ${userSpecifiedArgs.length}`
      );
    }
  }

  return init;
};

// deploy a smart contract whose code is in a file with given init arguments
export async function deployFromFile(
  path: string,
  init: Init,
  compress: boolean,
  txParamsForContractDeployment: any
): Promise<[Transaction, ScillaContract]> {
  if (setup === null) {
    throw new HardhatPluginError(
      "hardhat-scilla-plugin",
      "Please call initZilliqa function."
    );
  }

  const deployer = setup.zilliqa.wallet.defaultAccount ?? setup.accounts[0];
  let code = read(path);
  if (compress) {
    code = compressContract(code);
  }
  const contract = setup.zilliqa.contracts.new(code, init);
  const [tx, sc] = await contract.deploy(
    { ...setup, pubKey: deployer.publicKey, ...txParamsForContractDeployment },
    setup.attempts,
    setup.timeout,
    false
  );

  // Let's add this function for further signer/executer changes.
  ScillaContractProxy.injectConnectors(setup, sc);

  sc.connect(deployer);

  return [tx, sc];
}

export function compressContract(code: string): string {
  // Remove comments
  code = code.replace(/(\(\*.*?\*\))/gms, "");

  // Remove empty lines
  code = code.replace(/(^[ \t]*\n)/gm, "");

  // Remove extra whitespace at the end of the lines
  return code.replace(/[ \t]+$/gm, "");
}
