///|
/// The `loop` variable: where an iteration stands, its neighbours, and the
/// `cycle` and `changed` helpers; in a recursive loop, a callable that
/// renders the body again one level deeper.
priv struct Loop {
  items : Array[@spec.Value]
  mut index0 : Int32
  depth0 : Int32
  mut last_changed : Array[@spec.Value]?
  again : ((Array[@spec.Value], Map[String, @spec.Value]) -> @spec.Value raise @spec.Refused)?
}

///|
impl @spec.Object for Loop with fn kind(_) {
  "the loop"
}

///|
impl @spec.Object for Loop with fn attr(self, name) {
  let n = self.items.length()
  let i = self.index0
  match name {
    "index0" => Some(Int(i.to_int64()))
    "index" => Some(Int((i + 1).to_int64()))
    "revindex0" => Some(Int((n - i - 1).to_int64()))
    "revindex" => Some(Int((n - i).to_int64()))
    "first" => Some(Bool(i == 0))
    "last" => Some(Bool(i == n - 1))
    "length" => Some(Int(n.to_int64()))
    "depth0" => Some(Int(self.depth0.to_int64()))
    "depth" => Some(Int((self.depth0 + 1).to_int64()))
    "previtem" =>
      Some(
        if i > 0 {
          self.items[i - 1]
        } else {
          Undefined("there is no previous item")
        },
      )
    "nextitem" =>
      Some(
        if i + 1 < n {
          self.items[i + 1]
        } else {
          Undefined("there is no next item")
        },
      )
    "cycle" =>
      Some(
        @spec.Value::func("cycle", (_, args, _) => {
          if args.is_empty() {
            raise @spec.invalid("no items for cycling given")
          }
          args[self.index0 % args.length()]
        }),
      )
    "changed" =>
      Some(
        @spec.Value::func("changed", (_, args, _) => {
          let different = match self.last_changed {
            Some(before) => before != args
            None => true
          }
          if different {
            self.last_changed = Some(args.copy())
          }
          Bool(different)
        }),
      )
    _ => None
  }
}

///|
impl @spec.Object for Loop with fn callable(self) {
  self.again is Some(_)
}

///|
impl @spec.Object for Loop with fn call(self, _, args, named) {
  match self.again {
    Some(run) => run(args, named)
    None =>
      raise @spec.invalid("the loop is not recursive, so it cannot be called")
  }
}

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

///|
/// The items a loop walks. Jinja's loop walks a mapping's keys; the
/// indentation syntax's `each` walks its values, or value and key with two
/// names, and value and index over a list.
fn Run::loop_items(
  self : Run,
  target : @syntax.Target,
  iterable : @spec.Value,
  each : Bool,
) -> Array[@spec.Value] raise @spec.Refused {
  let pairs = each && target is Tuple([_, _])
  match (each, iterable) {
    (true, Dict(fields)) =>
      if pairs {
        fields.iter().map(p => @spec.Value::Tuple([p.1, Text(p.0)])).collect()
      } else {
        fields.values().collect()
      }
    (true, _) if pairs => {
      let items = self.call.items(iterable)
      items.mapi((i, v) => @spec.Value::Tuple([v, Int(i.to_int64())]))
    }
    _ => self.call.items(iterable)
  }
}

///|
fn Run::run_for(
  self : Run,
  target : @syntax.Target,
  iterable : @spec.Value,
  cond : @syntax.Expr?,
  recursive : Bool,
  each : Bool,
  body : Array[@syntax.Node],
  otherwise : Array[@syntax.Node],
  depth0 : Int32,
) -> Unit raise @spec.Refused {
  let all = self.loop_items(target, iterable, each)
  let items = match cond {
    None => all
    Some(check) => {
      let kept = []
      for item in all {
        let keep = Ref(false)
        self.scoped(Map([]), () => {
          self.assign(target, item)
          keep.val = self.call.truthy(self.eval(check))
        })
        if keep.val {
          kept.push(item)
        }
      }
      kept
    }
  }
  if items.is_empty() {
    self.run_nodes(otherwise)
    return
  }
  let again = if recursive {
    Some(fn(
      args : Array[@spec.Value],
      _named : Map[String, @spec.Value],
    ) raise @spec.Refused {
      guard args is [inner] else {
        raise @spec.invalid("a recursive loop takes one iterable")
      }
      self.enter()
      defer self.leave()
      let text = self.capture(() => {
        self.run_for(target, inner, cond, recursive, each, body, [], depth0 + 1)
      })
      self.rendered(text)
    })
  } else {
    None
  }
  let state = Loop::{ items, index0: 0, depth0, last_changed: None, again, }
  for i, item in items {
    state.index0 = i
    self.scoped(Map([]), () => {
      self.assign(target, item)
      self.bind("loop", Object(state))
      self.run_nodes(body)
    })
    if self.braking {
      self.braking = false
      break
    }
    self.skipping = false
  }
}