///|
/// A boolean expression over tags, supporting `and`, `or`, `not`,
/// parentheses, and bare `@tag` literals.
pub(all) enum TagExpression {
  TagLiteral(String)
  Not(TagExpression)
  And(TagExpression, TagExpression)
  Or(TagExpression, TagExpression)
  Always // matches everything (empty expression)
} derive(Debug, Eq)

///|
pub extend TagExpression with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend TagExpression with Eq::{not_equal, equal}

///|
/// A syntax error in a tag expression.
///
/// Every variant carries the whole expression, so the message can show it.
pub(all) suberror TagExpressionError {
  /// The expression ended where a tag, `not` or `(` was expected.
  UnexpectedEnd(expression~ : String)
  /// A token appeared where it is not allowed, for example `and @a`,
  /// `@a @b` or `@a)`.
  UnexpectedToken(expression~ : String, token~ : String)
  /// A `(` has no matching `)`.
  UnclosedParen(expression~ : String)
} derive(Eq, Debug)

///|
pub extend TagExpressionError with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend TagExpressionError with Eq::{not_equal, equal}

///|
pub extend TagExpressionError with Show::{to_string, output}

///|
pub impl Show for TagExpressionError with fn output(self, logger) {
  let detail = match self {
    UnexpectedEnd(expression~) =>
      (expression, "expected a tag, `not` or `(` but the expression ended")
    UnexpectedToken(expression~, token~) =>
      (expression, "unexpected \"\{token}\"")
    UnclosedParen(expression~) => (expression, "missing `)`")
  }
  logger.write_string(
    "invalid tag expression \{detail.0.escape()}: \{detail.1}",
  )
}

///|
/// Parse a tag expression string into a TagExpression AST.
/// An empty or whitespace-only string yields `Always` which matches any set
/// of tags. Malformed input raises a `TagExpressionError`.
pub fn TagExpression::parse(
  input : String,
) -> TagExpression raise TagExpressionError {
  let trimmed = trim_string(input)
  if trimmed.is_empty() {
    return Always
  }
  let parser = TagParser::{ expression: input, tokens: tokenize_tags(trimmed), }
  let (expr, pos) = parser.parse_or(0)
  if pos < parser.tokens.length() {
    raise UnexpectedToken(expression=input, token=parser.tokens[pos])
  }
  expr
}

///|
/// Evaluate whether this expression matches a given set of tags.
pub fn TagExpression::matches(
  self : TagExpression,
  tags : Array[String],
) -> Bool {
  match self {
    Always => true
    TagLiteral(name) => array_contains(tags, name)
    Not(inner) => !inner.matches(tags)
    And(left, right) => left.matches(tags) && right.matches(tags)
    Or(left, right) => left.matches(tags) || right.matches(tags)
  }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

///|
fn trim_string(s : String) -> String {
  let chars = s.to_array()
  let len = chars.length()
  let mut start = 0
  let mut end = len
  while start < end && is_whitespace(chars[start]) {
    start = start + 1
  }
  while end > start && is_whitespace(chars[end - 1]) {
    end = end - 1
  }
  if start == 0 && end == len {
    return s
  }
  let buf = StringBuilder()
  for i = start; i < end; i = i + 1 {
    buf.write_char(chars[i])
  }
  buf.to_string()
}

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

///|
fn array_contains(arr : Array[String], value : String) -> Bool {
  for item in arr {
    if item == value {
      return true
    }
  }
  false
}

// ---------------------------------------------------------------------------
// Tokenizer
// ---------------------------------------------------------------------------

///|
fn tokenize_tags(input : String) -> Array[String] {
  let tokens : Array[String] = []
  let chars = input.to_array()
  let len = chars.length()
  let mut i = 0
  while i < len {
    let c = chars[i]
    if is_whitespace(c) {
      i = i + 1
      continue
    }
    if c == '(' {
      tokens.push("(")
      i = i + 1
      continue
    }
    if c == ')' {
      tokens.push(")")
      i = i + 1
      continue
    }
    // Read a word (tag or keyword)
    let buf = StringBuilder()
    while i < len &&
          chars[i] != ' ' &&
          chars[i] != '\t' &&
          chars[i] != '\n' &&
          chars[i] != '\r' &&
          chars[i] != '(' &&
          chars[i] != ')' {
      buf.write_char(chars[i])
      i = i + 1
    }
    tokens.push(buf.to_string())
  }
  tokens
}

// ---------------------------------------------------------------------------
// Recursive descent parser
// Precedence (low to high): or < and < not < atom
// ---------------------------------------------------------------------------

///|
priv struct TagParser {
  expression : String
  tokens : Array[String]
}

///|
fn TagParser::parse_or(
  self : TagParser,
  pos : Int32,
) -> (TagExpression, Int32) raise TagExpressionError {
  let (first, next) = self.parse_and(pos)
  let mut left = first
  let mut p = next
  while p < self.tokens.length() && self.tokens[p] == "or" {
    let (right, after) = self.parse_and(p + 1)
    left = Or(left, right)
    p = after
  }
  (left, p)
}

///|
fn TagParser::parse_and(
  self : TagParser,
  pos : Int32,
) -> (TagExpression, Int32) raise TagExpressionError {
  let (first, next) = self.parse_not(pos)
  let mut left = first
  let mut p = next
  while p < self.tokens.length() && self.tokens[p] == "and" {
    let (right, after) = self.parse_not(p + 1)
    left = And(left, right)
    p = after
  }
  (left, p)
}

///|
fn TagParser::parse_not(
  self : TagParser,
  pos : Int32,
) -> (TagExpression, Int32) raise TagExpressionError {
  if pos < self.tokens.length() && self.tokens[pos] == "not" {
    let (inner, next) = self.parse_not(pos + 1)
    (Not(inner), next)
  } else {
    self.parse_atom(pos)
  }
}

///|
fn TagParser::parse_atom(
  self : TagParser,
  pos : Int32,
) -> (TagExpression, Int32) raise TagExpressionError {
  guard pos < self.tokens.length() else {
    raise UnexpectedEnd(expression=self.expression)
  }
  match self.tokens[pos] {
    "(" => {
      let (inner, next) = self.parse_or(pos + 1)
      guard next < self.tokens.length() else {
        raise UnclosedParen(expression=self.expression)
      }
      guard self.tokens[next] == ")" else {
        raise UnexpectedToken(
          expression=self.expression,
          token=self.tokens[next],
        )
      }
      (inner, next + 1)
    }
    ")" | "and" | "or" as token =>
      raise UnexpectedToken(expression=self.expression, token~)
    tag => (TagLiteral(tag), pos + 1)
  }
}