///|
// Canonical SGR state and emission order follow ansi-str 0.9, used by tabled.
// See THIRD_PARTY_NOTICES.md for the original license notices.
priv struct SgrState {
  flags : Array[Bool]
  mut foreground : String?
  mut background : String?
  mut underline_color : String?
  mut font : Int?
  mut reset : Bool
  mut unknown : Bool
}

///|
fn SgrState::new() -> SgrState {
  {
    flags: Array::make(76, false),
    foreground: None,
    background: None,
    underline_color: None,
    font: None,
    reset: false,
    unknown: false,
  }
}

///|
fn SgrState::has_any(self : SgrState) -> Bool {
  self.foreground is Some(_) ||
  self.background is Some(_) ||
  self.underline_color is Some(_) ||
  self.flags.any(value => value) ||
  (self.reset && self.unknown)
}

///|
fn sgr_number(text : String) -> Int? {
  guard !text.is_empty() else { return None }
  let chars = text.to_array()
  let start = if chars[0] == '+' { 1 } else { 0 }
  guard start < chars.length() else { return None }
  let mut value = 0
  for index in start..= '0' && ch <= '9' else { return None }
    value = value * 10 + ch.to_int() - '0'.to_int()
    guard value <= 255 else { return None }
  }
  Some(value)
}

///|
fn sgr_extended(
  parts : Array[String],
  index : Int,
  code : Int,
) -> (String?, Int) {
  guard index < parts.length() else { return (None, index) }
  let count = match parts[index] {
    "2" => 3
    "5" => 1
    _ => 0
  }
  guard count > 0 else { return (None, index + 1) }
  guard index + count < parts.length() else { return (None, parts.length()) }
  let values = Array::makei(count, offset => {
    sgr_number(parts[index + offset + 1]).unwrap_or(0).to_string()
  })
  (Some("\{code};\{parts[index]};" + values.join(";")), index + count + 1)
}

///|
fn SgrState::update(self : SgrState, sequence : String) -> Unit {
  guard sequence.has_prefix("\u001b[") && sequence.has_suffix("m") else {
    return
  }
  let parts = sequence[2:sequence.length() - 1]
    .split(";")
    .map(StringView::to_owned)
    .collect()
  let mut index = 0
  while index < parts.length() {
    let part = parts[index]
    index += 1
    let code = match sgr_number(part) {
      Some(code) if part == code.to_string() => code
      _ => {
        self.unknown = true
        continue
      }
    }
    match code {
      0 => {
        for i in 0..
        self.flags[code] = true
      10 => self.font = None
      11..=19 => self.font = Some(code)
      22 => {
        self.flags[1] = false
        self.flags[2] = false
      }
      23 => self.flags[3] = false
      24 => {
        self.flags[4] = false
        self.flags[21] = false
      }
      25 => {
        self.flags[5] = false
        self.flags[6] = false
      }
      27..=29 => self.flags[code - 20] = false
      30..=37 | 90..=97 => self.foreground = Some(part)
      40..=47 | 100..=107 => self.background = Some(part)
      38 | 48 | 58 => {
        let (color, next) = sgr_extended(parts, index, code)
        index = next
        if color is Some(_) {
          match code {
            38 => self.foreground = color
            48 => self.background = color
            _ => self.underline_color = color
          }
        }
      }
      39 => self.foreground = None
      49 => self.background = None
      50 => self.flags[26] = false
      54 => {
        self.flags[51] = false
        self.flags[52] = false
      }
      55 => self.flags[53] = false
      59 => self.underline_color = None
      65 =>
        for flag in 60..<=64 {
          self.flags[flag] = false
        }
      75 => {
        self.flags[73] = false
        self.flags[74] = false
      }
      _ => self.unknown = true
    }
  }
}

///|
fn sgr(code : String) -> String {
  "\u001b[" + code + "m"
}

///|
fn SgrState::prefix(self : SgrState) -> String {
  let out = StringBuilder()
  for code in [1, 2, 3, 4, 5, 6, 7, 8, 9] {
    if self.flags[code] {
      out.write_string(sgr(code.to_string()))
    }
  }
  if self.font is Some(font) {
    out.write_string(sgr(font.to_string()))
  }
  for code in [20, 21, 26] {
    if self.flags[code] {
      out.write_string(sgr(code.to_string()))
    }
  }
  for color in [self.foreground, self.background, self.underline_color] {
    if color is Some(code) {
      out.write_string(sgr(code))
    }
  }
  for code in [51, 52, 53, 60, 61, 62, 63, 64, 73, 74] {
    if self.flags[code] {
      out.write_string(sgr(code.to_string()))
    }
  }
  out.to_string()
}

///|
fn SgrState::suffix(self : SgrState) -> String {
  let out = StringBuilder()
  let codes = [
    (self.unknown && self.reset, 0),
    (self.font is Some(_), 10),
    (self.flags[1] || self.flags[2], 22),
    (self.flags[3] || self.flags[20], 23),
    (self.flags[4] || self.flags[21], 24),
    (self.flags[5] || self.flags[6], 25),
    (self.flags[7], 27),
    (self.flags[8], 28),
    (self.flags[9], 29),
    (self.foreground is Some(_), 39),
    (self.background is Some(_), 49),
    (self.flags[26], 50),
    (self.flags[51] || self.flags[52], 54),
    (self.flags[53], 55),
    (
      self.flags[60] ||
      self.flags[61] ||
      self.flags[62] ||
      self.flags[63] ||
      self.flags[64],
      65,
    ),
    (self.underline_color is Some(_), 59),
    (self.flags[73] || self.flags[74], 75),
    (self.unknown, 0),
  ]
  for pair in codes {
    if pair.0 {
      out.write_string(sgr(pair.1.to_string()))
    }
  }
  out.to_string()
}

///|
/// Extract the canonical style of the first text block, as in tabled.
/// Empty input or input containing only control sequences has no text block.
pub fn ANSIBuf::try_parse(text : String) -> Result[ANSIBuf, Unit] {
  let state = SgrState::new()
  for token in ansi_tokens(text) {
    match token.kind {
      Sgr => state.update(token.text)
      Text =>
        return Ok(
          if state.has_any() {
            ANSIBuf::new(state.prefix(), state.suffix())
          } else {
            ANSIBuf::empty()
          },
        )
      _ => ()
    }
  }
  Err(())
}

///|
/// Split lines while closing and reopening active SGR styles at each boundary.
/// Empty lines stay empty, and trailing newlines produce a final empty line.
pub fn get_lines(text : String) -> Array[String] {
  let state = SgrState::new()
  split_lines(text).map(raw => {
    if raw.is_empty() {
      return raw
    }
    let part = if state.has_any() { state.prefix() + raw } else { raw }
    for token in ansi_tokens(part) {
      if token.kind is Sgr {
        state.update(token.text)
      }
    }
    if state.has_any() {
      part + state.suffix()
    } else {
      part
    }
  })
}