///|
/// Parses an attribute list string (Ruby `AttributeList`).
priv struct AttributeListParser {
  src : String
  mut pos : Int
  block : Node?
  delimiter : String
  attributes : Attributes
}

///|
fn AttributeListParser::eos(self : AttributeListParser) -> Bool {
  self.pos >= self.src.length()
}

///|
fn AttributeListParser::peek(self : AttributeListParser) -> String {
  if self.pos < self.src.length() {
    self.src.unsafe_substring(start=self.pos, end=self.pos + 1)
  } else {
    ""
  }
}

///|
/// Reads one character (a whole code point).
fn AttributeListParser::get_char(self : AttributeListParser) -> String? {
  if self.pos >= self.src.length() {
    return None
  }
  let c = self.src[self.pos]
  let w = if c >= 0xD800 && c <= 0xDBFF && self.pos + 1 < self.src.length() {
    2
  } else {
    1
  }
  let s = self.src.unsafe_substring(start=self.pos, end=self.pos + w)
  self.pos += w
  Some(s)
}

///|
fn AttributeListParser::scan(
  self : AttributeListParser,
  rx : @regex.Regex,
) -> String? {
  match rx.match_at(self.src, self.pos) {
    Some(m) => {
      self.pos = m.end()
      Some(m.matched())
    }
    None => None
  }
}

///|
fn AttributeListParser::skip(
  self : AttributeListParser,
  rx : @regex.Regex,
) -> Int? {
  match rx.match_at(self.src, self.pos) {
    Some(m) => {
      let len = m.end() - m.begin()
      self.pos = m.end()
      Some(len)
    }
    None => None
  }
}

///|
let attribute_list_quot_boundary_rx : @regex.Regex = @regex.re(
  ".*?[^\\\\](?=\")",
)

///|
let attribute_list_apos_boundary_rx : @regex.Regex = @regex.re(
  ".*?[^\\\\](?=')",
)

///|
let attribute_list_comma_boundary_rx : @regex.Regex = @regex.re(
  ".*?(?=[ \\t]*(,|$))",
)

///|
let attribute_list_comma_skip_rx : @regex.Regex = @regex.re("[ \\t]*(,|$)")

///|
fn AttributeListParser::scan_to_delimiter(self : AttributeListParser) -> String {
  let rx = if self.delimiter == "," {
    attribute_list_comma_boundary_rx
  } else {
    @regex.re(".*?(?=[ \\t]*(\{regex_escape(self.delimiter)}|$))")
  }
  self.scan(rx).unwrap_or("")
}

///|
fn AttributeListParser::skip_delimiter(self : AttributeListParser) -> Unit {
  let rx = if self.delimiter == "," {
    attribute_list_comma_skip_rx
  } else {
    @regex.re("[ \\t]*(\{regex_escape(self.delimiter)}|$)")
  }
  self.skip(rx) |> ignore
}

///|
fn AttributeListParser::parse_attribute_value(
  self : AttributeListParser,
  quote : String,
) -> String {
  if self.peek() == quote {
    self.pos += 1
    return ""
  }
  let rx = if quote == "\"" {
    attribute_list_quot_boundary_rx
  } else {
    attribute_list_apos_boundary_rx
  }
  match self.scan(rx) {
    Some(value) => {
      self.pos += 1
      if value.contains("\\") {
        value.replace_all(old="\\" + quote, new=quote)
      } else {
        value
      }
    }
    None => "\{quote}\{self.scan_to_delimiter()}"
  }
}

///|
fn AttributeListParser::parse_attribute(
  self : AttributeListParser,
  index : Int,
  positional_attrs : Array[String?],
) -> Bool {
  let mut continue_ = true
  self.skip(attribute_list_blank_rx) |> ignore
  let mut name : String? = None
  let mut value : String? = None
  let mut single_quoted = false
  let first = self.peek()
  if first == "\"" {
    self.pos += 1
    name = Some(self.parse_attribute_value("\""))
  } else if first == "'" {
    self.pos += 1
    let n = self.parse_attribute_value("'")
    name = Some(n)
    if !n.has_prefix("'") {
      single_quoted = true
    }
  } else {
    name = self.scan(attribute_list_name_rx)
    let skipped = match name {
      Some(_) => self.skip(attribute_list_blank_rx).unwrap_or(0)
      None => 0
    }
    if self.eos() {
      if !(name is Some(_) || @rb.rstrip(self.src).has_suffix(self.delimiter)) {
        return false
      }
      continue_ = false
    } else {
      let c = self.get_char().unwrap()
      if c == self.delimiter {
        self.pos -= c.length()
      } else {
        match name {
          Some(n) =>
            if c == "=" {
              self.skip(attribute_list_blank_rx) |> ignore
              match self.get_char() {
                Some("\"") => value = Some(self.parse_attribute_value("\""))
                Some("'") => {
                  let v = self.parse_attribute_value("'")
                  value = Some(v)
                  if !v.has_prefix("'") {
                    single_quoted = true
                  }
                }
                Some(c2) if c2 == self.delimiter => {
                  value = Some("")
                  self.pos -= c2.length()
                }
                None => value = Some("")
                Some(c2) => {
                  let v = "\{c2}\{self.scan_to_delimiter()}"
                  if v == "None" {
                    return true
                  }
                  value = Some(v)
                }
              }
            } else {
              name = Some(
                "\{n}\{@rb.repeat(" ", skipped)}\{c}\{self.scan_to_delimiter()}",
              )
            }
          None => name = Some("\{c}\{self.scan_to_delimiter()}")
        }
      }
    }
  }
  match value {
    Some(v) => {
      let n = name.unwrap_or("")
      if n == "options" || n == "opts" {
        if v.contains(",") {
          let v2 = if v.contains(" ") { @rb.delete_chars(v, " ") } else { v }
          for opt in @rb.split(v2, ",") {
            if opt != "" {
              self.attributes.set_str("\{opt}-option", "")
            }
          }
        } else if v != "" {
          self.attributes.set_str("\{v}-option", "")
        }
      } else if single_quoted && self.block is Some(block) {
        if n == "title" || n == "reftext" {
          self.attributes.set_str(n, v)
        } else {
          self.attributes.set_str(n, block.apply_subs(v, normal_subs))
        }
      } else {
        self.attributes.set_str(n, v)
      }
    }
    None => {
      let name = match (name, self.block) {
        (Some(n), Some(block)) if single_quoted =>
          Some(block.apply_subs(n, normal_subs))
        _ => name
      }
      match (positional_attrs.get(index), name) {
        (Some(Some(pname)), Some(n)) => self.attributes.set_str(pname, n)
        _ => ()
      }
      self.attributes.set_pos(
        index + 1,
        match name {
          Some(n) => Str(n)
          None => Nil
        },
      )
    }
  }
  continue_
}

///|
/// Parses an attribute list into a new Attributes map.
pub fn parse_attribute_list(
  source : String,
  positional_attrs? : Array[String?] = [],
  block? : Node,
  delimiter? : String = ",",
) -> Attributes {
  let p : AttributeListParser = {
    src: source,
    pos: 0,
    block,
    delimiter,
    attributes: Attributes::new(),
  }
  let mut index = 0
  while p.parse_attribute(index, positional_attrs) {
    if p.eos() {
      break
    }
    p.skip_delimiter()
    index += 1
  }
  p.attributes
}

///|
/// Copies positional attributes to named keys (Ruby `AttributeList.rekey`).
pub fn rekey_attributes(
  attributes : Attributes,
  positional_attrs : Array[String?],
) -> Attributes {
  for index, key in positional_attrs {
    match key {
      Some(k) =>
        match attributes.get_pos(index + 1) {
          Some(v) if v.truthy() => attributes.set(k, v)
          _ => ()
        }
      None => ()
    }
  }
  attributes
}