function leftShift(mod: bigint, field: bigint, n: bigint) {
  return field * mod ** n;
}

function rightShift(mod: bigint, field: bigint, n: bigint) {
  return field / mod ** n;
}

function and(mod: bigint, field: bigint, bit: bigint): bigint {
  let out = 0n;
  let pos = 0n;
  while (field > 0n && bit > 0n) {
    const nextField = rightShift(mod, field, 1n);
    const nextBit = rightShift(mod, bit, 1n);
    const fieldPiece = field - leftShift(mod, nextField, 1n);
    const bitPiece = bit - leftShift(mod, nextBit, 1n);
    if (fieldPiece <= bitPiece) {
      out += leftShift(mod, fieldPiece, pos);
    }
    field = nextField;
    bit = nextBit;
    pos++;
  }
  return out;
}

function max(...bigints: bigint[]) {
  let max = bigints.pop();
  if (max === undefined) throw new Error("no big integers");
  for (const bigint of bigints) {
    if (bigint > max) max = bigint;
  }
  return max;
}

function or(mod: bigint, field: bigint, bit: bigint): bigint {
  let out = 0n;
  let pos = 0n;
  while (field > 0n && bit > 0n) {
    const nextField = rightShift(mod, field, 1n);
    const nextBit = rightShift(mod, bit, 1n);
    const fieldPiece = field - leftShift(mod, nextField, 1n);
    const bitPiece = bit - leftShift(mod, nextBit, 1n);
    out += leftShift(mod, max(fieldPiece, bitPiece), pos);
    field = nextField;
    bit = nextBit;
    pos++;
  }
  if (field > 0) out += leftShift(mod, field, pos);
  if (bit > 0) out += leftShift(mod, bit, pos);
  return out;
}

export class Base {
  public static from(alphabet: string): Base {
    return new Base(alphabet);
  }

  public size: bigint;
  public maxPiece: bigint;
  public alphabetIndex: Record<string, bigint> = {};
  constructor(public alphabet: string) {
    for (let i = 0; i < alphabet.length; i++) {
      this.alphabetIndex[alphabet.charAt(i)] = BigInt(i);
    }
    this.size = BigInt(alphabet.length);
    this.maxPiece = this.size - 1n;
  }

  encode(num: bigint, padLen = 0): string {
    let out = "";
    if (num === 0n) return this.alphabet.charAt(0);
    while (num > 0n) {
      const piece = and(this.size, num, this.maxPiece);
      num = rightShift(this.size, num, 1n);
      out = this.alphabet.charAt(Number(piece)) + out;
    }
    return out.padStart(padLen, this.alphabet.charAt(0));
  }

  decode(encoded: string): bigint {
    let output = 0n;
    for (const char of encoded) {
      output = or(
        this.size,
        leftShift(this.size, output, 1n),
        this.alphabetIndex[char],
      );
    }
    return output;
  }
}
export default Base;
