import {
  binToHex,
  binToNumberUintLE,
  decodeCashAddressFormat,
  decodeBase58Address,
  decodeCashAddress,
  decodeCashAddressFormatWithoutPrefix,
  cashAddressToLockingBytecode,
  CashAddressNetworkPrefix,
  hexToBin,
  instantiateSha256,
  instantiateRipemd160,
  isBech32CharacterSet,
  utf8ToBin,
  numberToBinUintLE,
  encodeCashAddress,
  encodeCashAddressFormat,
  CashAddressType,
} from "@bitauth/libauth";

import { Op, encodeNullDataScript } from "@cashscript/utils";

/**
 * Helper function to convert an address to a public key hash
 *
 * @param address   Address to convert to a hash
 *
 * @returns a public key hash corresponding to the passed address
 */
export function derivePublicKeyHash(address: string): Uint8Array {
  let result;

  // If the address has a prefix decode it as is
  if (address.includes(":")) {
    result = decodeCashAddressFormat(address);
  }
  // otherwise, derive the network from the address without prefix
  else {
    result = decodeCashAddressFormatWithoutPrefix(address);
  }

  if (typeof result === "string") throw new Error(result);

  // return the public key hash
  return result.payload;
}

export function derivePublicKeyHashHex(address: string): string {
  return binToHex(derivePublicKeyHash(address));
}

export function deriveLockingBytecodeHex(address: string): string {
  const bytecode = deriveLockingBytecode(address);
  return binToHex(bytecode);
}

export function deriveLockingBytecode(address: string): Uint8Array {
  const lock = cashAddressToLockingBytecode(address);
  if (typeof lock === "string") throw lock;
  return lock.bytecode;
}

export async function sanitizeAddress(wildString: string) {
  if (typeof wildString != "string")
    throw Error("Cashaddress was not a string");
  // If the address has a prefix decode it as is
  let r, cashAddrResult;

  // in case it comes with spaces
  wildString = wildString.trim()

  // Throw on segwit address
  if (
    wildString.substring(0, 3) === "bc1" ||
    wildString.substring(0, 3) === "tb1"
  )
    throw Error("Refusing to convert segwit P2SH address to cashaddress");

  if (wildString.includes(":")) {
    r = decodeCashAddressFormat(wildString);
  } else {
    // If not Bech32, try to decode a Base58 address
    if (!isBech32CharacterSet(wildString)) {
      if (wildString[0] === "3" || wildString[0] === "2")
        throw Error(
          "Refusing to convert a legacy P2SH address (possibly segwit) to cashaddress"
        );

      r = decodeBase58Address(wildString);
      if (typeof r === "string") throw Error(r);

      let prefix;
      if (r.version === 0 || r.version === 5) {
        prefix = "bitcoincash";
      } else if (r.version === 111) {
        prefix = "bchtest";
      } else {
        throw Error("Couldn't identify type of legacy address");
      }
      cashAddrResult = encodeCashAddress({payload: r.payload, prefix: prefix as CashAddressNetworkPrefix, throwErrors:true, type: "p2pkh"});
      return cashAddrResult.address;
    } else {
      r = decodeCashAddressFormatWithoutPrefix(wildString);
    }
  }
  // otherwise, derive the network from the address without prefix
  if (typeof r === "string") throw Error(r);
  cashAddrResult = encodeCashAddressFormat({payload:r.payload, prefix: r.prefix, throwErrors:true, version:r.version});
  return cashAddrResult.address;
}

export function getPrefixFromNetwork(
  network: string
): CashAddressNetworkPrefix {
  let prefix = !network ? CashAddressNetworkPrefix.mainnet : undefined;
  if (!prefix) {
    if (network == "mainnet") prefix = CashAddressNetworkPrefix.mainnet;
    if (network == "staging") prefix = CashAddressNetworkPrefix.testnet;
    if (network == "chipnet") prefix = CashAddressNetworkPrefix.testnet;
    if (network == "regtest") prefix = CashAddressNetworkPrefix.regtest;
  }
  if (!prefix) throw Error("unknown network");
  return prefix;
}

export function createOpReturnData(opReturnData: string[]): Uint8Array {
  const script = [
    Op.OP_RETURN,
    ...opReturnData.map((output: string) => toBin(output)),
  ];

  return encodeNullDataScript(script);
}

export function toBin(output: string): Uint8Array {
  const data = output.replace(/^0x/, "");
  const encode = data === output ? utf8ToBin : hexToBin;
  return encode(data);
}

export function toHex(num: number|bigint): string {
  num = Number(num)
  let hex = binToHex(numberToBinUintLE(num)).toUpperCase();
  if (!hex) hex = "00";
  return "0x" + hex;
}

export function binToNumber(data: Uint8Array): number {
  const h = binToNumberUintLE(data);
  return h;
}

export function binToBigInt(data: Uint8Array): bigint {
  const h = binToNumberUintLE(data);
  return BigInt(h);
}

// For decoding OP_RETURN data
export function decodeNullDataScript(data: Uint8Array | string) {
  if (typeof data === "string") data = hexToBin(data);

  if (data.slice(0, 1)[0] !== 106) {
    throw Error(
      "Attempted to decode NullDataScript without a OP_RETURN code (106), not an OpReturn output?"
    );
  }

  // skip the OP_RETURN code data[0]
  let i = 1;

  const r: Uint8Array[] = [];
  while (i < data.length) {
    if (data.slice(i, i + 1)[0] === 0x4c) {
      r.push(data.slice(i, i + 1));
      i + 1;
    } else if (data.slice(i, i + 1)[0] === 0x4d) {
      throw Error("Not Implemented");
    } else {
      const len = data.slice(i, i + 1)[0]!;
      const start = i + 1;
      const end = start + len;
      r.push(data.slice(start, end));
      i = end;
    }
  }
  return r;
}

/**
 * hash160 - Calculate the sha256, ripemd160 hash of a value
 *
 * @param {message} Uint8Array       value to hash as a binary array
 *
 * @returns a promise to the hash160 value of the input
 */
export async function hash160(message: Uint8Array) {
  const ripemd160 = await instantiateRipemd160();
  const sha256 = await instantiateSha256();
  return ripemd160.hash(sha256.hash(message));
}

/**
 * sha256 - Calculate the sha256 a value
 *
 * @param {message} Uint8Array       value to hash as a binary array
 *
 * @returns a promise to the sha256 value of the input
 */
export async function sha256(message: Uint8Array) {
  const sha256 = await instantiateSha256();
  return sha256.hash(message);
}

// Simple function to get a random integer
export function getRandomIntWeak(max: number) {
  return Math.floor(Math.random() * Math.floor(max));
}

export function sum(previousValue: any, currentValue: any) {
  return BigInt(previousValue) + BigInt(currentValue);
}

export function sumNumber(previousValue: any, currentValue: any) {
  return (previousValue) + (currentValue);
}

export function parseBigInt(num:string):bigint{
  return BigInt(parseInt(num))
}

export function assurePkh(address: string){
  const cashaddrInfo = decodeCashAddress(address)
  if(typeof cashaddrInfo === "string") throw Error(cashaddrInfo)
  if(cashaddrInfo.type!=CashAddressType.p2pkh) throw ("Provided address was not a pay to public key hash address")
}

/**
* Helper function to convert an address to a locking script
*
* @param address   Address to convert to locking script
*
* @returns a locking script corresponding to the passed address
*/
export function addressToLockScript(address: string): Uint8Array {
  const result = cashAddressToLockingBytecode(address);

  if (typeof result === 'string') throw new Error(result);

  return result.bytecode;
}


/**
 * Helper function to convert an address to an electrum-cash compatible scripthash.
 * This is necessary to support electrum versions lower than 1.4.3, which do not
 * support addresses, only script hashes.
 *
 * @param address Address to convert to an electrum scripthash
 *
 * @returns The corresponding script hash in an electrum-cash compatible format
 */
export async function addressToElectrumScriptHash(address: string): Promise<string> {
  // Retrieve locking script
  const lockScript = addressToLockScript(address);

  // Hash locking script
  const scriptHash = await sha256(lockScript);

  // Reverse scripthash
  scriptHash.reverse();

  // Return scripthash as a hex string
  return binToHex(scriptHash);
}
