///|
pub struct RenderOptions {
  priv autoescape : Bool
  priv trim_blocks : Bool
  priv lstrip_blocks : Bool
  priv strict_undefined : Bool
  priv fuel : Int
  priv max_recursion : Int
  priv max_output_size : Int
  priv max_template_size : Int
  priv max_range_size : Int
  priv max_parse_depth : Int
  priv max_include_depth : Int
  priv max_cache_size : Int
  priv sandboxed : Bool
  priv enable_macros : Bool
  priv enable_multi_template : Bool
  priv enable_loop_controls : Bool
}

///|
/// Read-only metadata passed to context-aware extensions.
pub struct ExtensionContext {
  priv template_name : String
  priv autoescape : Bool
  priv sandboxed : Bool
}

///|
pub fn ExtensionContext::template_name(self : ExtensionContext) -> String {
  self.template_name
}

///|
pub fn ExtensionContext::autoescape(self : ExtensionContext) -> Bool {
  self.autoescape
}

///|
pub fn ExtensionContext::sandboxed(self : ExtensionContext) -> Bool {
  self.sandboxed
}

///|
pub fn RenderOptions::default() -> RenderOptions {
  {
    autoescape: false,
    trim_blocks: false,
    lstrip_blocks: false,
    strict_undefined: true,
    fuel: 100000,
    max_recursion: 128,
    max_output_size: 16 * 1024 * 1024,
    max_template_size: 4 * 1024 * 1024,
    max_range_size: 100000,
    max_parse_depth: 256,
    max_include_depth: 64,
    max_cache_size: 400,
    sandboxed: false,
    enable_macros: true,
    enable_multi_template: true,
    enable_loop_controls: true,
  }
}

///|
pub fn RenderOptions::with_autoescape(
  self : RenderOptions,
  enabled : Bool,
) -> RenderOptions {
  { ..self, autoescape: enabled }
}

///|
pub fn RenderOptions::with_whitespace_control(
  self : RenderOptions,
  trim_blocks : Bool,
  lstrip_blocks : Bool,
) -> RenderOptions {
  { ..self, trim_blocks, lstrip_blocks }
}

///|
pub fn RenderOptions::with_strict_undefined(
  self : RenderOptions,
  enabled : Bool,
) -> RenderOptions {
  { ..self, strict_undefined: enabled }
}

///|
pub fn RenderOptions::with_limits(
  self : RenderOptions,
  fuel : Int,
  max_recursion : Int,
  max_output_size : Int,
) -> RenderOptions {
  { ..self, fuel, max_recursion, max_output_size }
}

///|
pub fn RenderOptions::with_compile_limits(
  self : RenderOptions,
  max_template_size : Int,
  max_range_size : Int,
) -> RenderOptions {
  { ..self, max_template_size, max_range_size }
}

///|
pub fn RenderOptions::with_structural_limits(
  self : RenderOptions,
  max_parse_depth : Int,
  max_include_depth : Int,
) -> RenderOptions {
  { ..self, max_parse_depth, max_include_depth }
}

///|
/// Set the parsed-template cache capacity. Zero disables the cache.
pub fn RenderOptions::with_cache_capacity(
  self : RenderOptions,
  max_cache_size : Int,
) -> RenderOptions {
  { ..self, max_cache_size, }
}

///|
/// Enable the restricted execution policy and force HTML autoescape.
pub fn RenderOptions::with_sandbox(
  self : RenderOptions,
  enabled : Bool,
) -> RenderOptions {
  {
    ..self,
    sandboxed: enabled,
    autoescape: if enabled {
      true
    } else {
      self.autoescape
    },
  }
}

///|
/// Enable or disable optional language feature groups at compile time.
pub fn RenderOptions::with_features(
  self : RenderOptions,
  macros : Bool,
  multi_template : Bool,
  loop_controls : Bool,
) -> RenderOptions {
  {
    ..self,
    enable_macros: macros,
    enable_multi_template: multi_template,
    enable_loop_controls: loop_controls,
  }
}

///|
pub struct Environment {
  priv sources : Map[String, String]
  priv compiled : Map[String, Template]
  priv cache_order : Array[String]
  priv loader_sources : Map[String, Bool]
  priv loader_versions : Map[String, String]
  priv filters : Map[String, (Value, Array[Value]) -> Result[Value, String]]
  priv functions : Map[String, (Array[Value]) -> Result[Value, String]]
  priv tests : Map[String, (Value, Array[Value]) -> Result[Bool, String]]
  priv context_filters : Map[
    String,
    (ExtensionContext, Value, Array[Value], Map[String, Value]) -> Result[
      Value,
      String,
    ],
  ]
  priv context_functions : Map[
    String,
    (ExtensionContext, Array[Value], Map[String, Value]) -> Result[
      Value,
      String,
    ],
  ]
  priv context_tests : Map[
    String,
    (ExtensionContext, Value, Array[Value], Map[String, Value]) -> Result[
      Bool,
      String,
    ],
  ]
  priv restricted_extensions : Map[String, Bool]
  priv loader : Ref[((String) -> Result[String, String])?]
  priv versioned_loader : Ref[((String) -> Result[(String, String), String])?]
  priv autoescape_callback : Ref[((String) -> Bool)?]
  priv options : Ref[RenderOptions]
}

///|
pub struct CompiledTemplate {
  priv environment : Environment
  priv name : String
  priv template : Template
}

///|
pub fn Environment::new() -> Environment {
  let environment : Environment = {
    sources: Map([]),
    compiled: Map([]),
    cache_order: [],
    loader_sources: Map([]),
    loader_versions: Map([]),
    filters: Map([]),
    functions: Map([]),
    tests: Map([]),
    context_filters: Map([]),
    context_functions: Map([]),
    context_tests: Map([]),
    restricted_extensions: Map([]),
    loader: { val: None },
    versioned_loader: { val: None },
    autoescape_callback: { val: None },
    options: { val: RenderOptions::default() },
  }
  environment.install_builtins()
  environment.restricted_extensions.clear()
  environment
}

///|
pub fn Environment::set_options(
  self : Environment,
  options : RenderOptions,
) -> Unit {
  self.options.val = options
  // Compiled text depends on block whitespace policy.
  self.compiled.clear()
  self.cache_order.clear()
}

///|
fn Environment::options(self : Environment) -> RenderOptions {
  self.options.val
}

///|
fn Environment::options_for(
  self : Environment,
  template_name : String,
) -> RenderOptions {
  match self.autoescape_callback.val {
    Some(select) =>
      {
        ..self.options.val,
        autoescape: self.options.val.sandboxed || select(template_name),
      }
    None => self.options.val
  }
}

///|
/// Select autoescape per logical template name (for example by suffix).
pub fn Environment::set_autoescape_callback(
  self : Environment,
  select : (String) -> Bool,
) -> Unit {
  self.autoescape_callback.val = Some(select)
}

///|
pub fn Environment::clear_autoescape_callback(self : Environment) -> Unit {
  self.autoescape_callback.val = None
}

///|
pub fn Environment::set_loader(
  self : Environment,
  loader : (String) -> Result[String, String],
) -> Unit {
  for name in self.loader_sources.keys().to_array() {
    self.sources.remove(name)
    self.compiled.remove(name)
    self.remove_cache_order(name)
  }
  self.loader_sources.clear()
  self.loader_versions.clear()
  self.loader.val = Some(loader)
  self.versioned_loader.val = None
}

///|
/// Install a loader that returns `(source, version)` and automatically
/// recompiles an entry when its version changes.
pub fn Environment::set_versioned_loader(
  self : Environment,
  loader : (String) -> Result[(String, String), String],
) -> Unit {
  for name in self.loader_sources.keys().to_array() {
    self.sources.remove(name)
    self.compiled.remove(name)
    self.remove_cache_order(name)
  }
  self.loader_sources.clear()
  self.loader_versions.clear()
  self.loader.val = None
  self.versioned_loader.val = Some(loader)
}

///|
/// Clear parsed templates while preserving registered source strings.
pub fn Environment::clear_cache(self : Environment) -> Unit {
  self.compiled.clear()
  self.cache_order.clear()
}

///|
/// Invalidate one parsed template and any loader-owned source snapshot.
pub fn Environment::reload_template(self : Environment, name : String) -> Unit {
  self.compiled.remove(name)
  self.remove_cache_order(name)
  if self.loader_sources.contains(name) {
    self.sources.remove(name)
    self.loader_sources.remove(name)
    self.loader_versions.remove(name)
  }
}

///|
pub fn Environment::remove_template(self : Environment, name : String) -> Unit {
  self.sources.remove(name)
  self.compiled.remove(name)
  self.remove_cache_order(name)
  self.loader_sources.remove(name)
  self.loader_versions.remove(name)
}

///|
pub fn Environment::add_filter(
  self : Environment,
  name : String,
  filter : (Value, Array[Value]) -> Result[Value, String],
) -> Unit {
  self.filters[name] = filter
  self.restricted_extensions[name] = true
}

///|
pub fn Environment::remove_filter(self : Environment, name : String) -> Unit {
  self.filters.remove(name)
  self.context_filters.remove(name)
}

///|
pub fn Environment::add_context_filter(
  self : Environment,
  name : String,
  filter : (ExtensionContext, Value, Array[Value], Map[String, Value]) -> Result[
    Value,
    String,
  ],
) -> Unit {
  self.context_filters[name] = filter
  self.restricted_extensions[name] = true
}

///|
pub fn Environment::add_function(
  self : Environment,
  name : String,
  function : (Array[Value]) -> Result[Value, String],
) -> Unit {
  self.functions[name] = function
  self.restricted_extensions[name] = true
}

///|
pub fn Environment::remove_function(self : Environment, name : String) -> Unit {
  self.functions.remove(name)
  self.context_functions.remove(name)
}

///|
pub fn Environment::add_context_function(
  self : Environment,
  name : String,
  function : (ExtensionContext, Array[Value], Map[String, Value]) -> Result[
    Value,
    String,
  ],
) -> Unit {
  self.context_functions[name] = function
  self.restricted_extensions[name] = true
}

///|
pub fn Environment::add_test(
  self : Environment,
  name : String,
  test_function : (Value, Array[Value]) -> Result[Bool, String],
) -> Unit {
  self.tests[name] = test_function
  self.restricted_extensions[name] = true
}

///|
pub fn Environment::remove_test(self : Environment, name : String) -> Unit {
  self.tests.remove(name)
  self.context_tests.remove(name)
}

///|
pub fn Environment::add_context_test(
  self : Environment,
  name : String,
  test_function : (ExtensionContext, Value, Array[Value], Map[String, Value]) -> Result[
    Bool,
    String,
  ],
) -> Unit {
  self.context_tests[name] = test_function
  self.restricted_extensions[name] = true
}

///|
/// Allow a registered extension to execute while sandbox mode is enabled.
pub fn Environment::allow_extension_in_sandbox(
  self : Environment,
  name : String,
) -> Unit {
  self.restricted_extensions.remove(name)
}

///|
pub fn Environment::add_template(
  self : Environment,
  name : String,
  source : String,
) -> Unit raise JinjaError {
  validate_template_name(name)
  let template = self.compile_source(name, source)
  self.sources[name] = source
  self.cache_template(name, template)
  self.loader_sources.remove(name)
}

///|
fn Environment::resolve_template(
  self : Environment,
  name : String,
) -> Template raise JinjaError {
  validate_template_name(name)
  if self.loader_sources.contains(name) || !self.sources.contains(name) {
    match self.versioned_loader.val {
      Some(loader) => return self.resolve_versioned_template(name, loader)
      None => ()
    }
  }
  match self.compiled.get(name) {
    Some(template) => {
      self.touch_cache(name)
      template
    }
    None => {
      let (source, from_loader) = match self.sources.get(name) {
        Some(source) => (source, false)
        None =>
          match self.loader.val {
            Some(loader) =>
              match loader(name) {
                Ok(source) => (source, true)
                Err(message) => raise RenderError(message)
              }
            None => raise RenderError("Template not found: " + name)
          }
      }
      let template = self.compile_source(name, source)
      if !from_loader || self.options.val.max_cache_size > 0 {
        self.sources[name] = source
      }
      self.cache_template(name, template)
      if from_loader && self.options.val.max_cache_size > 0 {
        self.loader_sources[name] = true
      }
      template
    }
  }
}

///|
fn Environment::resolve_versioned_template(
  self : Environment,
  name : String,
  loader : (String) -> Result[(String, String), String],
) -> Template raise JinjaError {
  let (source, version) = match loader(name) {
    Ok(loaded) => loaded
    Err(message) => raise RenderError(message)
  }
  match (self.loader_versions.get(name), self.compiled.get(name)) {
    (Some(previous), Some(template)) if previous == version => {
      self.touch_cache(name)
      return template
    }
    _ => ()
  }
  let template = self.compile_source(name, source)
  if self.options.val.max_cache_size > 0 {
    self.sources[name] = source
    self.loader_sources[name] = true
    self.loader_versions[name] = version
  }
  self.cache_template(name, template)
  template
}

///|
fn Environment::compile_source(
  self : Environment,
  name : String,
  source : String,
) -> Template raise JinjaError {
  if utf8_length(source) > self.options.val.max_template_size {
    raise LexerError(
      "Template '" + name + "' exceeds the configured source size limit",
    )
  }
  parse(
    tokenize(preprocess_whitespace(source, self.options.val)),
    max_depth=self.options.val.max_parse_depth,
    enable_macros=self.options.val.enable_macros,
    enable_multi_template=self.options.val.enable_multi_template,
    enable_loop_controls=self.options.val.enable_loop_controls,
  ) catch {
    LexerError(message) =>
      raise LexerError("Template '" + name + "': " + message)
    ParseError(message) =>
      raise ParseError("Template '" + name + "': " + message)
    RenderError(message) =>
      raise RenderError("Template '" + name + "': " + message)
  }
}

///|
fn Environment::remove_cache_order(self : Environment, name : String) -> Unit {
  for index, cached_name in self.cache_order {
    if cached_name == name {
      ignore(self.cache_order.remove(index))
      return
    }
  }
}

///|
fn Environment::touch_cache(self : Environment, name : String) -> Unit {
  self.remove_cache_order(name)
  self.cache_order.push(name)
}

///|
fn Environment::cache_template(
  self : Environment,
  name : String,
  template : Template,
) -> Unit {
  let capacity = self.options.val.max_cache_size
  if capacity <= 0 {
    self.compiled.remove(name)
    self.remove_cache_order(name)
    return
  }
  self.compiled[name] = template
  self.touch_cache(name)
  while self.cache_order.length() > capacity {
    let evicted = self.cache_order.remove(0)
    self.compiled.remove(evicted)
    if self.loader_sources.contains(evicted) {
      self.sources.remove(evicted)
      self.loader_sources.remove(evicted)
      self.loader_versions.remove(evicted)
    }
  }
}

///|
/// Apply environment-level `trim_blocks` and `lstrip_blocks` before lexing.
/// Explicit Jinja `-` markers remain handled by the lexer/parser.
fn preprocess_whitespace(source : String, options : RenderOptions) -> String {
  if !options.trim_blocks && !options.lstrip_blocks {
    return source
  }
  let chars = source.to_array()
  let out : Array[Char] = []
  let mut i = 0
  while i < chars.length() {
    if i + 1 < chars.length() && chars[i] == '{' && chars[i + 1] == '%' {
      if options.lstrip_blocks {
        lstrip_block_indentation(out)
      }
      match match_named_block_tag(chars, i, "raw") {
        Some(opening) =>
          match find_named_block_tag(chars, opening.next_index, "endraw") {
            Some((content_end, closing)) => {
              for index in i.. ()
          }
        None => ()
      }
      let mut end = i + 2
      while end + 1 < chars.length() &&
            !(chars[end] == '%' && chars[end + 1] == '}') {
        end += 1
      }
      if end + 1 >= chars.length() {
        // Let the lexer produce the typed unterminated-tag error.
        while i < chars.length() {
          out.push(chars[i])
          i += 1
        }
        break
      }
      for index in i..<(end + 2) {
        out.push(chars[index])
      }
      i = end + 2
      if options.trim_blocks {
        i = skip_one_newline(chars, i)
      }
    } else {
      out.push(chars[i])
      i += 1
    }
  }
  String::from_array(out)
}

///|
fn lstrip_block_indentation(out : Array[Char]) -> Unit {
  let mut line_is_indentation = true
  let mut cursor = out.length() - 1
  while cursor >= 0 {
    match out[cursor] {
      '\n' | '\r' => break
      ' ' | '\t' => cursor -= 1
      _ => {
        line_is_indentation = false
        break
      }
    }
  }
  if line_is_indentation {
    while out.length() > 0 {
      match out.last() {
        Some(' ') | Some('\t') => ignore(out.pop())
        _ => break
      }
    }
  }
}

///|
fn skip_one_newline(chars : Array[Char], start : Int) -> Int {
  let mut index = start
  if index < chars.length() && chars[index] == '\r' {
    index += 1
    if index < chars.length() && chars[index] == '\n' {
      index += 1
    }
  } else if index < chars.length() && chars[index] == '\n' {
    index += 1
  }
  index
}

///|
pub fn Environment::get_template(
  self : Environment,
  name : String,
) -> CompiledTemplate raise JinjaError {
  let template = self.resolve_template(name)
  { environment: self, name, template }
}

///|
pub fn[T : ToJson] Environment::render(
  self : Environment,
  name : String,
  value : T,
) -> String raise JinjaError {
  self.get_template(name).render(value)
}

///|
pub fn[T : ToJson] CompiledTemplate::render(
  self : CompiledTemplate,
  value : T,
) -> String raise JinjaError {
  self.render_json(value.to_json())
}

///|
pub fn CompiledTemplate::render_json(
  self : CompiledTemplate,
  value : Json,
) -> String raise JinjaError {
  let context = context_from_json(value)
  render_compiled(self.template, context, self.environment, self.name)
}

///|
/// Render JSON data incrementally into a caller-provided sink.
pub fn CompiledTemplate::render_json_to(
  self : CompiledTemplate,
  value : Json,
  write : (String) -> Unit,
) -> Unit raise JinjaError {
  let context = context_from_json(value)
  render_compiled_to(self.template, context, self.environment, self.name, write)
}

///|
/// Convert a MoonBit value with `ToJson` and render it incrementally.
pub fn[T : ToJson] CompiledTemplate::render_to(
  self : CompiledTemplate,
  value : T,
  write : (String) -> Unit,
) -> Unit raise JinjaError {
  self.render_json_to(value.to_json(), write)
}

///|
fn context_from_json(value : Json) -> Map[String, Value] {
  match value {
    Object(entries) => {
      let context : Map[String, Value] = Map([])
      for key, item in entries {
        context[key] = value_from_json(item)
      }
      context
    }
    _ => { "value": value_from_json(value) }
  }
}

///|
fn value_from_json(value : Json) -> Value {
  match value {
    Null => Value::Null
    True => BoolValue(true)
    False => BoolValue(false)
    Number(number, repr~) => {
      ignore(repr)
      if number == number.trunc() {
        IntValue(number.to_int())
      } else {
        DoubleValue(number)
      }
    }
    String(text) => StrValue(text)
    Array(items) => ListValue(items.map(value_from_json))
    Object(entries) => {
      let result : Map[String, Value] = Map([])
      for key, item in entries {
        result[key] = value_from_json(item)
      }
      MapValue(result)
    }
  }
}

///|
fn validate_template_name(name : String) -> Unit raise JinjaError {
  if name == "" {
    raise RenderError("Template name cannot be empty")
  }
  let unified = name.replace(old="\\", new="/")
  if unified.has_prefix("/") {
    raise RenderError("Absolute template names are not allowed")
  }
  if unified.length() >= 2 &&
    unified[1].to_int() == ':'.to_int() &&
    (
      (
        'a'.to_int() <= unified[0].to_int() &&
        unified[0].to_int() <= 'z'.to_int()
      ) ||
      (
        'A'.to_int() <= unified[0].to_int() &&
        unified[0].to_int() <= 'Z'.to_int()
      )
    ) {
    raise RenderError("Drive-letter template names are not allowed")
  }
  for part in unified.split("/") {
    if part == ".." {
      raise RenderError("Parent template paths are not allowed")
    }
  }
}