///|
/// A pattern element after the escape is applied.
priv enum Wild {
  Lit(Char)
  One
  Many
}

///|
fn chars_of(s : String) -> Array[Char] {
  Array::from_iter(s.iter())
}

///|
/// Applies the escape: an escape before `%`, `_` or itself makes the next
/// character literal; before anything else, or at the end, it is an error.
fn compile_pattern(
  pattern : String,
  escape : Char?,
) -> Array[Wild] raise OpFailure {
  let out : Array[Wild] = []
  let chars = chars_of(pattern)
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if Some(c) == escape {
      if i + 1 >= chars.length() {
        raise DataFailure("LIKE pattern ends with the escape character")
      }
      let next = chars[i + 1]
      if next == '%' || next == '_' || Some(next) == escape {
        out.push(Lit(next))
        i += 2
      } else {
        raise DataFailure("LIKE escape character before an ordinary character")
      }
    } else {
      out.push(
        match c {
          '%' => Many
          '_' => One
          _ => Lit(c)
        },
      )
      i += 1
    }
  }
  out
}

///|
/// Iterative wildcard matching: on a mismatch, back up to the last `%` and
/// let it take one more character.
fn wildcard_match(text : Array[Char], pat : Array[Wild]) -> Bool {
  let mut t = 0
  let mut p = 0
  let mut star = -1
  let mut mark = 0
  while t < text.length() {
    let step = if p < pat.length() {
      match pat[p] {
        Many => {
          star = p
          mark = t
          p += 1
          true
        }
        One => {
          t += 1
          p += 1
          true
        }
        Lit(c) =>
          if c == text[t] {
            t += 1
            p += 1
            true
          } else {
            false
          }
      }
    } else {
      false
    }
    if !step {
      if star < 0 {
        return false
      }
      mark += 1
      t = mark
      p = star + 1
    }
  }
  while p < pat.length() && pat[p] is Many {
    p += 1
  }
  p == pat.length()
}

///|
/// `text LIKE pattern [ESCAPE escape]`. MISSING if an operand is MISSING,
/// else NULL if one is NULL, else a type failure unless all are strings.
pub fn like(
  text : @value.Value,
  pattern : @value.Value,
  escape : @value.Value?,
) -> @value.Value raise OpFailure {
  let operands = [boxed(text), boxed(pattern)]
  match escape {
    Some(e) => operands.push(boxed(e))
    None => ()
  }
  if operands.any(v => v is Missing) {
    return Missing
  }
  if operands.any(v => v is Null) {
    return Null
  }
  match operands {
    [String(t), String(p)] =>
      @value.Value::from_bool(
        wildcard_match(chars_of(t), compile_pattern(p, None)),
      )
    [String(t), String(p), String(e)] => {
      let units = chars_of(e)
      guard units.length() == 1 else {
        raise DataFailure("LIKE escape must be one character")
      }
      @value.Value::from_bool(
        wildcard_match(chars_of(t), compile_pattern(p, Some(units[0]))),
      )
    }
    _ => raise TypeFailure("LIKE of a value that is not a string")
  }
}