import { evaluationError } from "./errors.js";
import { Decimal } from "./decimal.js";
import { TYPES, Type } from "./registry.js";
import { register as registerOptional, optionalOf } from "./optional.js";
import { objKeys, arrayFrom } from "./globals.js";
import { TZDate } from "@date-fns/tz";
import { startOfDay, startOfWeek, startOfMonth, startOfYear, endOfDay, endOfWeek, endOfMonth, endOfYear, addDays, addWeeks, addMonths, addYears, addHours, addMinutes, setDate as setDayOfMonth, setMonth, setYear, format as formatDate } from "date-fns";
function toNum(v) {
  if (v instanceof Decimal) return v.toNumber();
  if (typeof v === "bigint") return Number(v);
  return v;
}
export function registerFunctions(registry) {
  const sync = { async: false };
  const functionOverload = (sig, handler) => registry.registerFunctionOverload(sig, handler, sync);
  const identity = (v) => v;
  functionOverload("dyn(dyn): dyn", identity);
  for (const _t in TYPES) {
    const type = TYPES[_t];
    if (!(type instanceof Type)) continue;
    functionOverload(`type(${type.name}): type`, () => type);
  }
  functionOverload("bool(bool): bool", identity);
  functionOverload("bool(string): bool", (v) => {
    switch (v) {
      case "1":
      case "t":
      case "true":
      case "TRUE":
      case "True":
        return true;
      case "0":
      case "f":
      case "false":
      case "FALSE":
      case "False":
        return false;
      default:
        throw evaluationError(
          "bool_conversion_error",
          `bool() conversion error: invalid string value "${v}"`
        );
    }
  });
  functionOverload("size(string): number", (v) => Decimal.from(stringSize(v)));
  functionOverload("size(bytes): number", (v) => Decimal.from(v.length));
  functionOverload("size(list): number", (v) => Decimal.from(v.length ?? v.size));
  functionOverload("size(map): number", (v) => Decimal.from(v instanceof Map ? v.size : objKeys(v).length));
  functionOverload("string.size(): number", (v) => Decimal.from(stringSize(v)));
  functionOverload("bytes.size(): number", (v) => Decimal.from(v.length));
  functionOverload("list.size(): number", (v) => Decimal.from(v.length ?? v.size));
  functionOverload("map.size(): number", (v) => Decimal.from(v instanceof Map ? v.size : objKeys(v).length));
  functionOverload(
    "list.enumerate(): list<map<string, dyn>>",
    (v) => v.map((value, i) => ({ index: Decimal.from(i), value }))
  );
  functionOverload(
    "list.slice(number): list<dyn>",
    (v, start) => v.slice(toNum(start))
  );
  functionOverload(
    "list.slice(number, number): list<dyn>",
    (v, start, end) => v.slice(toNum(start), toNum(end))
  );
  functionOverload("bytes(string): bytes", (v) => ByteOpts.fromString(v));
  functionOverload("bytes(bytes): bytes", identity);
  functionOverload("number(number): number", (v) => Decimal.from(v));
  functionOverload("number(string): number", (v) => {
    const s = v;
    if (!s || s !== s.trim())
      throw evaluationError(
        "number_conversion_error",
        "number() type error: cannot convert to number"
      );
    try {
      return Decimal.from(s);
    } catch {
      throw evaluationError(
        "number_conversion_error",
        "number() type error: cannot convert to number"
      );
    }
  });
  functionOverload("number(bool): number", (v) => Decimal.from(v ? 1 : 0));
  functionOverload("int(number): number", (v) => {
    const d = Decimal.from(v);
    if (d.isInteger()) return d;
    const n = d.toNumber();
    if (!Number.isFinite(n)) throw evaluationError("numeric_overflow", "int() type error: integer overflow");
    return Decimal.from(BigInt(Math.trunc(n)));
  });
  functionOverload("int(string): number", (v) => {
    const s = v;
    if (s !== s.trim() || s.length > 20 || s.includes("0x")) {
      throw evaluationError("int_conversion_error", "int() type error: cannot convert to int");
    }
    try {
      return Decimal.from(s);
    } catch {
      throw evaluationError("int_conversion_error", "int() type error: cannot convert to int");
    }
  });
  functionOverload("double(number): number", (v) => Decimal.from(v));
  functionOverload("double(string): number", (v) => {
    const s = v;
    if (!s || s !== s.trim())
      throw evaluationError(
        "double_conversion_error",
        "double() type error: cannot convert to double"
      );
    try {
      return Decimal.from(s);
    } catch {
      throw evaluationError(
        "double_conversion_error",
        "double() type error: cannot convert to double"
      );
    }
  });
  functionOverload("uint(number): number", (v) => Decimal.from(v));
  functionOverload("uint(string): number", (v) => {
    const s = v;
    if (s !== s.trim() || s.length > 20 || s.includes("0x")) {
      throw evaluationError("uint_conversion_error", "uint() type error: cannot convert to uint");
    }
    try {
      return Decimal.from(s);
    } catch {
      throw evaluationError("uint_conversion_error", "uint() type error: cannot convert to uint");
    }
  });
  functionOverload("string(string): string", identity);
  functionOverload("string(bool): string", (v) => `${v}`);
  functionOverload("string(number): string", (v) => Decimal.from(v).toString());
  functionOverload("string(bytes): string", (v) => ByteOpts.toUtf8(v));
  functionOverload("string.startsWith(string): bool", (a, b) => a.startsWith(b));
  functionOverload("string.endsWith(string): bool", (a, b) => a.endsWith(b));
  functionOverload("string.contains(string): bool", (a, b) => a.includes(b));
  functionOverload("string.lowerAscii(): string", (a) => a.toLowerCase());
  functionOverload("string.upperAscii(): string", (a) => a.toUpperCase());
  functionOverload("string.trim(): string", (a) => a.trim());
  functionOverload(
    "string.indexOf(string): number",
    (string, search) => Decimal.from(string.indexOf(search))
  );
  functionOverload("string.indexOf(string, number): number", (string, search, fromIndex) => {
    if (search === "") return fromIndex;
    const from = toNum(fromIndex);
    const str = string;
    if (from < 0 || from >= str.length) {
      throw evaluationError(
        "index_out_of_range",
        "string.indexOf(search, fromIndex): fromIndex out of range"
      );
    }
    return Decimal.from(str.indexOf(search, from));
  });
  functionOverload(
    "string.lastIndexOf(string): number",
    (string, search) => Decimal.from(string.lastIndexOf(search))
  );
  functionOverload("string.lastIndexOf(string, number): number", (string, search, fromIndex) => {
    if (search === "") return fromIndex;
    const from = toNum(fromIndex);
    const str = string;
    if (from < 0 || from >= str.length) {
      throw evaluationError(
        "index_out_of_range",
        "string.lastIndexOf(search, fromIndex): fromIndex out of range"
      );
    }
    return Decimal.from(str.lastIndexOf(search, from));
  });
  functionOverload("string.substring(number): string", (string, start) => {
    const startNum = toNum(start);
    const str = string;
    if (startNum < 0 || startNum > str.length) {
      throw evaluationError(
        "index_out_of_range",
        "string.substring(start, end): start index out of range"
      );
    }
    return str.substring(startNum);
  });
  functionOverload("string.substring(number, number): string", (string, start, end) => {
    const startNum = toNum(start);
    const str = string;
    if (startNum < 0 || startNum > str.length) {
      throw evaluationError(
        "index_out_of_range",
        "string.substring(start, end): start index out of range"
      );
    }
    const endNum = toNum(end);
    if (endNum < startNum || endNum > str.length) {
      throw evaluationError(
        "index_out_of_range",
        "string.substring(start, end): end index out of range"
      );
    }
    return str.substring(startNum, endNum);
  });
  functionOverload("string.matches(string): bool", (a, b) => {
    try {
      return new RegExp(b).test(a);
    } catch {
      throw evaluationError("invalid_regular_expression", `Invalid regular expression: ${b}`);
    }
  });
  functionOverload("string.split(string): list<string>", (s, sep) => s.split(sep));
  functionOverload("string.split(string, number): list<string>", (s, sep, l) => {
    const limit = toNum(l);
    if (limit === 0) return [];
    const parts = s.split(sep);
    if (limit < 0 || parts.length <= limit) return parts;
    const limited = parts.slice(0, limit - 1);
    limited.push(parts.slice(limit - 1).join(sep));
    return limited;
  });
  functionOverload("list<string>.join(): string", (v) => {
    const arr = v;
    for (let i = 0; i < arr.length; i++) {
      if (typeof arr[i] !== "string") {
        throw evaluationError(
          "invalid_list_element_type",
          "string.join(): list must contain only strings"
        );
      }
    }
    return arr.join("");
  });
  functionOverload("list<string>.join(string): string", (v, sep) => {
    const arr = v;
    for (let i = 0; i < arr.length; i++) {
      if (typeof arr[i] !== "string") {
        throw evaluationError(
          "invalid_list_element_type",
          "string.join(separator): list must contain only strings"
        );
      }
    }
    return arr.join(sep);
  });
  const textEncoder = new TextEncoder();
  const textDecoder = new TextDecoder("utf8");
  const ByteOpts = typeof Buffer !== "undefined" ? {
    byteLength: (v) => Buffer.byteLength(v),
    fromString: (str) => Buffer.from(str, "utf8"),
    toHex: (b) => Buffer.prototype.hexSlice.call(b, 0, b.length),
    toBase64: (b) => Buffer.prototype.base64Slice.call(b, 0, b.length),
    toUtf8: (b) => Buffer.prototype.utf8Slice.call(b, 0, b.length),
    jsonParse: (b) => JSON.parse(b)
  } : {
    textEncoder: new TextEncoder(),
    byteLength: (v) => textEncoder.encode(v).length,
    fromString: (str) => textEncoder.encode(str),
    toHex: typeof Uint8Array.prototype.toHex === "function" ? (b) => b.toHex() : (b) => arrayFrom(b, (i) => i.toString(16).padStart(2, "0")).join(""),
    toBase64: typeof Uint8Array.prototype.toBase64 === "function" ? (b) => b.toBase64() : (b) => btoa(arrayFrom(b, (i) => String.fromCodePoint(i)).join("")),
    toUtf8: (b) => textDecoder.decode(b),
    jsonParse: (b) => JSON.parse(textDecoder.decode(b))
  };
  functionOverload("bytes.json(): map", ByteOpts.jsonParse);
  functionOverload("bytes.hex(): string", ByteOpts.toHex);
  functionOverload("bytes.string(): string", ByteOpts.toUtf8);
  functionOverload("bytes.base64(): string", ByteOpts.toBase64);
  functionOverload("bytes.at(number): number", (b, index) => {
    const idx = toNum(index);
    const buf = b;
    if (idx < 0 || idx >= buf.length) {
      throw evaluationError("index_out_of_range", "Bytes index out of range");
    }
    return Decimal.from(buf[idx]);
  });
  function wrapTZ(result, source) {
    if (result instanceof TZDate) return result;
    return new TZDate(result.getTime(), source.timeZone);
  }
  registry.registerType("Date", TZDate);
  functionOverload("date(string): Date", (v) => {
    const d = new TZDate(v);
    if (Number.isNaN(d.getTime())) throw evaluationError("invalid_date", `date() requires a valid date string, got "${v}"`);
    return d;
  });
  functionOverload("date(number): Date", (v) => {
    const n = toNum(v);
    if (!Number.isFinite(n)) {
      throw evaluationError("invalid_date", `date() requires a finite timestamp, got ${v}`);
    }
    const ms = Math.abs(n) < 1e11 ? n * 1e3 : n;
    const d = new TZDate(ms);
    if (Number.isNaN(d.getTime())) {
      throw evaluationError("invalid_date", `date() got an out-of-range timestamp: ${v}`);
    }
    return d;
  });
  const RANGE_LIMIT = 1e5;
  function numericRange(startD, endD, stepD) {
    const step = stepD.toNumber();
    if (!(step > 0)) {
      throw evaluationError("invalid_range", `range(): step must be greater than 0, got ${step}`);
    }
    const end = endD.toNumber();
    const ascending = startD.toNumber() < end;
    const delta = ascending ? stepD : stepD.neg();
    const out = [];
    let cur = startD;
    while (ascending ? cur.toNumber() < end : cur.toNumber() > end) {
      out.push(cur);
      if (out.length > RANGE_LIMIT) {
        throw evaluationError("range_too_large", `range(): exceeded the ${RANGE_LIMIT}-item limit`);
      }
      cur = cur.add(delta);
    }
    return out;
  }
  functionOverload("range(number, number): list<dyn>", (start, end) => numericRange(start, end, Decimal.from(1)));
  functionOverload("range(number, number, number): list<dyn>", (start, end, step) => numericRange(start, end, step));
  const rangeSteppers = {
    year: addYears,
    month: addMonths,
    day: addDays,
    hour: addHours,
    minute: addMinutes
  };
  functionOverload("range(Date, Date, string): list<dyn>", (startV, endV, unitV) => {
    const start = startV;
    const stepFn = rangeSteppers[unitV];
    if (!stepFn) {
      throw evaluationError("invalid_unit", `range(): unknown unit "${unitV}" (expected year / month / day / hour / minute)`);
    }
    const endMs = endV.getTime();
    const ascending = start.getTime() < endMs;
    const out = [];
    let cur = start;
    while (ascending ? cur.getTime() < endMs : cur.getTime() > endMs) {
      out.push(cur);
      if (out.length > RANGE_LIMIT) {
        throw evaluationError("range_too_large", `range(): exceeded the ${RANGE_LIMIT}-item limit`);
      }
      cur = wrapTZ(stepFn(cur, ascending ? 1 : -1), start);
    }
    return out;
  });
  functionOverload("Date.startOf(string): Date", (d, unit) => {
    const date = d;
    switch (unit) {
      case "day":
        return wrapTZ(startOfDay(date), date);
      case "week":
        return wrapTZ(startOfWeek(date), date);
      case "month":
        return wrapTZ(startOfMonth(date), date);
      case "year":
        return wrapTZ(startOfYear(date), date);
      default:
        throw evaluationError("invalid_unit", `startOf(): unknown unit "${unit}"`);
    }
  });
  functionOverload("Date.endOf(string): Date", (d, unit) => {
    const date = d;
    switch (unit) {
      case "day":
        return wrapTZ(endOfDay(date), date);
      case "week":
        return wrapTZ(endOfWeek(date), date);
      case "month":
        return wrapTZ(endOfMonth(date), date);
      case "year":
        return wrapTZ(endOfYear(date), date);
      default:
        throw evaluationError("invalid_unit", `endOf(): unknown unit "${unit}"`);
    }
  });
  functionOverload("Date.offset(number, string): Date", (d, n, unit) => {
    const date = d;
    const num = toNum(n);
    switch (unit.replace(/s$/, "")) {
      case "day":
        return wrapTZ(addDays(date, num), date);
      case "week":
        return wrapTZ(addWeeks(date, num), date);
      case "month":
        return wrapTZ(addMonths(date, num), date);
      case "year":
        return wrapTZ(addYears(date, num), date);
      default:
        throw evaluationError("invalid_unit", `offset(): unknown unit "${unit}"`);
    }
  });
  functionOverload("Date.set(string, number): Date", (d, field, value) => {
    const date = d;
    const val = toNum(value);
    switch (field) {
      case "day":
        return wrapTZ(setDayOfMonth(date, val), date);
      case "month":
        return wrapTZ(setMonth(date, val - 1), date);
      case "year":
        return wrapTZ(setYear(date, val), date);
      default:
        throw evaluationError("invalid_field", `set(): unknown field "${field}"`);
    }
  });
  functionOverload("Date.format(string): string", (d, fmt) => formatDate(d, fmt));
  registry.registerType("File", File);
  registry.registerType("URL", URL);
  functionOverload("URL.query(string): optional<string>", (url, key) => optionalOf(url.searchParams.get(key) ?? void 0));
  registerOptional(registry);
}
function stringSize(str) {
  let count = 0;
  for (const _ of str) count++;
  return count;
}
