///|
pub type Filter = (Value, Array[Value]) -> Value

///|
struct FilterEntry {
  name : String
  filter : Filter
}

///|
fn default_filter_registry() -> Array[FilterEntry] {
  []
}

///|
fn copy_filter_registry(filters : Array[FilterEntry]) -> Array[FilterEntry] {
  filters.copy()
}

///|
fn has_filter(filters : Array[FilterEntry], name : String) -> Bool {
  if is_builtin_filter_name(name) {
    return true
  }
  for entry in filters {
    if entry.name == name {
      return true
    }
  }
  false
}

///|
fn register_filter_entry(
  filters : Array[FilterEntry],
  name : String,
  filter : Filter,
) -> Unit raise MoldError {
  if has_filter(filters, name) {
    raise DuplicateFilter(name)
  }
  filters.push({ name, filter })
}

///|
fn apply_filter(
  filters : Array[FilterEntry],
  value : Value,
  name : String,
  args : Array[Value],
) -> Value raise MoldError {
  let peeled = match value {
    Safe(inner) => inner
    _ => value
  }
  let result = match name {
    "upper" => filter_upper(peeled, args)
    "lower" => filter_lower(peeled, args)
    "trim" => filter_trim(peeled, args)
    "default" => filter_default_impl(peeled, args)
    "join" => filter_join(peeled, args)
    "escape" => filter_escape(peeled, args)
    "safe" => filter_safe(value, args)
    "length" => filter_length(peeled, args)
    _ => {
      for entry in filters {
        if entry.name == name {
          let inner = (entry.filter)(peeled, args)
          match value {
            Safe(_) => return Safe(inner)
            _ => return inner
          }
        }
      }
      raise UnknownFilter(name)
    }
  }
  match value {
    Safe(_) => Safe(result)
    _ => result
  }
}

///|
fn is_builtin_filter_name(name : String) -> Bool {
  match name {
    "upper" => true
    "lower" => true
    "trim" => true
    "default" => true
    "join" => true
    "escape" => true
    "safe" => true
    "length" => true
    _ => false
  }
}

///|
fn filter_upper(value : Value, _args : Array[Value]) -> Value raise MoldError {
  match value {
    String(s) => String(s.to_upper())
    _ => raise TypeMismatch(("upper requires string", "string"))
  }
}

///|
fn filter_lower(value : Value, _args : Array[Value]) -> Value raise MoldError {
  match value {
    String(s) => String(s.to_lower())
    _ => raise TypeMismatch(("lower requires string", "string"))
  }
}

///|
fn filter_trim(value : Value, _args : Array[Value]) -> Value raise MoldError {
  match value {
    String(s) => String(s.trim().to_owned())
    _ => raise TypeMismatch(("trim requires string", "string"))
  }
}

///|
fn filter_default_impl(value : Value, args : Array[Value]) -> Value noraise {
  match value {
    Null => if args.length() > 0 { args[0] } else { String("") }
    String(s) =>
      if s.length() == 0 && args.length() > 0 {
        args[0]
      } else {
        value
      }
    _ => value
  }
}

///|
fn filter_join(value : Value, args : Array[Value]) -> Value raise MoldError {
  let separator = if args.length() > 0 {
    match args[0] {
      String(s) => s
      _ => ", "
    }
  } else {
    ", "
  }
  match value {
    Array(items) => {
      let buf = StringBuilder::new()
      let mut first = true
      for item in items {
        if !first {
          buf.write_string(separator)
        }
        first = false
        match item {
          String(s) => buf.write_string(s)
          Int(n) => buf.write_string(n.to_string())
          Float(d) => buf.write_string(d.to_string())
          Bool(b) => buf.write_string(if b { "true" } else { "false" })
          Safe(inner) =>
            match inner {
              String(s) => buf.write_string(s)
              _ => buf.write_string("")
            }
          _ => buf.write_string("")
        }
      }
      String(buf.to_string())
    }
    _ => raise TypeMismatch(("join requires array", "array"))
  }
}

///|
fn filter_escape(value : Value, _args : Array[Value]) -> Value raise MoldError {
  String(escape_html(value_to_string(value, "escape")))
}

///|
fn escape_html(text : String) -> String {
  let buf = StringBuilder::new(size_hint=text.length())
  for ch in text {
    match ch {
      '&' => buf.write_string("&")
      '<' => buf.write_string("<")
      '>' => buf.write_string(">")
      '"' => buf.write_string(""")
      '\'' => buf.write_string("'")
      _ => buf.write_char(ch)
    }
  } nobreak {
    buf.to_string()
  }
}

///|
fn filter_length(value : Value, _args : Array[Value]) -> Value raise MoldError {
  match value {
    Array(items) => Int(items.length())
    String(s) => Int(s.length())
    Null => Int(0)
    _ =>
      raise TypeMismatch(
        ("length requires array, string, or null", "array|string|null"),
      )
  }
}

///|
fn filter_safe(value : Value, _args : Array[Value]) -> Value noraise {
  Safe(value)
}