const BECH32_ALPHABET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
const BECH32_CONST = 1;
const BECH32M_CONST = 0x2bc830a3;

export function bech32Polymod(values: number[]): number {
  const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
  let chk = 1;
  for (const v of values) {
    const b = chk >> 25;
    chk = ((chk & 0x1ffffff) << 5) ^ v;
    for (let i = 0; i < 5; i++) {
      if ((b >> i) & 1) {
        chk ^= GEN[i];
      }
    }
  }
  return chk;
}

export function bech32VerifyChecksum(
  hrp: string,
  data: number[],
  encoding: "bech32" | "bech32m",
): boolean {
  const consts = encoding === "bech32" ? BECH32_CONST : BECH32M_CONST;
  return bech32Polymod([...bech32HrpExpand(hrp), ...data]) === consts;
}

function bech32HrpExpand(hrp: string): number[] {
  const result = [];
  for (let i = 0; i < hrp.length; i++) {
    result.push(hrp.charCodeAt(i) >> 5);
  }
  result.push(0);
  for (let i = 0; i < hrp.length; i++) {
    result.push(hrp.charCodeAt(i) & 31);
  }
  return result;
}

export function bech32Decode(
  bechString: string,
): { hrp: string; words: number[]; encoding: "bech32" | "bech32m" } | null {
  let hasLower = false;
  let hasUpper = false;
  for (let i = 0; i < bechString.length; i++) {
    if (bechString[i] < "!" || bechString[i] > "~") return null;
    if (bechString[i] >= "a" && bechString[i] <= "z") hasLower = true;
    if (bechString[i] >= "A" && bechString[i] <= "Z") hasUpper = true;
  }
  if (hasLower && hasUpper) return null;

  const bechStringLower = bechString.toLowerCase();
  const pos = bechStringLower.lastIndexOf("1");
  if (
    pos < 1 ||
    pos + 7 > bechStringLower.length ||
    bechStringLower.length > 90
  )
    return null;

  const hrp = bechStringLower.substring(0, pos);
  const data = [];
  for (let i = pos + 1; i < bechStringLower.length; i++) {
    const c = bechStringLower[i];
    const d = BECH32_ALPHABET.indexOf(c);
    if (d === -1) return null;
    data.push(d);
  }

  if (
    !bech32VerifyChecksum(hrp, data, "bech32") &&
    !bech32VerifyChecksum(hrp, data, "bech32m")
  ) {
    return null;
  }

  const encoding = bech32VerifyChecksum(hrp, data, "bech32")
    ? "bech32"
    : "bech32m";
  return { hrp, words: data.slice(0, -6), encoding };
}

export function bech32ConvertWords(
  data: number[],
  inBits: number,
  outBits: number,
  pad: boolean,
): number[] {
  let value = 0;
  let bits = 0;
  const maxV = (1 << outBits) - 1;
  const result = [];

  for (const d of data) {
    value = (value << inBits) | d;
    bits += inBits;
    while (bits >= outBits) {
      bits -= outBits;
      result.push((value >> bits) & maxV);
    }
  }

  if (pad && bits > 0) {
    result.push((value << (outBits - bits)) & maxV);
  }

  return result;
}
