const BASE64_CHUNK_UTF16 = 32 * 1024;

function base64Value(character: string): number {
  const code = character.charCodeAt(0);
  if (code >= 65 && code <= 90) return code - 65;
  if (code >= 97 && code <= 122) return code - 97 + 26;
  if (code >= 48 && code <= 57) return code - 48 + 52;
  if (character === "+") return 62;
  if (character === "/") return 63;
  return -1;
}

function isCanonicalBase64(encoded: string): boolean {
  if (encoded.length === 0) return true;
  if (encoded.length % 4 !== 0) return false;
  const padding = encoded.endsWith("==") ? 2 : encoded.endsWith("=") ? 1 : 0;
  const dataEnd = encoded.length - padding;
  for (let index = 0; index < dataEnd; index += 1) {
    if (base64Value(encoded[index] ?? "") < 0) return false;
  }
  if (padding === 2) {
    return (base64Value(encoded[dataEnd - 1] ?? "") & 0x0f) === 0;
  }
  if (padding === 1) {
    return (base64Value(encoded[dataEnd - 1] ?? "") & 0x03) === 0;
  }
  return true;
}

function decodeBoundedBase64Utf8(
  encoded: string,
  maximumBytes: number,
  label: string,
): string {
  if (
    encoded.length > Math.ceil(maximumBytes / 3) * 4 ||
    !isCanonicalBase64(encoded)
  ) {
    throw new Error(`${label} returned invalid base64 output.`);
  }
  const chunks: Uint8Array[] = [];
  let totalBytes = 0;
  for (let offset = 0; offset < encoded.length; offset += BASE64_CHUNK_UTF16) {
    const binary = atob(encoded.slice(offset, offset + BASE64_CHUNK_UTF16));
    const bytes = new Uint8Array(binary.length);
    for (let index = 0; index < binary.length; index += 1) {
      bytes[index] = binary.charCodeAt(index);
    }
    totalBytes += bytes.byteLength;
    if (totalBytes > maximumBytes) {
      throw new Error(`${label} exceeded the replacement output limit.`);
    }
    chunks.push(bytes);
  }
  const combined = new Uint8Array(totalBytes);
  let writeOffset = 0;
  for (const chunk of chunks) {
    combined.set(chunk, writeOffset);
    writeOffset += chunk.byteLength;
  }
  try {
    return new TextDecoder("utf-8", { fatal: true }).decode(combined);
  } catch {
    throw new Error(`${label} returned non-UTF-8 replacement output.`);
  }
}

export function maximumBase64JsonUtf16(
  maximumNonOutputUtf16: number,
  maximumOutputBytes: number,
): number {
  return (
    maximumNonOutputUtf16 + Math.ceil(Math.max(0, maximumOutputBytes) / 3) * 4
  );
}

export function decodeBase64OutputEnvelope(
  value: Readonly<Record<string, unknown>>,
  maximumOutputBytes: number | undefined,
  label: string,
): Readonly<Record<string, unknown>> {
  const hasEncodedOutput = Object.hasOwn(value, "outputBase64");
  if (value.ok !== true) {
    if (hasEncodedOutput || Object.hasOwn(value, "output")) {
      throw new Error(`${label} returned output in a failure envelope.`);
    }
    return value;
  }
  if (maximumOutputBytes === undefined) {
    if (hasEncodedOutput || Object.hasOwn(value, "output")) {
      throw new Error(
        `${label} returned replacement output for a match request.`,
      );
    }
    return value;
  }
  if (
    typeof value.outputBase64 !== "string" ||
    !Number.isSafeInteger(value.outputBytes) ||
    Number(value.outputBytes) < 0 ||
    Number(value.outputBytes) > maximumOutputBytes ||
    Object.hasOwn(value, "output")
  ) {
    throw new Error(`${label} returned invalid encoded output.`);
  }
  const { outputBase64, ...envelope } = value;
  return {
    ...envelope,
    output: decodeBoundedBase64Utf8(
      outputBase64,
      Number(value.outputBytes),
      label,
    ),
  };
}
