///|
priv enum Expr {
  Literal(LiquidValue)
  Lookup(Array[String])
  Pipeline(Expr, Array[FilterCall])
}

///|
priv struct FilterCall {
  name : String
  arguments : Array[(String?, Expr)]
}

///|
priv enum Condition {
  Truth(Expr)
  Compare(Expr, String, Expr)
  And(Condition, Condition)
  Or(Condition, Condition)
  Not(Condition)
}

///|
fn compile_expression(source : String) -> Expr {
  let (text, filters) = parse_output(source)
  let value = if text.length() >= 2 &&
    (text[0] == 39 || text[0] == 34) &&
    text[text.length() - 1] == text[0] {
    Expr::Literal(@value.string_value(text[1:text.length() - 1].to_owned()))
  } else {
    match text {
      "true" => Literal(@value.bool_value(true))
      "false" => Literal(@value.bool_value(false))
      "nil" | "null" => Literal(@value.null_value())
      _ =>
        match @semantics.parse_number(text) {
          Some(n) => Literal(@semantics.numeric_result(n, text.contains(".")))
          None => Lookup(text.split(".").map(fn(p) { p.to_owned() }).collect())
        }
    }
  }
  if filters.is_empty() {
    value
  } else {
    Pipeline(value, filters.map(compile_filter))
  }
}

///|
fn compile_filter(filter : ParsedFilter) -> FilterCall {
  {
    name: filter.name,
    arguments: filter.parameters.map(fn(parameter) {
      if find_unquoted(parameter, ":") is Some(at) {
        (
          Some(parameter[:at].trim().to_owned()),
          compile_expression(parameter[at + 1:].to_owned()),
        )
      } else {
        (None, compile_expression(parameter))
      }
    }),
  }
}

///|
fn compile_output(source : String, filters : Array[ParsedFilter]) -> Expr {
  let expression = compile_expression(source)
  if filters.is_empty() {
    expression
  } else {
    Pipeline(expression, filters.map(compile_filter))
  }
}

///|
fn Expr::eval(
  self : Expr,
  context : LiquidContext,
  state : RenderState,
) -> LiquidValue {
  match self {
    Literal(value) => value
    Lookup(path) => lookup_path(context, path).unwrap_or(@value.null_value())
    Pipeline(expression, filters) => {
      let mut value = expression.eval(context, state)
      for filter in filters {
        let arguments : Array[LiquidValue] = []
        let options : Map[String, LiquidValue] = Map([])
        for (name, expr) in filter.arguments {
          let argument = expr.eval(context, state)
          match name {
            Some(key) => options[key] = argument
            None => arguments.push(argument)
          }
        }
        value = match apply_filter(value, filter.name, arguments, options~) {
          Ok(result) => result
          Err(diagnostic) =>
            @value.string_value(report_diagnostic(state, diagnostic))
        }
      }
      value
    }
  }
}

///|
fn compile_condition(source : String) -> Condition {
  let text = source.trim().to_owned()
  let logical = match
    (find_unquoted(text, " and "), find_unquoted(text, " or ")) {
    (Some(a), Some(b)) =>
      if a < b {
        Some((a, " and "))
      } else {
        Some((b, " or "))
      }
    (Some(a), None) => Some((a, " and "))
    (None, Some(b)) => Some((b, " or "))
    _ => None
  }
  if logical is Some((at, op)) {
    let left = compile_condition(text[:at].to_owned())
    let right = compile_condition(text[at + op.length():].to_owned())
    return if op == " and " { And(left, right) } else { Or(left, right) }
  }
  if text.strip_prefix("not ") is Some(rest) {
    return Not(compile_condition(rest.to_owned()))
  }
  for op in ["==", "!=", ">=", "<=", ">", "<", " contains "] {
    if find_unquoted(text, op) is Some(at) {
      return Compare(
        compile_expression(text[:at].to_owned()),
        op.trim().to_owned(),
        compile_expression(text[at + op.length():].to_owned()),
      )
    }
  }
  Truth(compile_expression(text))
}

///|
fn Condition::eval(
  self : Condition,
  context : LiquidContext,
  state : RenderState,
) -> Bool {
  match self {
    Truth(expression) => @semantics.is_truthy(expression.eval(context, state))
    Compare(left, op, right) =>
      @semantics.compare_typed_values(
        left.eval(context, state),
        right.eval(context, state),
        op,
      )
    And(left, right) => left.eval(context, state) && right.eval(context, state)
    Or(left, right) => left.eval(context, state) || right.eval(context, state)
    Not(condition) => !condition.eval(context, state)
  }
}

///|
fn Expr::missing(self : Expr, context : LiquidContext) -> Bool {
  match self {
    Lookup(path) => lookup_path(context, path) is None
    Pipeline(base, _) => base.missing(context)
    _ => false
  }
}

///|
fn Expr::label(self : Expr) -> String {
  match self {
    Lookup(path) => path.join(".")
    Pipeline(base, _) => base.label()
    Literal(value) => value.to_string()
  }
}