import { Effect, Option } from "effect";
import { evaluationError, parseError } from "./errors.js";
import { OPERATORS as OPS } from "./operators.js";
import { optionalOf } from "./optional.js";
const identity = (x) => x;
function assertIdentifier(node, message) {
  if (node.op === "id") return node.args;
  throw parseError("invalid_macro_argument", message, node);
}
function buildMapComprehension(callAst, receiver, iterVar, predicate, transform, invalidMsg, label) {
  let step = transform.clone(OPS.accuPush, transform);
  if (predicate) {
    const accuValue = predicate.clone(OPS.accuValue, void 0);
    step = predicate.clone(OPS.ternary, [predicate.setMeta("label", label), step, accuValue]);
  }
  return callAst.clone(OPS.comprehension, {
    errorsAreFatal: true,
    iterable: receiver,
    iterVarName: assertIdentifier(iterVar, invalidMsg),
    init: callAst.clone(OPS.list, []),
    step,
    result: identity
  });
}
function createMapExpander(hasFilter) {
  const functionDesc = hasFilter ? "map(var, filter, transform)" : "map(var, transform)";
  const invalidMsg = `${functionDesc} invalid predicate iteration variable`;
  const label = `${functionDesc} filter predicate must return bool`;
  if (hasFilter) {
    return (opts) => {
      const { args, receiver, ast: callAst } = opts;
      return { callAst: buildMapComprehension(callAst, receiver, args[0], args[1], args[2], invalidMsg, label) };
    };
  }
  function functorMap(ev, macro, ctx, recv) {
    return Effect.gen(function* () {
      if (Option.isNone(recv)) return Option.none();
      const iterCtx = macro.iterCtx.reuse(ctx);
      const result = yield* ev.eval(macro.body, iterCtx.setIterValue(recv.value, ev));
      return Option.some(result);
    });
  }
  function typeCheck(chk, macro, ctx) {
    const recvType = chk.check(macro.receiver, ctx);
    if (recvType.kind === "optional") {
      macro.mode = "optional";
      const innerType = recvType.valueType ?? chk.getType("dyn");
      macro.iterCtx = ctx.forkWithVariable(macro.iterVarName, innerType);
      const bodyType = chk.check(macro.body, macro.iterCtx);
      return chk.registry.getOptionalType(bodyType);
    }
    if (recvType.kind === "dyn") {
      macro.mode = "dyn";
      macro.iterCtx = ctx.forkWithVariable(macro.iterVarName, chk.getType("dyn"));
      chk.check(macro.comprehension, ctx);
      return chk.getType("dyn");
    }
    macro.mode = "list";
    return chk.check(macro.comprehension, ctx);
  }
  function evaluate(ev, macro, ctx) {
    if (macro.mode === "list") return ev.eval(macro.comprehension, ctx);
    if (macro.mode === "optional") {
      return Effect.flatMap(ev.eval(macro.receiver, ctx), (recv) => functorMap(ev, macro, ctx, recv));
    }
    return Effect.flatMap(
      ev.eval(macro.receiver, ctx),
      (recv) => Option.isOption(recv) ? functorMap(ev, macro, ctx, recv) : ev.eval(macro.comprehension, ctx)
    );
  }
  return (opts) => {
    const { args, receiver, ast: callAst } = opts;
    return {
      ast: callAst,
      receiver,
      iterVarName: assertIdentifier(args[0], invalidMsg),
      body: args[1],
      comprehension: buildMapComprehension(callAst, receiver, args[0], null, args[1], invalidMsg, label),
      iterCtx: void 0,
      mode: "list",
      typeCheck,
      evaluate
    };
  };
}
function createFilterExpander() {
  const functionDesc = "filter(var, predicate)";
  const invalidMsg = `${functionDesc} invalid predicate iteration variable`;
  const label = `${functionDesc} predicate must return bool`;
  return (opts) => {
    const { args, receiver, ast: callAst } = opts;
    const iterVarName = assertIdentifier(args[0], invalidMsg);
    const accuValue = callAst.clone(OPS.accuValue, void 0);
    const predicate = args[1].setMeta("label", label);
    const appendItem = callAst.clone(OPS.accuPush, callAst.clone(OPS.id, iterVarName));
    const step = predicate.clone(OPS.ternary, [predicate, appendItem, accuValue]);
    return {
      callAst: callAst.clone(OPS.comprehension, {
        errorsAreFatal: true,
        iterable: receiver,
        iterVarName,
        init: callAst.clone(OPS.list, []),
        step,
        result: identity
      })
    };
  };
}
function createQuantifierExpander(opts) {
  const invalidMsg = `${opts.name}(var, predicate) invalid predicate iteration variable`;
  const label = `${opts.name}(var, predicate) predicate must return bool`;
  return (callOpts) => {
    const { args, receiver, ast: callAst } = callOpts;
    const predicate = args[1].setMeta("label", label);
    const transform = opts.transform({ args, ast: callAst, predicate, opts });
    return {
      callAst: callAst.clone(OPS.comprehension, {
        kind: "quantifier",
        errorsAreFatal: opts.errorsAreFatal || false,
        iterable: receiver,
        iterVarName: assertIdentifier(args[0], invalidMsg),
        init: transform.init,
        condition: transform.condition,
        step: transform.step,
        result: transform.result || identity
      })
    };
  };
}
function createFindExpander() {
  const functionDesc = "find(var, predicate)";
  const invalidMsg = `${functionDesc} invalid iteration variable`;
  const label = `${functionDesc} predicate must return bool`;
  function findElementType(chk, receiver, ctx) {
    const iterType = chk.check(receiver, ctx);
    if (iterType.kind === "dyn") return iterType;
    if (iterType.kind === "list") return iterType.valueType;
    if (iterType.kind === "map") return iterType.keyType;
    throw chk.createError(
      "invalid_comprehension_range",
      `Expression of type '${iterType}' cannot be range of find (must be list, map, or dynamic).`,
      receiver
    );
  }
  function typeCheck(chk, macro, ctx) {
    const elemType = findElementType(chk, macro.receiver, ctx);
    macro.iterCtx = ctx.forkWithVariable(macro.iterVarName, elemType);
    chk.check(macro.predicate, macro.iterCtx);
    return chk.registry.getOptionalType(elemType);
  }
  function evaluate(ev, macro, ctx) {
    return Effect.gen(function* () {
      let items = yield* ev.eval(macro.receiver, ctx);
      if (!Array.isArray(items)) {
        if (items instanceof Set) items = [...items];
        else if (items instanceof Map) items = [...items.keys()];
        else if (items && typeof items === "object") items = Object.keys(items);
        else {
          return yield* Effect.fail(
            ev.createError(
              "invalid_comprehension_range",
              `Expression of type '${ev.debugType(items)}' cannot be range of find.`,
              macro.receiver
            )
          );
        }
      }
      const iterCtx = macro.iterCtx.reuse(ctx);
      const arr = items;
      for (let i = 0; i < arr.length; i++) {
        const result = yield* ev.eval(macro.predicate, iterCtx.setIterValue(arr[i], ev));
        if (result) return optionalOf(arr[i]);
      }
      return Option.none();
    });
  }
  return (opts) => {
    const { args, receiver, ast } = opts;
    return {
      ast,
      receiver,
      iterVarName: assertIdentifier(args[0], invalidMsg),
      predicate: args[1].setMeta("label", label),
      iterCtx: void 0,
      evaluate,
      typeCheck
    };
  };
}
function createHasExpander() {
  const invalidHasArgument = "has() invalid argument";
  function evaluate(ev, macro, ctx) {
    return Effect.gen(function* () {
      const nodes = macro.macroHasProps;
      let i = nodes.length;
      let obj = yield* ev.eval(nodes[--i], ctx);
      let inOptionalContext;
      while (i--) {
        const node = nodes[i];
        if (node.op === ".?") inOptionalContext ??= true;
        obj = ev.debugType(obj).fieldLazy(obj, node.args[1], node, ev);
        if (obj !== void 0) continue;
        if (!(!inOptionalContext && i && node.op === ".")) break;
        throw evaluationError("no_such_key", `No such key: ${node.args[1]}`, node);
      }
      return obj !== void 0;
    });
  }
  function typeCheck(checker, macro, ctx) {
    let node = macro.args[0];
    if (node.op !== ".") throw checker.createError("invalid_macro_argument", invalidHasArgument, node);
    if (!macro.macroHasProps) {
      const props = [];
      while (node.op === "." || node.op === ".?") node = (props.push(node), node.args[0]);
      if (node.op !== "id") throw checker.createError("invalid_macro_argument", invalidHasArgument, node);
      checker.check(node, ctx);
      props.push(node);
      macro.macroHasProps = props;
    }
    return checker.getType("bool");
  }
  return function(opts) {
    const { args } = opts;
    return { args, evaluate, typeCheck };
  };
}
export function registerMacros(registry) {
  const functionOverload = (sig, handler) => registry.registerFunctionOverload(sig, handler);
  functionOverload("has(ast): bool", createHasExpander());
  functionOverload(
    "list.all(ast, ast): bool",
    createQuantifierExpander({
      name: "all",
      transform({ ast: callAst, predicate }) {
        return {
          init: callAst.clone(OPS.value, true),
          condition: identity,
          step: predicate.clone(OPS.ternary, [
            predicate,
            predicate.clone(OPS.value, true),
            predicate.clone(OPS.value, false)
          ])
        };
      }
    })
  );
  functionOverload(
    "list.exists(ast, ast): bool",
    createQuantifierExpander({
      name: "exists",
      condition(accu) {
        return !accu;
      },
      transform({ ast: callAst, predicate, opts }) {
        return {
          init: callAst.clone(OPS.value, false),
          condition: opts.condition,
          step: predicate.clone(OPS.ternary, [
            predicate,
            predicate.clone(OPS.value, true),
            predicate.clone(OPS.value, false)
          ])
        };
      }
    })
  );
  functionOverload(
    "list.exists_one(ast, ast): bool",
    createQuantifierExpander({
      name: "exists_one",
      errorsAreFatal: true,
      result(accu) {
        return accu === 1;
      },
      transform({ ast: callAst, predicate, opts }) {
        const accuValue = callAst.clone(OPS.accuValue, void 0);
        return {
          init: callAst.clone(OPS.value, 0),
          step: predicate.clone(OPS.ternary, [predicate, callAst.clone(OPS.accuInc, void 0), accuValue]),
          result: opts.result
        };
      }
    })
  );
  functionOverload("list.map(ast, ast): list<dyn>", createMapExpander(false));
  functionOverload("list.map(ast, ast, ast): list<dyn>", createMapExpander(true));
  functionOverload("list.filter(ast, ast): list<dyn>", createFilterExpander());
  functionOverload("list.find(ast, ast): optional<dyn>", createFindExpander());
  class CelNamespace {
    #kind = "cel";
    toString() {
      return this.#kind;
    }
  }
  const celNamespace = new CelNamespace();
  registry.registerType("CelNamespace", CelNamespace);
  registry.registerConstant("cel", "CelNamespace", celNamespace);
  function bindTypeCheck(checker, m, ctx) {
    m.bindCtx = ctx.forkWithVariable(m.var, checker.check(m.val, ctx));
    return checker.check(m.exp, m.bindCtx);
  }
  function bindEvaluate(ev, { val, exp, bindCtx }, ctx) {
    return Effect.gen(function* () {
      const v = yield* ev.eval(val, ctx);
      return yield* ev.eval(exp, bindCtx.reuse(ctx).setIterValue(v, ev));
    });
  }
  functionOverload("CelNamespace.bind(ast, dyn, ast): dyn", (opts) => {
    const { ast, args } = opts;
    return {
      ast,
      var: assertIdentifier(args[0], "invalid variable argument"),
      val: args[1],
      exp: args[2],
      bindCtx: void 0,
      typeCheck: bindTypeCheck,
      evaluate: bindEvaluate
    };
  });
}
