///|
priv enum LineKind {
  ListItem
  DictItem
  StringItem
  KeyItem
  InlineDict
  InlineList
  Unrecognized
} derive(Eq)

///|
priv struct Line {
  kind : LineKind
  lineno : Int
  depth : Int
  key : String?
  value : String?
  text : String
}

///|
priv struct Lexer {
  lines : Array[Line]
  mut pos : Int
}

///|
fn Lexer::from_string(input : String) -> Result[Lexer, NestedTextError] {
  let stripped = match input.strip_prefix("\u{FEFF}") {
    Some(rest) => rest.to_owned()
    None => input
  }
  let normalized = stripped
    .replace_all(old="\r\n", new="\n")
    .replace_all(old="\r", new="\n")
  let lines : Array[Line] = []
  for lineno, raw_view in normalized.split("\n") {
    let raw = raw_view.to_owned()
    let depth = count_leading_spaces(raw)
    match check_indentation(raw, lineno + 1) {
      Some(err) => return Err(err)
      None => ()
    }
    let rest = raw.unsafe_substring(start=depth, end=raw.length())
    if rest.is_empty() || rest.has_prefix("#") {
      continue
    }
    lines.push(classify_line(rest, depth, lineno + 1, raw))
  }
  Ok({ lines, pos: 0 })
}

///|
fn Lexer::peek(self : Lexer) -> Line? {
  self.lines.get(self.pos)
}

///|
fn Lexer::next_line(self : Lexer) -> Line? {
  match self.lines.get(self.pos) {
    Some(line) => {
      self.pos += 1
      Some(line)
    }
    None => None
  }
}

///|
fn Lexer::next_is(self : Lexer, depth : Int, kind : LineKind) -> Bool {
  match self.peek() {
    Some(line) => line.depth == depth && line.kind == kind
    None => false
  }
}

///|
fn count_leading_spaces(line : String) -> Int {
  for count = 0, i = 0; i < line.length(); {
    if line[i] == ' ' {
      continue count + 1, i + 1
    } else {
      break count
    }
  } nobreak {
    count
  }
}

///|
fn check_indentation(raw : String, lineno : Int) -> NestedTextError? {
  for colno, ch in raw.iter2() {
    if ch == ' ' {
      continue
    }
    if ch == '\t' {
      return Some(
        NestedTextError::at(
          TabInIndentation,
          "invalid character in indentation: '\\t'.",
          lineno,
          colno + 1,
          raw,
        ),
      )
    }
    if ch.is_whitespace() {
      let desc = if ch == '\u{00A0}' {
        "'\\xa0' (NO-BREAK SPACE)."
      } else {
        "'\\u{\{ch.to_int()}}'."
      }
      return Some(
        NestedTextError::at(
          InvalidIndentation,
          "invalid character in indentation: \{desc}",
          lineno,
          colno + 1,
          raw,
        ),
      )
    }
    break None
  } nobreak {
    None
  }
}

///|
fn classify_line(
  rest : String,
  depth : Int,
  lineno : Int,
  raw : String,
) -> Line {
  if rest.has_prefix("- ") || rest == "-" {
    let value = if rest == "-" {
      ""
    } else {
      rest.unsafe_substring(start=2, end=rest.length())
    }
    return {
      kind: ListItem,
      lineno,
      depth,
      key: None,
      value: Some(value),
      text: raw,
    }
  }
  if rest.has_prefix("> ") || rest == ">" {
    let value = if rest == ">" {
      ""
    } else {
      rest.unsafe_substring(start=2, end=rest.length())
    }
    return {
      kind: StringItem,
      lineno,
      depth,
      key: None,
      value: Some(value),
      text: raw,
    }
  }
  if rest.has_prefix(": ") || rest == ":" {
    let value = if rest == ":" {
      ""
    } else {
      rest.unsafe_substring(start=2, end=rest.length())
    }
    return {
      kind: KeyItem,
      lineno,
      depth,
      key: None,
      value: Some(value),
      text: raw,
    }
  }
  if rest.has_prefix("[") {
    return {
      kind: InlineList,
      lineno,
      depth,
      key: None,
      value: Some(rest),
      text: raw,
    }
  }
  if rest.has_prefix("{") {
    return {
      kind: InlineDict,
      lineno,
      depth,
      key: None,
      value: Some(rest),
      text: raw,
    }
  }
  match try_parse_dict_item(rest) {
    Some((key, value)) =>
      {
        kind: DictItem,
        lineno,
        depth,
        key: Some(key),
        value: Some(value),
        text: raw,
      }
    None =>
      { kind: Unrecognized, lineno, depth, key: None, value: None, text: raw }
  }
}

///|

///|
/// Strip trailing whitespace including NBSP (NO-BREAK SPACE U+00A0).
/// MoonBit's trim_end does not treat NBSP as whitespace.
fn strip_trailing_ws(s : String) -> String {
  if s.is_empty() {
    return s
  }
  let mut end = s.length()
  while end > 0 {
    let ch = s[end - 1]
    if ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '\u{00A0}' {
      end -= 1
    } else {
      break
    }
  }
  if end == s.length() {
    s
  } else {
    s.unsafe_substring(start=0, end~)
  }
}

///|
fn try_parse_dict_item(rest : String) -> (String, String)? {
  match rest.find(": ") {
    Some(colon_pos) => {
      let key = strip_trailing_ws(rest.unsafe_substring(start=0, end=colon_pos))
      let value = rest.unsafe_substring(start=colon_pos + 2, end=rest.length())
      if key.is_empty() {
        None
      } else {
        Some((key, value))
      }
    }
    None =>
      if rest.has_suffix(":") {
        let key = strip_trailing_ws(
          rest.unsafe_substring(start=0, end=rest.length() - 1),
        )
        if key.is_empty() {
          None
        } else {
          Some((key, ""))
        }
      } else {
        None
      }
  }
}