///|
/// Matcher for style names in style-map rules.
pub(all) enum StyleNameMatcher {
  ExactStyleName(String)
  PrefixStyleName(String)
} derive(Debug, Eq)

///|
/// Matcher for document elements in style-map rules.
pub(all) enum DocumentMatcher {
  ParagraphMatcher(
    style_id~ : String?,
    style_name~ : StyleNameMatcher?,
    numbering~ : Numbering?
  )
  RunMatcher(style_id~ : String?, style_name~ : StyleNameMatcher?)
  RunPropertyMatcher(String)
  HighlightMatcher(String?)
  TableMatcher(style_id~ : String?, style_name~ : StyleNameMatcher?)
  BreakMatcher(BreakType)
  CommentReferenceMatcher
} derive(Debug, Eq)

///|
/// One element in a parsed style-map HTML path.
pub(all) struct HtmlPathElement {
  tag : String
  attributes : Map[String, String]
  fresh : Bool
  separator : String?
} derive(Debug, Eq)

///|
/// Parsed mapping from a document matcher to an HTML path.
pub(all) struct StyleMapping {
  from : DocumentMatcher
  to : Array[HtmlPathElement]
} derive(Debug, Eq)

///|
/// Parsed style mappings together with style-map diagnostics.
pub(all) struct StyleMapParseResult {
  mappings : Array[StyleMapping]
  messages : Array[Message]
} derive(Debug, Eq)

///|
priv struct ParsedMatcherSuffixes {
  style_id : String?
  style_name : StyleNameMatcher?
  numbering : Numbering?
}

///|
priv enum ParsedDocumentMatcher {
  ParsedDocumentMatcherOk(DocumentMatcher, Int)
  ParsedDocumentMatcherError(String)
}

///|
priv enum ParsedHtmlPathElement {
  ParsedHtmlPathElementOk(HtmlPathElement, Int)
  ParsedHtmlPathElementError(Int, String)
}

///|
/// Parses one style-map line into a mapping, if valid.
pub fn read_style_mapping(line : String) -> StyleMapping? {
  read_style_mapping_with_message(line).0
}

///|
/// Parses style-map lines and returns valid mappings.
pub fn read_style_map(lines : Array[String]) -> Array[StyleMapping] {
  read_style_map_with_messages(lines).mappings
}

///|
/// Parses style-map lines and preserves diagnostics.
pub fn parse_style_map(lines : Array[String]) -> StyleMapParseResult {
  read_style_map_with_messages(lines)
}

///|
/// Parses style-map lines with diagnostics.
pub fn read_style_map_with_messages(
  lines : Array[String],
) -> StyleMapParseResult {
  let mappings : Array[StyleMapping] = []
  let messages : Array[Message] = []
  for line in lines {
    let (mapping, message) = read_style_mapping_with_message(line)
    match mapping {
      Some(mapping) => mappings.push(mapping)
      None => ()
    }
    match message {
      Some(message) => messages.push(message)
      None => ()
    }
  }
  { mappings, messages }
}

///|
fn read_style_mapping_with_message(line : String) -> (StyleMapping?, Message?) {
  let end = line.length()
  let (from, first_index) = match parse_style_document_matcher(line, 0, end) {
    ParsedDocumentMatcherOk(from, index) => (from, index)
    ParsedDocumentMatcherError(error) =>
      return (None, Some(style_mapping_warning(line, error)))
  }
  let mut index = first_index
  if index >= end || !is_style_whitespace(line[index]) {
    return (
      None,
      Some(
        style_mapping_warning(
          line,
          style_parse_error(line, index, end, "whitespace"),
        ),
      ),
    )
  }
  while index < end && is_style_whitespace(line[index]) {
    index = index + 1
  }
  if !starts_with_at(line, index, end, "=>") {
    return (
      None,
      Some(
        style_mapping_warning(
          line,
          style_parse_error(line, index, end, "arrow"),
        ),
      ),
    )
  }
  let after_arrow = index + 2
  let to_raw = line[after_arrow:].to_owned()
  let (to, error) = read_html_path_text(line, to_raw, after_arrow)
  match error {
    Some(error) => (None, Some(style_mapping_warning(line, error)))
    None => (Some({ from, to }), None)
  }
}

///|
fn style_mapping_warning(line : String, error : String) -> Message {
  Warning(
    "Did not understand this style mapping, so ignored it: " +
    line +
    "\n" +
    error,
  )
}

///|
/// Splits a style-map string into meaningful lines.
pub fn read_style_map_string(style_map : String) -> Array[String] {
  let lines : Array[String] = []
  for line_view in style_map.split("\n") {
    let line = line_view.trim().to_owned()
    if line != "" && !line.has_prefix("#") {
      lines.push(line)
    }
  }
  lines
}

///|
/// Parses a style-map string and preserves diagnostics.
pub fn parse_style_map_string(style_map : String) -> StyleMapParseResult {
  parse_style_map(read_style_map_string(style_map))
}

///|
fn parse_style_document_matcher(
  line : String,
  index : Int,
  end : Int,
) -> ParsedDocumentMatcher {
  let (name, next, error) = parse_style_identifier(
    line, index, end, "element type",
  )
  match error {
    Some(error) => return ParsedDocumentMatcherError(error)
    None => ()
  }
  match name {
    "p" => {
      let (suffixes, next, error) = parse_style_matcher_suffixes(
        line,
        next,
        end,
        allow_list=true,
      )
      match error {
        Some(error) => ParsedDocumentMatcherError(error)
        None =>
          ParsedDocumentMatcherOk(
            ParagraphMatcher(
              style_id=suffixes.style_id,
              style_name=suffixes.style_name,
              numbering=suffixes.numbering,
            ),
            next,
          )
      }
    }
    "r" => {
      let (suffixes, next, error) = parse_style_matcher_suffixes(
        line,
        next,
        end,
        allow_list=false,
      )
      match error {
        Some(error) => ParsedDocumentMatcherError(error)
        None =>
          ParsedDocumentMatcherOk(
            RunMatcher(
              style_id=suffixes.style_id,
              style_name=suffixes.style_name,
            ),
            next,
          )
      }
    }
    "table" => parse_style_table_matcher(line, next, end)
    "b" => ParsedDocumentMatcherOk(RunPropertyMatcher("bold"), next)
    "i" => ParsedDocumentMatcherOk(RunPropertyMatcher("italic"), next)
    "u" => ParsedDocumentMatcherOk(RunPropertyMatcher("underline"), next)
    "strike" =>
      ParsedDocumentMatcherOk(RunPropertyMatcher("strikethrough"), next)
    "all-caps" => ParsedDocumentMatcherOk(RunPropertyMatcher("allCaps"), next)
    "small-caps" =>
      ParsedDocumentMatcherOk(RunPropertyMatcher("smallCaps"), next)
    "highlight" => parse_style_highlight_matcher(line, next, end)
    "comment-reference" =>
      ParsedDocumentMatcherOk(CommentReferenceMatcher, next)
    "br" => parse_style_break_matcher(line, next, end)
    _ =>
      ParsedDocumentMatcherError(
        style_parse_error(line, index, end, "element type"),
      )
  }
}

///|
fn parse_style_matcher_suffixes(
  line : String,
  index : Int,
  end : Int,
  allow_list~ : Bool,
) -> (ParsedMatcherSuffixes, Int, String?) {
  let mut suffixes = ParsedMatcherSuffixes::{
    style_id: None,
    style_name: None,
    numbering: None,
  }
  let mut index = index
  while index < end {
    match line[index] {
      '.' => {
        let (style_id, next, error) = parse_style_identifier(
          line,
          index + 1,
          end,
          "identifier",
        )
        match error {
          Some(error) => return (suffixes, next, Some(error))
          None => {
            suffixes = { ..suffixes, style_id: Some(style_id) }
            index = next
          }
        }
      }
      '[' => {
        let (style_name, next, error) = parse_style_name_suffix(
          line,
          index + 1,
          end,
        )
        match error {
          Some(error) => return (suffixes, next, Some(error))
          None => {
            suffixes = { ..suffixes, style_name, }
            index = next
          }
        }
      }
      ':' if allow_list => {
        let (numbering, next, consumed, error) = parse_style_list_suffix_after_colon(
          line, index, end,
        )
        match error {
          Some(error) => return (suffixes, next, Some(error))
          None =>
            if consumed {
              suffixes = { ..suffixes, numbering, }
              index = next
            } else {
              return (suffixes, index, None)
            }
        }
      }
      _ => return (suffixes, index, None)
    }
  }
  (suffixes, index, None)
}

///|
fn parse_style_table_matcher(
  line : String,
  index : Int,
  end : Int,
) -> ParsedDocumentMatcher {
  let (suffixes, next, error) = parse_style_matcher_suffixes(
    line,
    index,
    end,
    allow_list=false,
  )
  match error {
    Some(error) => ParsedDocumentMatcherError(error)
    None =>
      ParsedDocumentMatcherOk(
        TableMatcher(style_id=suffixes.style_id, style_name=suffixes.style_name),
        next,
      )
  }
}

///|
fn parse_style_name_suffix(
  line : String,
  index : Int,
  end : Int,
) -> (StyleNameMatcher?, Int, String?) {
  let name_start = index
  let (name, first_index, error) = parse_style_identifier(
    line, index, end, "identifier \"style-name\"",
  )
  let mut index = first_index
  match error {
    Some(error) => return (None, index, Some(error))
    None => ()
  }
  if name != "style-name" {
    return (
      None,
      name_start,
      Some(
        style_parse_error(line, name_start, end, "identifier \"style-name\""),
      ),
    )
  }
  let mut is_prefix = false
  if starts_with_at(line, index, end, "^=") {
    is_prefix = true
    index = index + 2
  } else if index < end && line[index] == '=' {
    index = index + 1
  } else {
    return (
      None,
      index,
      Some(style_parse_error(line, index, end, "style name matcher")),
    )
  }
  let (value, next, error) = parse_style_string(line, index, end)
  match error {
    Some(error) => return (None, next, Some(error))
    None => index = next
  }
  if index >= end || line[index] != ']' {
    return (
      None,
      index,
      Some(style_parse_error(line, index, end, "close-square-bracket")),
    )
  }
  let matcher = if is_prefix {
    PrefixStyleName(value)
  } else {
    ExactStyleName(value)
  }
  (Some(matcher), index + 1, None)
}

///|
fn parse_style_list_suffix_after_colon(
  line : String,
  index : Int,
  end : Int,
) -> (Numbering?, Int, Bool, String?) {
  let (name, first_next, error) = parse_style_identifier(
    line,
    index + 1,
    end,
    "identifier",
  )
  let mut next = first_next
  match error {
    Some(_) => return (None, index, false, None)
    None => ()
  }
  let is_ordered = match name {
    "ordered-list" => true
    "unordered-list" => false
    _ => return (None, index, false, None)
  }
  if next >= end || line[next] != '(' {
    return (
      None,
      next,
      true,
      Some(style_parse_error(line, next, end, "open-paren")),
    )
  }
  let (level, after_level, error) = parse_style_integer(line, next + 1, end)
  match error {
    Some(error) => return (None, after_level, true, Some(error))
    None => next = after_level
  }
  if next >= end || line[next] != ')' {
    return (
      None,
      next,
      true,
      Some(style_parse_error(line, next, end, "close-paren")),
    )
  }
  (Some({ is_ordered, level }), next + 1, true, None)
}

///|
fn parse_style_highlight_matcher(
  line : String,
  index : Int,
  end : Int,
) -> ParsedDocumentMatcher {
  if index >= end || line[index] != '[' {
    return ParsedDocumentMatcherOk(HighlightMatcher(None), index)
  }
  let name_start = index + 1
  let (name, first_index, error) = parse_style_identifier(
    line, name_start, end, "identifier \"color\"",
  )
  let mut index = first_index
  match error {
    Some(error) => return ParsedDocumentMatcherError(error)
    None => ()
  }
  if name != "color" {
    return ParsedDocumentMatcherError(
      style_parse_error(line, name_start, end, "identifier \"color\""),
    )
  }
  if index >= end || line[index] != '=' {
    return ParsedDocumentMatcherError(
      style_parse_error(line, index, end, "equals"),
    )
  }
  let (color, next, error) = parse_style_string(line, index + 1, end)
  match error {
    Some(error) => return ParsedDocumentMatcherError(error)
    None => index = next
  }
  if index >= end || line[index] != ']' {
    return ParsedDocumentMatcherError(
      style_parse_error(line, index, end, "close-square-bracket"),
    )
  }
  ParsedDocumentMatcherOk(HighlightMatcher(Some(color)), index + 1)
}

///|
fn parse_style_break_matcher(
  line : String,
  index : Int,
  end : Int,
) -> ParsedDocumentMatcher {
  if index >= end || line[index] != '[' {
    return ParsedDocumentMatcherError(
      style_parse_error(line, index, end, "open-square-bracket"),
    )
  }
  let name_start = index + 1
  let (name, first_index, error) = parse_style_identifier(
    line, name_start, end, "identifier \"type\"",
  )
  let mut index = first_index
  match error {
    Some(error) => return ParsedDocumentMatcherError(error)
    None => ()
  }
  if name != "type" {
    return ParsedDocumentMatcherError(
      style_parse_error(line, name_start, end, "identifier \"type\""),
    )
  }
  if index >= end || line[index] != '=' {
    return ParsedDocumentMatcherError(
      style_parse_error(line, index, end, "equals"),
    )
  }
  let (break_type, next, error) = parse_style_string(line, index + 1, end)
  let break_type_start = index + 2
  match error {
    Some(error) => return ParsedDocumentMatcherError(error)
    None => index = next
  }
  if index >= end || line[index] != ']' {
    return ParsedDocumentMatcherError(
      style_parse_error(line, index, end, "close-square-bracket"),
    )
  }
  let break_type : BreakType = match break_type {
    "line" => Line
    "page" => Page
    "column" => Column
    _ =>
      return ParsedDocumentMatcherError(
        style_parse_error(line, break_type_start, end, "break type"),
      )
  }
  ParsedDocumentMatcherOk(BreakMatcher(break_type), index + 1)
}

///|
fn read_html_path_text(
  line : String,
  to_raw : String,
  after_arrow : Int,
) -> (Array[HtmlPathElement], String?) {
  let end = after_arrow + to_raw.length()
  if after_arrow >= end {
    return ([], None)
  }
  if !is_style_whitespace(line[after_arrow]) {
    return ([], Some(style_parse_error(line, after_arrow, end, "end")))
  }
  let mut index = after_arrow
  while index < end && is_style_whitespace(line[index]) {
    index = index + 1
  }
  if index >= end {
    return ([], None)
  }
  let (elements, index, error) = parse_style_html_path(line, index, end)
  match error {
    Some(error) => ([], Some(error))
    None =>
      if index == end {
        (elements, None)
      } else {
        ([], Some(style_parse_error(line, index, end, "end")))
      }
  }
}

///|
fn parse_style_html_path(
  line : String,
  index : Int,
  end : Int,
) -> (Array[HtmlPathElement], Int, String?) {
  if line[index] == '!' {
    return (
      [{ tag: "!", attributes: Map([]), fresh: false, separator: None }],
      index + 1,
      None,
    )
  }
  if !is_style_identifier_start(line, index, end) {
    return ([], index, None)
  }
  let elements : Array[HtmlPathElement] = []
  let mut index = match parse_style_html_path_element(line, index, end) {
    ParsedHtmlPathElementOk(element, next) => {
      elements.push(element)
      next
    }
    ParsedHtmlPathElementError(next, error) => return ([], next, Some(error))
  }
  while index < end {
    let separator_start = index
    let mut after_space = index
    while after_space < end && is_style_whitespace(line[after_space]) {
      after_space = after_space + 1
    }
    if after_space == index {
      return (elements, index, None)
    }
    if after_space < end && line[after_space] == '>' {
      let mut next = after_space + 1
      if next < end && is_style_whitespace(line[next]) {
        while next < end && is_style_whitespace(line[next]) {
          next = next + 1
        }
        if next >= end || !is_style_identifier_start(line, next, end) {
          return (elements, separator_start, None)
        }
        match parse_style_html_path_element(line, next, end) {
          ParsedHtmlPathElementError(next_index, _) =>
            return (
              elements,
              next_index,
              Some(style_parse_error(line, next_index, end, "end")),
            )
          ParsedHtmlPathElementOk(element, next_index) => {
            elements.push(element)
            index = next_index
          }
        }
      } else {
        return (elements, separator_start, None)
      }
    } else {
      return (elements, separator_start, None)
    }
  }
  (elements, index, None)
}

///|
fn parse_style_html_path_element(
  line : String,
  index : Int,
  end : Int,
) -> ParsedHtmlPathElement {
  let (tag, first_index) = parse_style_tag_choices_after_start(line, index, end)
  let mut index = first_index
  let attributes : Map[String, String] = Map([])
  while index < end {
    match line[index] {
      '.' => {
        let (class_name, next, error) = parse_style_identifier(
          line,
          index + 1,
          end,
          "identifier",
        )
        match error {
          Some(error) => return ParsedHtmlPathElementError(next, error)
          None => {
            match attributes.get("class") {
              Some(existing) =>
                attributes["class"] = existing + " " + class_name
              None => attributes["class"] = class_name
            }
            index = next
          }
        }
      }
      '[' => {
        let (name, value, next, error) = parse_style_attribute(
          line,
          index + 1,
          end,
        )
        match error {
          Some(error) => return ParsedHtmlPathElementError(next, error)
          None => {
            attributes[name] = value
            index = next
          }
        }
      }
      _ => break
    }
  }
  let (next, consumed) = consume_style_option_identifier(
    line, index, end, "fresh",
  )
  let fresh = consumed
  if consumed {
    index = next
  }
  let (separator, next, consumed) = consume_style_separator_option(
    line, index, end,
  )
  if consumed {
    index = next
  }
  ParsedHtmlPathElementOk({ tag, attributes, fresh, separator }, index)
}

///|
fn parse_style_tag_choices_after_start(
  line : String,
  index : Int,
  end : Int,
) -> (String, Int) {
  let builder = StringBuilder()
  let (tag, first_index) = read_style_identifier_after_start(line, index, end)
  builder.write_string(tag)
  let mut index = first_index
  while index < end && line[index] == '|' {
    let choice_index = index
    if !is_style_identifier_start(line, index + 1, end) {
      return (builder.to_string(), choice_index)
    }
    builder.write_string("|")
    let (tag, next) = read_style_identifier_after_start(line, index + 1, end)
    builder.write_string(tag)
    index = next
  }
  (builder.to_string(), index)
}

///|
fn is_style_whitespace(char : UInt16) -> Bool {
  char is (' ' | '\t' | '\n' | '\r')
}

///|
fn parse_style_attribute(
  line : String,
  index : Int,
  end : Int,
) -> (String, String, Int, String?) {
  let (name, first_index, error) = parse_style_identifier(
    line, index, end, "identifier",
  )
  let mut index = first_index
  match error {
    Some(error) => return ("", "", index, Some(error))
    None => ()
  }
  if index >= end || line[index] != '=' {
    return (
      name,
      "",
      index,
      Some(style_parse_error(line, index, end, "equals")),
    )
  }
  index = index + 1
  let (value, next, error) = parse_style_string(line, index, end)
  match error {
    Some(error) => return (name, "", next, Some(error))
    None => index = next
  }
  if index >= end || line[index] != ']' {
    return (
      name,
      value,
      index,
      Some(style_parse_error(line, index, end, "close-square-bracket")),
    )
  }
  (name, value, index + 1, None)
}

///|
fn consume_style_option_identifier(
  line : String,
  index : Int,
  end : Int,
  expected : String,
) -> (Int, Bool) {
  if index >= end || line[index] != ':' {
    return (index, false)
  }
  let (name, next, error) = parse_style_identifier(
    line,
    index + 1,
    end,
    "identifier",
  )
  match error {
    Some(_) => (index, false)
    None => if name == expected { (next, true) } else { (index, false) }
  }
}

///|
fn consume_style_separator_option(
  line : String,
  index : Int,
  end : Int,
) -> (String?, Int, Bool) {
  let (next, consumed) = consume_style_option_identifier(
    line, index, end, "separator",
  )
  if !consumed {
    return (None, index, false)
  }
  if next >= end || line[next] != '(' {
    return (None, index, false)
  }
  let (separator, value_end, error) = parse_style_string(line, next + 1, end)
  match error {
    Some(_) => return (None, index, false)
    None => ()
  }
  if value_end >= end || line[value_end] != ')' {
    return (None, index, false)
  }
  (Some(separator), value_end + 1, true)
}

///|
fn parse_style_identifier(
  line : String,
  index : Int,
  end : Int,
  expected : String,
) -> (String, Int, String?) {
  if !is_style_identifier_start(line, index, end) {
    return ("", index, Some(style_parse_error(line, index, end, expected)))
  }
  let (identifier, next) = read_style_identifier_after_start(line, index, end)
  (identifier, next, None)
}

///|
fn read_style_identifier_after_start(
  line : String,
  index : Int,
  end : Int,
) -> (String, Int) {
  let builder = StringBuilder()
  let mut index = index
  while index < end {
    match line[index] {
      '\\' if index + 1 < end => {
        builder.write_string(line[index + 1:index + 2].to_owned())
        index = index + 2
      }
      char if is_style_identifier_part(char) => {
        builder.write_string(line[index:index + 1].to_owned())
        index = index + 1
      }
      _ => break
    }
  }
  (builder.to_string(), index)
}

///|
fn parse_style_string(
  line : String,
  index : Int,
  end : Int,
) -> (String, Int, String?) {
  if index >= end || line[index] != '\'' {
    return ("", index, Some(style_parse_error(line, index, end, "string")))
  }
  let builder = StringBuilder()
  let mut cursor = index + 1
  while cursor < end {
    match line[cursor] {
      '\\' if cursor + 1 < end => {
        write_style_escape(builder, line, cursor + 1)
        cursor = cursor + 2
      }
      '\'' => return (builder.to_string(), cursor + 1, None)
      _ => {
        builder.write_string(line[cursor:cursor + 1].to_owned())
        cursor = cursor + 1
      }
    }
  }
  (
    "",
    index,
    Some(
      "Error was at character number " +
      (index + 1).to_string() +
      ": Expected string but got " +
      describe_unterminated_style_string(line, index, end),
    ),
  )
}

///|
fn parse_style_integer(
  line : String,
  index : Int,
  end : Int,
) -> (Int, Int, String?) {
  if index >= end || !(line[index] is ('0'..='9')) {
    return (0, index, Some(style_parse_error(line, index, end, "integer")))
  }
  let value_text = read_style_integer_token(line, index, end)
  let next = index + value_text.length()
  let value = @string.parse_int(value_text) catch { _ => 0 }
  (value, next, None)
}

///|
fn starts_with_at(
  line : String,
  index : Int,
  end : Int,
  value : String,
) -> Bool {
  index + value.length() <= end &&
  line[index:index + value.length()].to_owned() == value
}

///|
fn is_style_identifier_start(line : String, index : Int, end : Int) -> Bool {
  if index >= end {
    false
  } else if line[index] == '\\' {
    index + 1 < end
  } else {
    is_style_identifier_start_char(line[index])
  }
}

///|
fn is_style_identifier_start_char(char : UInt16) -> Bool {
  char is ('a'..='z' | 'A'..='Z' | '-' | '_')
}

///|
fn is_style_identifier_part(char : UInt16) -> Bool {
  is_style_identifier_start_char(char) || char is ('0'..='9')
}

///|
fn style_parse_error(
  line : String,
  index : Int,
  end : Int,
  expected : String,
) -> String {
  "Error was at character number " +
  (index + 1).to_string() +
  ": Expected " +
  expected +
  " but got " +
  describe_style_token(line, index, end)
}

///|
fn describe_style_token(line : String, index : Int, end : Int) -> String {
  if index >= end {
    return "end"
  }
  if starts_with_at(line, index, end, "=>") {
    return "arrow"
  }
  if starts_with_at(line, index, end, "^=") {
    return "startsWith"
  }
  match line[index] {
    ' ' | '\t' | '\n' | '\r' => "whitespace"
    '=' => "equals"
    '>' => "gt"
    ':' => "colon"
    '.' => "dot"
    '[' => "open-square-bracket"
    ']' => "close-square-bracket"
    '(' => "open-paren"
    ')' => "close-paren"
    '|' => "choice"
    '!' => "bang \"!\""
    '\'' => describe_unterminated_style_string(line, index, end)
    _ =>
      if is_style_identifier_start(line, index, end) {
        let (identifier, _, _) = parse_style_identifier(
          line, index, end, "identifier",
        )
        "identifier \"" + identifier + "\""
      } else if line[index] is ('0'..='9') {
        "integer \"" + read_style_integer_token(line, index, end) + "\""
      } else {
        "unrecognisedCharacter \"" + line[index:index + 1].to_owned() + "\""
      }
  }
}

///|
fn describe_unterminated_style_string(
  line : String,
  index : Int,
  end : Int,
) -> String {
  let token = read_unterminated_style_string_token(line, index, end)
  if token == "" {
    "unterminated-string"
  } else {
    "unterminated-string \"" + token + "\""
  }
}

///|
fn read_unterminated_style_string_token(
  line : String,
  index : Int,
  end : Int,
) -> String {
  if index + 1 >= end {
    ""
  } else {
    line[index + 1:end].to_owned()
  }
}

///|
fn read_style_integer_token(line : String, index : Int, end : Int) -> String {
  let mut cursor = index
  while cursor < end && line[cursor] is ('0'..='9') {
    cursor = cursor + 1
  }
  line[index:cursor].to_owned()
}

///|
fn write_style_escape(
  builder : StringBuilder,
  value : String,
  index : Int,
) -> Unit {
  let char = value[index]
  match char {
    'n' => builder.write_string("\n")
    'r' => builder.write_string("\r")
    't' => builder.write_string("\t")
    _ => builder.write_string(value[index:index + 1].to_owned())
  }
}

///|
/// Returns whether this matcher accepts the document element.
pub fn DocumentMatcher::matches(
  self : DocumentMatcher,
  element : DocumentElement,
) -> Bool {
  match (self, element) {
    (
      ParagraphMatcher(style_id~, style_name~, numbering~),
      Paragraph(properties~, ..),
    ) =>
      option_matches(style_id, properties.style_id) &&
      style_name_matches(style_name, properties.style_name) &&
      option_matches(numbering, properties.numbering)
    (RunMatcher(style_id~, style_name~), Run(properties~, ..)) =>
      option_matches(style_id, properties.style_id) &&
      style_name_matches(style_name, properties.style_name)
    (TableMatcher(style_id~, style_name~), Table(properties~, ..)) =>
      option_matches(style_id, properties.style_id) &&
      style_name_matches(style_name, properties.style_name)
    (RunPropertyMatcher("bold"), Run(properties~, ..)) => properties.is_bold
    (RunPropertyMatcher("italic"), Run(properties~, ..)) => properties.is_italic
    (RunPropertyMatcher("underline"), Run(properties~, ..)) =>
      properties.is_underline
    (RunPropertyMatcher("strikethrough"), Run(properties~, ..)) =>
      properties.is_strikethrough
    (RunPropertyMatcher("allCaps"), Run(properties~, ..)) =>
      properties.is_all_caps
    (RunPropertyMatcher("smallCaps"), Run(properties~, ..)) =>
      properties.is_small_caps
    (HighlightMatcher(None), Run(properties~, ..)) =>
      properties.highlight != None
    (HighlightMatcher(Some(color)), Run(properties~, ..)) =>
      properties.highlight == Some(color)
    (BreakMatcher(expected), Break(actual)) => expected == actual
    (CommentReferenceMatcher, CommentReference(_)) => true
    _ => false
  }
}

///|
fn[T : Eq] option_matches(expected : T?, actual : T?) -> Bool {
  match expected {
    Some(value) => actual == Some(value)
    None => true
  }
}

///|
fn style_name_matches(expected : StyleNameMatcher?, actual : String?) -> Bool {
  match (expected, actual) {
    (Some(ExactStyleName(a)), Some(b)) => a.to_lower() == b.to_lower()
    (Some(PrefixStyleName(a)), Some(b)) => b.to_lower().has_prefix(a.to_lower())
    (Some(_), None) => false
    (None, _) => true
  }
}

///|
/// Finds the first style mapping that matches an element.
pub fn find_style_mapping(
  element : DocumentElement,
  style_map : Array[StyleMapping],
) -> StyleMapping? {
  for mapping in style_map {
    if mapping.from.matches(element) {
      return Some(mapping)
    }
  }
  None
}

///|
/// Returns Mammoth-compatible default style-map lines.
pub fn default_style_map_lines() -> Array[String] {
  [
    "p.Heading1 => h1:fresh", "p.Heading2 => h2:fresh", "p.Heading3 => h3:fresh",
    "p.Heading4 => h4:fresh", "p.Heading5 => h5:fresh", "p.Heading6 => h6:fresh",
    "p[style-name='Heading 1'] => h1:fresh", "p[style-name='Heading 2'] => h2:fresh",
    "p[style-name='Heading 3'] => h3:fresh", "p[style-name='Heading 4'] => h4:fresh",
    "p[style-name='Heading 5'] => h5:fresh", "p[style-name='Heading 6'] => h6:fresh",
    "p[style-name='heading 1'] => h1:fresh", "p[style-name='heading 2'] => h2:fresh",
    "p[style-name='heading 3'] => h3:fresh", "p[style-name='heading 4'] => h4:fresh",
    "p[style-name='heading 5'] => h5:fresh", "p[style-name='heading 6'] => h6:fresh",
    "p.Heading => h1:fresh", "p[style-name='Heading'] => h1:fresh", "r[style-name='Strong'] => strong",
    "p[style-name='footnote text'] => p:fresh", "r[style-name='footnote reference'] =>",
    "p[style-name='endnote text'] => p:fresh", "r[style-name='endnote reference'] =>",
    "p[style-name='annotation text'] => p:fresh", "r[style-name='annotation reference'] =>",
    "p[style-name='Footnote'] => p:fresh", "r[style-name='Footnote anchor'] =>",
    "p[style-name='Endnote'] => p:fresh", "r[style-name='Endnote anchor'] =>", "p:unordered-list(1) => ul > li:fresh",
    "p:unordered-list(2) => ul|ol > li > ul > li:fresh", "p:unordered-list(3) => ul|ol > li > ul|ol > li > ul > li:fresh",
    "p:unordered-list(4) => ul|ol > li > ul|ol > li > ul|ol > li > ul > li:fresh",
    "p:unordered-list(5) => ul|ol > li > ul|ol > li > ul|ol > li > ul|ol > li > ul > li:fresh",
    "p:ordered-list(1) => ol > li:fresh", "p:ordered-list(2) => ul|ol > li > ol > li:fresh",
    "p:ordered-list(3) => ul|ol > li > ul|ol > li > ol > li:fresh", "p:ordered-list(4) => ul|ol > li > ul|ol > li > ul|ol > li > ol > li:fresh",
    "p:ordered-list(5) => ul|ol > li > ul|ol > li > ul|ol > li > ul|ol > li > ol > li:fresh",
    "r[style-name='Hyperlink'] =>", "p[style-name='Normal'] => p:fresh", "p.Body => p:fresh",
    "p[style-name='Body'] => p:fresh",
  ]
}