///|
priv enum RenderFlow {
  Next
  Break
  Continue
} derive(Eq)

///|
priv struct RenderState {
  mut flow : RenderFlow
  mut loops : Int
  mut depth : Int
  counters : Map[String, Int]
  cycles : Map[String, Int]
  mut previous : String?
  diagnostics : Array[Diagnostic]
  mut template_name : String?
  partials : Map[String, Array[RenderNode]]
}

///|
fn new_render_state() -> RenderState {
  {
    flow: Next,
    loops: 0,
    depth: 0,
    counters: Map([]),
    cycles: Map([]),
    previous: None,
    diagnostics: [],
    template_name: None,
    partials: Map([]),
  }
}

///|
fn restore_variable(
  context : LiquidContext,
  key : String,
  value : LiquidValue?,
) -> Unit {
  match value {
    Some(value) => context.set(key, value)
    None => context.variables.remove(key)
  }
}

///|
/// Register a caller-supplied template for include/render. No filesystem access is performed.
pub fn LiquidContext::register_template(
  self : LiquidContext,
  name : String,
  source : String,
) -> Unit {
  self.templates[name] = source
}

///|
fn render_partial(
  call : PartialCall,
  context : LiquidContext,
  state : RenderState,
  isolated : Bool,
) -> String {
  let name = call.name
  guard context.templates.get(name) is Some(source) else {
    return render_error(
      state,
      "Template '" + name + "' not found",
      code="missing_template",
    )
  }
  if state.depth >= 64 {
    return render_error(
      state,
      "Template nesting limit exceeded",
      code="nesting_limit",
    )
  }
  let child = if isolated {
    { variables: Map([]), templates: context.templates, }
  } else {
    context
  }
  let saved : Map[String, LiquidValue?] = Map([])
  // Evaluate all arguments in the caller before binding any of them.
  let arguments = call.arguments.map(fn(arg) {
    (arg.0, arg.1.eval(context, state))
  })
  for (key, value) in arguments {
    if !saved.contains(key) {
      saved[key] = child.variables.get(key)
    }
    child.set(key, value)
  }
  let child_state = if isolated {
    {
      ..new_render_state(),
      depth: state.depth + 1,
      diagnostics: state.diagnostics,
      partials: state.partials,
    }
  } else {
    state
  }
  if !isolated {
    state.depth += 1
  }
  let previous_template = child_state.template_name
  child_state.template_name = Some(name)
  let nodes = match state.partials.get(source) {
    Some(nodes) => nodes
    None => {
      let diagnostics : Array[Diagnostic] = []
      let nodes = parse_template(source, diagnostics~)
      for diagnostic in diagnostics {
        state.diagnostics.push({ ..diagnostic, template: Some(name), })
      }
      state.partials[source] = nodes
      nodes
    }
  }
  let mut output = ""
  for node in nodes {
    output += render_node_inner(node, child, child_state)
  }
  child_state.template_name = previous_template
  if !isolated {
    state.depth -= 1
  }
  for key, value in saved {
    restore_variable(child, key, value)
  }
  output
}

///|
fn render_nodes(
  nodes : Array[RenderNode],
  context : LiquidContext,
  state : RenderState,
) -> String {
  let mut output = ""
  for node in nodes {
    output += render_node_inner(node, context, state)
  }
  output
}

///|
fn render_for(
  loop_var : String,
  collection : Expr,
  body : Array[RenderNode],
  modifiers : LoopModifiers,
  otherwise : Array[RenderNode],
  context : LiquidContext,
  state : RenderState,
) -> String {
  match collection.eval(context, state) {
    Array(items) => {
      // Apply modifiers to create the actual items to iterate
      let mut processed_items = items

      // Apply offset
      match modifiers.offset {
        Some(offset_count) => {
          let offset_items : Array[LiquidValue] = []
          for i in offset_count.max(0).. ()
      }

      // Apply limit
      match modifiers.limit {
        Some(limit_count) => {
          let limited_items : Array[LiquidValue] = []
          let max_items = if processed_items.length() > limit_count {
            limit_count
          } else {
            processed_items.length()
          }
          for i in 0.. ()
      }

      // Apply reversed
      if modifiers.reversed {
        let reversed_items : Array[LiquidValue] = []
        let mut i = processed_items.length() - 1
        while i >= 0 {
          reversed_items.push(processed_items[i])
          i = i - 1
        }
        processed_items = reversed_items
      }
      if processed_items.is_empty() {
        return render_nodes(otherwise, context, state)
      }
      let mut output = ""
      let mut index = 0
      let saved_item = context.variables.get(loop_var)
      let saved_loop = context.variables.get("forloop")
      state.loops += 1
      for item in processed_items {
        let loop_context = context
        loop_context.set(loop_var, item)

        // Set enhanced forloop object properties (based on processed items)
        let forloop_obj = Map([])
        forloop_obj.set("index", @value.number_value((index + 1).to_double()))
        forloop_obj.set("index0", @value.number_value(index.to_double()))
        forloop_obj.set("first", @value.bool_value(index == 0))
        forloop_obj.set(
          "last",
          @value.bool_value(index == processed_items.length() - 1),
        )
        forloop_obj.set(
          "length",
          @value.number_value(processed_items.length().to_double()),
        )
        forloop_obj.set(
          "rindex",
          @value.number_value((processed_items.length() - index).to_double()),
        )
        forloop_obj.set(
          "rindex0",
          @value.number_value(
            (processed_items.length() - index - 1).to_double(),
          ),
        )

        // Enhanced forloop properties
        forloop_obj.set("parentloop", saved_loop.unwrap_or(@value.null_value())) // Parent loop (for nested loops)
        forloop_obj.set("name", @value.string_value(collection.label())) // Collection name

        // Add cycle functionality to forloop
        let cycle_odd_even = if index % 2 == 0 { "even" } else { "odd" }
        forloop_obj.set("cycle", @value.string_value(cycle_odd_even))
        loop_context.set("forloop", @value.object_value(forloop_obj))
        for body_node in body {
          output = output + render_node_inner(body_node, loop_context, state)
        }
        let flow = state.flow
        state.flow = Next
        if flow == Break {
          break
        }
        index = index + 1
      }
      state.loops -= 1
      restore_variable(context, loop_var, saved_item)
      restore_variable(context, "forloop", saved_loop)
      output
    }
    _ => render_nodes(otherwise, context, state)
  }
}

///|
fn render_error(
  state : RenderState,
  message : String,
  code? : String = "evaluation_error",
) -> String {
  report_diagnostic(state, filter_diagnostic(code, message))
}

///|
fn report_diagnostic(state : RenderState, diagnostic : Diagnostic) -> String {
  state.diagnostics.push({ ..diagnostic, template: state.template_name, })
  ""
}