///|
/// One parsed query clause.
pub struct QueryClause {
  value : String
  field : String?
  required : Bool
  excluded : Bool
  phrase : Bool
  prefix : Bool
} derive(Eq, Debug)

///|
/// Parsed query language representation.
pub struct QueryPlan {
  original : String
  clauses : Array[QueryClause]
} derive(Eq, Debug)

///|
priv struct RawClause {
  value : String
  phrase : Bool
}

///|
fn chars_to_string(chars : Array[Char]) -> String {
  StringView::from_iter(chars.iter()).to_owned()
}

///|
fn raw_query_clauses(query : String) -> Result[Array[RawClause], String] {
  let raw : Array[RawClause] = []
  let current : Array[Char] = []
  let mut quoted = false
  let mut clause_phrase = false
  for char in query.iter() {
    if char == '"' {
      if quoted {
        quoted = false
      } else {
        let modifier_only = current.length() == 1 &&
          (current[0] == '+' || current[0] == '-')
        if !current.is_empty() && !modifier_only {
          return Err("quote must start at a clause boundary")
        }
        quoted = true
        clause_phrase = true
      }
    } else if (char == ' ' || char == '\t' || char == '\n') && !quoted {
      if !current.is_empty() {
        raw.push({ value: chars_to_string(current), phrase: clause_phrase, })
        current.clear()
        clause_phrase = false
      }
    } else {
      current.push(char)
    }
  }
  if quoted {
    return Err("unterminated quoted phrase")
  }
  if !current.is_empty() {
    raw.push({ value: chars_to_string(current), phrase: clause_phrase, })
  }
  Ok(raw)
}

///|
fn parse_field(value : String) -> (String?, String) {
  match value.find(":") {
    None => (None, value)
    Some(index) => {
      let field = value.exact_view(start=0, end=index).to_owned()
      let rest = value
        .exact_view(start=index + 1, end=value.length())
        .to_owned()
      if field.is_empty() || rest.is_empty() {
        (None, value)
      } else {
        (Some(field), rest)
      }
    }
  }
}

///|
fn parse_raw_clause(raw : RawClause) -> Result[QueryClause, String] {
  let mut value = raw.value
  let mut required = false
  let mut excluded = false
  if value.has_prefix("+") {
    required = true
    value = value.exact_view(start=1, end=value.length()).to_owned()
  } else if value.has_prefix("-") {
    excluded = true
    value = value.exact_view(start=1, end=value.length()).to_owned()
  }
  if value.is_empty() {
    return Err("query modifier requires a value")
  }
  let (field, field_value) = parse_field(value)
  value = field_value
  let mut prefix = false
  if value.has_suffix("*") {
    prefix = true
    value = value.exact_view(start=0, end=value.length() - 1).to_owned()
  }
  if value.is_empty() {
    return Err("prefix operator requires a value")
  }
  if value.contains("*") {
    return Err("wildcard is supported only at the end of a clause")
  }
  Ok({ value, field, required, excluded, phrase: raw.phrase, prefix, })
}

///|
/// Parse a compact query language:
/// - `+term`: required term
/// - `-term`: excluded term
/// - `"exact phrase"`: phrase clause
/// - `prefix*`: prefix clause
/// - `title:moonbit`: field-qualified clause
pub fn parse_query(query : String) -> Result[QueryPlan, String] {
  let trimmed = query.trim().to_owned()
  if trimmed.is_empty() {
    return Err("query must not be empty")
  }
  let clauses : Array[QueryClause] = []
  match raw_query_clauses(trimmed) {
    Err(message) => Err(message)
    Ok(raw_clauses) => {
      for raw in raw_clauses {
        match parse_raw_clause(raw) {
          Ok(clause) => clauses.push(clause)
          Err(message) => return Err(message)
        }
      }
      if clauses.is_empty() {
        Err("query must contain at least one clause")
      } else {
        Ok({ original: trimmed, clauses, })
      }
    }
  }
}

///|
pub fn QueryPlan::positive_values(self : QueryPlan) -> Array[String] {
  let out : Array[String] = []
  for clause in self.clauses {
    if !clause.excluded && !out.contains(clause.value) {
      out.push(clause.value)
    }
  }
  out
}

///|
pub fn QueryPlan::excluded_values(self : QueryPlan) -> Array[String] {
  let out : Array[String] = []
  for clause in self.clauses {
    if clause.excluded && !out.contains(clause.value) {
      out.push(clause.value)
    }
  }
  out
}

///|
pub fn QueryPlan::required_values(self : QueryPlan) -> Array[String] {
  let out : Array[String] = []
  for clause in self.clauses {
    if clause.required && !clause.excluded && !out.contains(clause.value) {
      out.push(clause.value)
    }
  }
  out
}

///|
pub fn QueryPlan::search_text(self : QueryPlan) -> String {
  let builder = StringBuilder()
  let mut first = true
  for value in self.positive_values() {
    if !first {
      builder.write_string(" ")
    }
    builder.write_string(value)
    first = false
  }
  builder.to_string()
}

///|
pub fn QueryPlan::has_field(self : QueryPlan, field : String) -> Bool {
  for clause in self.clauses {
    if clause.field == Some(field) {
      return true
    }
  }
  false
}

///|
fn clause_matches(
  clause : QueryClause,
  text : String,
  tokenizer : Tokenizer,
) -> Bool {
  if clause.phrase {
    return text.contains(clause.value)
  }
  let tokens = tokenizer.tokenize(text)
  let query_tokens = tokenizer.tokenize(clause.value)
  if clause.prefix {
    for token in tokens {
      for query_token in query_tokens {
        if token.has_prefix(query_token) {
          return true
        }
      }
    }
    return false
  }
  for query_token in query_tokens {
    if tokens.contains(query_token) {
      return true
    }
  }
  false
}

///|
/// Test required and excluded clauses against plain text. Optional clauses do
/// not filter the document; they are used for ranking.
pub fn QueryPlan::matches(
  self : QueryPlan,
  text : String,
  tokenizer? : Tokenizer = Tokenizer::new(),
) -> Bool {
  for clause in self.clauses {
    let matched = clause_matches(clause, text, tokenizer)
    if clause.excluded && matched {
      return false
    }
    if clause.required && !matched {
      return false
    }
  }
  true
}

///|
pub fn filter_with_query_plan(
  results : Array[SearchResult],
  plan : QueryPlan,
  tokenizer? : Tokenizer = Tokenizer::new(),
) -> Array[SearchResult] {
  let out : Array[SearchResult] = []
  for result in results {
    if plan.matches(result.text, tokenizer~) {
      out.push(result)
    }
  }
  out
}

///|
pub fn format_query_plan(plan : QueryPlan) -> String {
  let builder = StringBuilder()
  for i = 0; i < plan.clauses.length(); i = i + 1 {
    if i > 0 {
      builder.write_string(" ")
    }
    let clause = plan.clauses[i]
    if clause.required {
      builder.write_string("+")
    } else if clause.excluded {
      builder.write_string("-")
    }
    match clause.field {
      Some(field) => {
        builder.write_string(field)
        builder.write_string(":")
      }
      None => ()
    }
    if clause.phrase {
      builder.write_string("\"")
    }
    builder.write_string(clause.value)
    if clause.phrase {
      builder.write_string("\"")
    }
    if clause.prefix {
      builder.write_string("*")
    }
  }
  builder.to_string()
}