///|
/// A macro, a mixin, or the caller of a call block: its parameters, body,
/// and the scopes its names resolve in.
priv struct Macro {
  run : Run
  name : String
  params : Array[@syntax.Param]
  body : Array[@syntax.Node]
  home : Array[Map[String, @spec.Value]]
  template : String
  pug : Bool
  caller : Bool?
  varargs : Bool
  kwargs : Bool
}

///|
impl @spec.Object for Macro with fn kind(self) {
  "the macro \{self.name}"
}

///|
impl @spec.Object for Macro with fn attr(self, name) {
  match name {
    "name" => Some(Text(self.name))
    "arguments" =>
      Some(
        Tuple(
          self.params.filter(p => !p.rest).map(p => @spec.Value::Text(p.name)),
        ),
      )
    "catch_varargs" => Some(Bool(self.varargs))
    "catch_kwargs" => Some(Bool(self.kwargs))
    "caller" => Some(Bool(self.caller.unwrap_or(false)))
    _ => None
  }
}

///|
impl @spec.Object for Macro with fn callable(_) {
  true
}

///|
impl @spec.Object for Macro with fn show(self) {
  ""
}

///|
impl @spec.Object for Macro with fn call(self, _, args, named) {
  self.run.invoke_macro(self, args, named)
}

///|
/// Whether a name is read anywhere in the nodes; macros take `varargs`,
/// `kwargs` and `caller` only when their body reads them.
fn reads(nodes : Array[@syntax.Node], name : String) -> Bool {
  let found = Ref(false)
  fn expr(e : @syntax.Expr) -> Unit {
    if found.val {
      return
    }
    match e {
      Name(n) => if n == name { found.val = true }
      Lit(_) => ()
      List(xs) | Tuple(xs) | Concat(xs) => xs.each(expr)
      Dict(ps) =>
        ps.each(p => {
          expr(p.0)
          expr(p.1)
        })
      Attr(t, _) | Not(t) | Neg(t) | Pos(t) => expr(t)
      Item(t, k) => {
        expr(t)
        expr(k)
      }
      Slice(a, b, c) => [a, b, c].each(o => if o is Some(x) { expr(x) })
      Call(t, args) | Test(t, _, args) => {
        expr(t)
        args.each(a => {
          match a {
            Pos(e) | Named(_, e) | Spread(e) | SpreadNamed(e) => expr(e)
          }
        })
      }
      Filter(t, _, args) => {
        if t is Some(x) {
          expr(x)
        }
        args.each(a => {
          match a {
            Pos(e) | Named(_, e) | Spread(e) | SpreadNamed(e) => expr(e)
          }
        })
      }
      Binary(_, l, r) => {
        expr(l)
        expr(r)
      }
      Compare(f, ops) => {
        expr(f)
        ops.each(p => expr(p.1))
      }
      Cond(a, b, c) => {
        expr(a)
        expr(b)
        if c is Some(x) {
          expr(x)
        }
      }
      Attributes(attrs, splats) => {
        attrs.each(a => if a.value is Some(x) { expr(x) })
        splats.each(expr)
      }
    }
  }
  fn walk(ns : Array[@syntax.Node]) -> Unit {
    for n in ns {
      if found.val {
        return
      }
      match n {
        Out(e) | Raw(e) | Extends(e) | Do(e) => expr(e)
        For(iter~, cond~, body~, otherwise~, ..) => {
          expr(iter)
          if cond is Some(c) {
            expr(c)
          }
          walk(body)
          walk(otherwise)
        }
        If(bs, o) => {
          bs.each(b => {
            expr(b.0)
            walk(b.1)
          })
          walk(o)
        }
        Set(_, e) => expr(e)
        SetBlock(_, f, b) => {
          if f is Some(x) {
            expr(x)
          }
          walk(b)
        }
        CallBlock(_, c, b) => {
          expr(c)
          walk(b)
        }
        FilterBlock(c, b) => {
          expr(c)
          walk(b)
        }
        BlockDef(b) => walk(b.body)
        Include(e, block~, ..) => {
          expr(e)
          walk(block)
        }
        Import(e, _, ..) | FromImport(e, _, ..) => expr(e)
        With(ps, b) => {
          ps.each(p => expr(p.1))
          walk(b)
        }
        Autoescape(e, b) | Show(e, b) => {
          expr(e)
          walk(b)
        }
        Once(k, b) => {
          if k is Some(x) {
            expr(x)
          }
          walk(b)
        }
        TransOut(t) => t.vars.each(v => expr(v.1))
        Element(el) => {
          expr(el.tag)
          el.attrs.each(a => if a.value is Some(x) { expr(x) })
          el.splats.each(expr)
          walk(el.body)
        }
        Comment(b) => walk(b)
        Case(s, arms, o) => {
          expr(s)
          arms.each(a => {
            a.0.each(expr)
            walk(a.1)
          })
          walk(o)
        }
        MacroDef(_)
        | Line(_)
        | Text(_)
        | Debug
        | Break
        | Continue
        | Doctype(_)
        | Yield => ()
      }
    }
  }
  walk(nodes)
  found.val
}

///|
fn Run::make_macro(self : Run, m : @syntax.Macro) -> @spec.Value {
  Object(Macro::{
    run: self,
    name: m.name,
    params: m.params,
    body: m.body,
    home: self.scopes.copy(),
    template: self.template,
    pug: m.pug,
    caller: Some(reads(m.body, "caller")),
    varargs: reads(m.body, "varargs"),
    kwargs: reads(m.body, "kwargs"),
  })
}

///|
/// Call a macro: parameters from the positional arguments, then by name,
/// then their defaults; a parameter given none of these is undefined.
fn Run::invoke_macro(
  self : Run,
  m : Macro,
  args : Array[@spec.Value],
  named : Map[String, @spec.Value],
) -> @spec.Value raise @spec.Refused {
  self.enter()
  defer self.leave()
  let scope : Map[String, @spec.Value] = Map([])
  let rest_named = named.copy()
  let takes_caller = m.caller.unwrap_or(false)
  let caller = rest_named.get("caller")
  if takes_caller || m.pug {
    rest_named.remove("caller")
  }
  let attributes = if m.pug { rest_named.get("attributes") } else { None }
  if m.pug {
    rest_named.remove("attributes")
  }
  let saved_scopes = self.scopes
  let saved_template = self.template
  self.scopes = [..m.home, scope]
  self.template = m.template
  defer {
    self.scopes = saved_scopes
    self.template = saved_template
  }
  let mut used = 0
  for i, p in m.params {
    if p.rest {
      scope[p.name] = Seq(
        if used < args.length() {
          args[used:].to_owned()
        } else {
          []
        },
      )
      used = args.length()
      continue
    }
    if i < args.length() {
      if rest_named.contains(p.name) {
        raise @spec.invalid(
          "macro \{@spec.repr_text(m.name)} got multiple values for argument \{@spec.repr_text(p.name)}",
        )
      }
      scope[p.name] = args[i]
      used = i + 1
    } else if rest_named.contains(p.name) {
      scope[p.name] = rest_named.get(p.name).unwrap()
      rest_named.remove(p.name)
    } else {
      scope[p.name] = match p.default {
        Some(e) => self.eval(e)
        None => Undefined(p.name)
      }
    }
  }
  for p in m.params {
    rest_named.remove(p.name)
  }
  if used < args.length() {
    if m.varargs {
      scope["varargs"] = Tuple(args[used:].to_owned())
    } else {
      raise @spec.invalid(
        "macro \{@spec.repr_text(m.name)} takes not more than \{m.params.length()} argument(s)",
      )
    }
  } else if m.varargs {
    scope["varargs"] = Tuple([])
  }
  if !rest_named.is_empty() {
    if m.kwargs {
      scope["kwargs"] = Dict(rest_named)
    } else {
      let first = rest_named.keys().collect()[0]
      raise @spec.invalid(
        "macro \{@spec.repr_text(m.name)} takes no keyword argument \{@spec.repr_text(first)}",
      )
    }
  } else if m.kwargs {
    scope["kwargs"] = Dict(Map([]))
  }
  if takes_caller {
    scope["caller"] = caller.unwrap_or(Undefined("caller"))
  }
  if m.pug {
    scope["attributes"] = attributes.unwrap_or(Dict(Map([])))
    scope["block"] = caller.unwrap_or(Bool(false))
  }
  let text = self.capture(() => self.run_nodes(m.body))
  self.rendered(text)
}