///|
/// Dynamic values exposed to custom functions and filters.
pub(all) enum Value {
  Undefined
  Null
  IntValue(Int)
  DoubleValue(Double)
  BoolValue(Bool)
  StrValue(String)
  SafeStr(String)
  ListValue(Array[Value])
  MapValue(Map[String, Value])
}

///|
fn escape_html(source : String) -> String {
  let output = StringBuilder(size_hint=source.length())
  for char in source {
    match char {
      '&' => output.write_string("&")
      '<' => output.write_string("<")
      '>' => output.write_string(">")
      '"' => output.write_string(""")
      '\'' => output.write_string("'")
      _ => output.write_char(char)
    }
  }
  output.to_string()
}

///|
fn utf8_length(source : String) -> Int {
  let mut length = 0
  for char in source {
    length += utf8_char_size(char)
  }
  length
}

///|
fn display_string(value : Value, autoescape? : Bool = false) -> String {
  match value {
    SafeStr(text) => text
    StrValue(text) => if autoescape { escape_html(text) } else { text }
    _ => {
      let text = value_to_string(value)
      if autoescape {
        escape_html(text)
      } else {
        text
      }
    }
  }
}

///|
fn is_ws(char : Char) -> Bool {
  char == ' ' || char == '\t' || char == '\n' || char == '\r'
}

///|
fn trim_ws(source : String) -> String {
  let chars = source.to_array()
  let mut left = 0
  let mut right = chars.length()
  while left < right && is_ws(chars[left]) {
    left += 1
  }
  while left < right && is_ws(chars[right - 1]) {
    right -= 1
  }
  String::from_array(chars[left:right].to_owned())
}

///|
fn trim_ws_start(source : String) -> String {
  let chars = source.to_array()
  let mut left = 0
  while left < chars.length() && is_ws(chars[left]) {
    left += 1
  }
  String::from_array(chars[left:].to_owned())
}

///|
fn trim_ws_end(source : String) -> String {
  let chars = source.to_array()
  let mut right = chars.length()
  while right > 0 && is_ws(chars[right - 1]) {
    right -= 1
  }
  String::from_array(chars[:right].to_owned())
}

///|
fn value_to_string(value : Value) -> String {
  match value {
    Null | Undefined => ""
    IntValue(number) => number.to_string()
    DoubleValue(number) => number.to_string()
    BoolValue(flag) => if flag { "true" } else { "false" }
    StrValue(text) | SafeStr(text) => text
    ListValue(items) => items.map(value_to_string).join(", ")
    MapValue(_) => "[object]"
  }
}

///|
fn is_truthy(value : Value) -> Bool {
  match value {
    Null | Undefined => false
    BoolValue(flag) => flag
    StrValue(text) | SafeStr(text) => text != ""
    IntValue(number) => number != 0
    DoubleValue(number) => number != 0.0
    ListValue(items) => !items.is_empty()
    MapValue(entries) => !entries.is_empty()
  }
}

///|
fn apply_binop(
  lhs : Value,
  operator : BinOp,
  rhs : Value,
) -> Value raise JinjaError {
  if operator == In || operator == NotIn {
    let included = contains_value(rhs, lhs)
    return BoolValue(if operator == NotIn { !included } else { included })
  }
  if operator == Concat {
    return StrValue(value_to_string(lhs) + value_to_string(rhs))
  }
  match (lhs, rhs) {
    (IntValue(left), IntValue(right)) =>
      match operator {
        Add => IntValue(left + right)
        Sub => IntValue(left - right)
        Mul => IntValue(left * right)
        Div =>
          if right == 0 {
            raise RenderError("Division by zero")
          } else {
            DoubleValue(left.to_double() / right.to_double())
          }
        FloorDiv =>
          if right == 0 {
            raise RenderError("Division by zero")
          } else {
            let quotient = left / right
            let remainder = left % right
            IntValue(
              if remainder != 0 && (remainder < 0) != (right < 0) {
                quotient - 1
              } else {
                quotient
              },
            )
          }
        Mod =>
          if right == 0 {
            raise RenderError("Modulo by zero")
          } else {
            let remainder = left % right
            IntValue(
              if remainder != 0 && (remainder < 0) != (right < 0) {
                remainder + right
              } else {
                remainder
              },
            )
          }
        Pow => apply_integer_pow(left, right)
        Eq => BoolValue(left == right)
        Ne => BoolValue(left != right)
        Lt => BoolValue(left < right)
        Le => BoolValue(left <= right)
        Gt => BoolValue(left > right)
        Ge => BoolValue(left >= right)
        _ => raise RenderError("Unsupported numeric operation")
      }
    (DoubleValue(left), DoubleValue(right)) =>
      apply_double_binop(left, operator, right)
    (IntValue(left), DoubleValue(right)) =>
      apply_double_binop(left.to_double(), operator, right)
    (DoubleValue(left), IntValue(right)) =>
      apply_double_binop(left, operator, right.to_double())
    (StrValue(left), StrValue(right)) =>
      match operator {
        Add => StrValue(left + right)
        Eq => BoolValue(left == right)
        Ne => BoolValue(left != right)
        _ => raise RenderError("Unsupported string operation")
      }
    (BoolValue(left), BoolValue(right)) =>
      match operator {
        And => BoolValue(left && right)
        Or => BoolValue(left || right)
        Eq => BoolValue(left == right)
        Ne => BoolValue(left != right)
        _ => raise RenderError("Unsupported boolean operation")
      }
    (Null, Null) =>
      match operator {
        Eq => BoolValue(true)
        Ne => BoolValue(false)
        _ => raise RenderError("Unsupported null operation")
      }
    _ => raise RenderError("Type mismatch in binary expression")
  }
}

///|
fn apply_integer_pow(base : Int, exponent : Int) -> Value {
  if exponent < 0 {
    return DoubleValue(@math.pow(base.to_double(), exponent.to_double()))
  }
  let mut result = 1
  let mut factor = base
  let mut remaining = exponent
  while remaining > 0 {
    if remaining % 2 == 1 {
      result *= factor
    }
    remaining /= 2
    if remaining > 0 {
      factor *= factor
    }
  }
  IntValue(result)
}

///|
fn contains_value(container : Value, needle : Value) -> Bool raise JinjaError {
  match (container, needle) {
    (StrValue(text) | SafeStr(text), StrValue(part) | SafeStr(part)) =>
      text.contains(part)
    (MapValue(entries), StrValue(key) | SafeStr(key)) => entries.contains(key)
    (ListValue(items), value) =>
      items.any(item => primitive_values_equal(item, value))
    _ => raise RenderError("Right operand of 'in' is not iterable")
  }
}

///|
fn primitive_values_equal(left : Value, right : Value) -> Bool {
  match (left, right) {
    (Undefined, Undefined) | (Null, Null) => true
    (IntValue(a), IntValue(b)) => a == b
    (DoubleValue(a), DoubleValue(b)) => a == b
    (IntValue(a), DoubleValue(b)) => a.to_double() == b
    (DoubleValue(a), IntValue(b)) => a == b.to_double()
    (BoolValue(a), BoolValue(b)) => a == b
    (StrValue(a) | SafeStr(a), StrValue(b) | SafeStr(b)) => a == b
    _ => false
  }
}

///|
fn apply_double_binop(
  left : Double,
  operator : BinOp,
  right : Double,
) -> Value raise JinjaError {
  match operator {
    Add => DoubleValue(left + right)
    Sub => DoubleValue(left - right)
    Mul => DoubleValue(left * right)
    Div =>
      if right == 0.0 {
        raise RenderError("Division by zero")
      } else {
        DoubleValue(left / right)
      }
    FloorDiv =>
      if right == 0.0 {
        raise RenderError("Division by zero")
      } else {
        DoubleValue((left / right).floor())
      }
    Mod =>
      if right == 0.0 {
        raise RenderError("Modulo by zero")
      } else {
        DoubleValue(left - (left / right).floor() * right)
      }
    Pow => DoubleValue(@math.pow(left, right))
    Eq => BoolValue(left == right)
    Ne => BoolValue(left != right)
    Lt => BoolValue(left < right)
    Le => BoolValue(left <= right)
    Gt => BoolValue(left > right)
    Ge => BoolValue(left >= right)
    _ => raise RenderError("Unsupported numeric operation")
  }
}