///|
/// Output byte ranges include the punctuation emitted for that variable.
/// One entry per literal or variable occurrence, not per collection member.
pub(all) struct OutputOrigin {
  name : String?
  source : Span
  output_start : Int32
  output_end : Int32
} derive(Debug)

///|
pub(all) struct Expansion {
  output : String
  bytes : Int32
  origins : Array[OutputOrigin]
} derive(Debug)

///|
/// Count encoded bytes using the very same emitter, without building URI text.
pub fn Template::measure(
  self : Template,
  variables : Variables,
  required~ : Array[String] = [],
  reject_unused~ : Bool = false,
  limits~ : ExpansionLimits = ExpansionLimits::new(),
) -> Result[Int32, TemplateError] {
  match self.validate_bindings(variables, required, reject_unused, limits) {
    Err(e) => Err(e)
    Ok(_) =>
      self.render_impl(variables, Some(limits), false, false).map(r => r.bytes)
  }
}

///|
pub fn Template::expand_mapped(
  self : Template,
  variables : Variables,
  required~ : Array[String] = [],
  reject_unused~ : Bool = false,
  limits~ : ExpansionLimits = ExpansionLimits::new(),
) -> Result[Expansion, TemplateError] {
  match self.validate_bindings(variables, required, reject_unused, limits) {
    Err(e) => Err(e)
    Ok(_) => self.render_impl(variables, Some(limits), true, true)
  }
}

///|
pub(all) struct BatchLimits {
  max_items : Int32
  max_total_input_codepoints : Int32
  max_total_output_bytes : Int32
} derive(Debug)

///|
pub fn BatchLimits::new(
  max_items~ : Int32 = 256,
  max_total_input_codepoints~ : Int32 = 4194304,
  max_total_output_bytes~ : Int32 = 4194304,
) -> BatchLimits {
  { max_items, max_total_input_codepoints, max_total_output_bytes, }
}

///|
/// index=-1 denotes a batch-level error before visiting any item.
pub(all) struct BatchError {
  index : Int32
  error : TemplateError
} derive(Debug)

///|
fn input_texts(vars : Variables) -> Array[String] {
  let texts : Array[String] = []
  for name, value in vars.values {
    texts.push(name)
    match value {
      Scalar(s) => texts.push(s)
      List(xs) => texts.push_iter(xs.iter())
      Assoc(xs) =>
        for (k, v) in xs.entries {
          texts.push(k)
          texts.push(v)
        }
    }
  }
  texts
}

///|
/// Atomic: validate and measure every item before allocating any result URI.
/// Errors include the zero-based failing item. No partial array is returned.
pub fn Template::expand_batch(
  self : Template,
  inputs : Array[Variables],
  required~ : Array[String] = [],
  reject_unused~ : Bool = false,
  limits~ : ExpansionLimits = ExpansionLimits::new(),
  batch_limits~ : BatchLimits = BatchLimits::new(),
) -> Result[Array[String], BatchError] {
  if batch_limits.max_items < 0 ||
    batch_limits.max_total_input_codepoints < 0 ||
    batch_limits.max_total_output_bytes < 0 {
    return Err({
      index: -1,
      error: problem("InvalidLimit", "negative batch budget", 0, 0),
    })
  }
  if inputs.length() > batch_limits.max_items {
    return Err({
      index: -1,
      error: problem("BatchItemLimit", "batch count exceeds budget", 0, 0),
    })
  }
  // Validate options even for an empty batch.
  match check_input_budget(Variables::new(), limits) {
    Err(error) => return Err({ index: -1, error, })
    Ok(_) => ()
  }
  let mut remaining_input = batch_limits.max_total_input_codepoints
  let mut remaining_output = batch_limits.max_total_output_bytes
  for index, vars in inputs {
    let input_size = match
      self.validate_bindings(vars, required, reject_unused, limits) {
      Err(error) => return Err({ index, error, })
      Ok(left) => limits.max_total_value_codepoints - left
    }
    if input_size > remaining_input {
      return Err({
        index,
        error: problem(
          "BatchInputLimit", "total input exceeds batch budget", 0, 0,
        ),
      })
    }
    remaining_input = remaining_input - input_size
    let per_item = {
      ..limits,
      max_output_bytes: Int32::min(limits.max_output_bytes, remaining_output),
    }
    match self.render_impl(vars, Some(per_item), false, false) {
      Err(e) => {
        let error = if e.code == "OutputLimit" &&
          remaining_output < limits.max_output_bytes {
          {
            ..e,
            code: "BatchOutputLimit",
            message: "total output exceeds batch budget",
          }
        } else {
          e
        }
        return Err({ index, error, })
      }
      Ok(r) => remaining_output = remaining_output - r.bytes
    }
  }
  let outputs : Array[String] = []
  for index, vars in inputs {
    match self.expand_impl(vars, Some(limits)) {
      Err(error) => return Err({ index, error, })
      Ok(text) => outputs.push(text)
    }
  }
  Ok(outputs)
}

///|
/// Prepared partial binding keeps original operators intact. Fixed names cannot
/// be overridden; input collections are copied. It is not residual-template text.
struct BoundTemplate {
  template : Template
  fixed : Variables
}

///|
pub fn Template::bind(
  self : Template,
  fixed : Variables,
  limits~ : ExpansionLimits = ExpansionLimits::new(),
) -> Result[BoundTemplate, TemplateError] {
  match self.validate_bindings(fixed, [], true, limits) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let copied = Variables::new()
  for name, value in fixed.values {
    copied.set(name, value)
  }
  Ok({ template: self, fixed: copied, })
}

///|
fn BoundTemplate::merge(
  self : BoundTemplate,
  variables : Variables,
) -> Result[Variables, TemplateError] {
  let merged = Variables::new()
  for name, value in self.fixed.values {
    merged.set(name, value)
  }
  for name, value in variables.values {
    if merged.values.contains(name) {
      return Err(
        problem("FixedBinding", "cannot override a fixed binding", 0, 0),
      )
    }
    merged.set(name, value)
  }
  Ok(merged)
}

///|
pub fn BoundTemplate::expand(
  self : BoundTemplate,
  variables : Variables,
  required~ : Array[String] = [],
  limits~ : ExpansionLimits = ExpansionLimits::new(),
) -> Result[String, TemplateError] {
  match check_input_budget(variables, limits) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  match self.merge(variables) {
    Err(e) => Err(e)
    Ok(vars) =>
      self.template.expand(vars, required~, reject_unused=true, limits~)
  }
}

///|
pub fn BoundTemplate::measure(
  self : BoundTemplate,
  variables : Variables,
  required~ : Array[String] = [],
  limits~ : ExpansionLimits = ExpansionLimits::new(),
) -> Result[Int32, TemplateError] {
  match check_input_budget(variables, limits) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  match self.merge(variables) {
    Err(e) => Err(e)
    Ok(vars) =>
      self.template.measure(vars, required~, reject_unused=true, limits~)
  }
}

///|
pub fn Template::source(self : Template) -> String {
  self.source_text
}

///|
pub extend OutputOrigin with Debug::{to_repr}

///|
pub extend Expansion with Debug::{to_repr}

///|
pub extend BatchLimits with Debug::{to_repr}

///|
pub extend BatchError with Debug::{to_repr}