// Port of rich/ansi.py.

///|
let re_ansi : @regex.Regex = @regex.compile(
  "\n(?:\u{1b}[0-?])|\n(?:\u{1b}\\](.*?)\u{1b}\\\\)|\n(?:\u{1b}([(@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~]))\n",
  flags=@regex.VERBOSE,
) catch {
  _ => abort("bad ansi regex")
}

///|
priv struct AnsiToken {
  plain : String
  sgr : String?
  osc : String?
}

///|
fn ansi_tokenize(ansi_text : String) -> Array[AnsiToken] {
  let out = []
  let mut position = 0
  let matches = re_ansi.find_all(ansi_text) catch { _ => [] }
  for m in matches {
    let start = m.start()
    let end = m.end()
    let osc = m.group(1)
    let sgr = m.group(2)
    if start > position {
      out.push({
        plain: py_slice(ansi_text, position, start),
        sgr: Some(""),
        osc: Some(""),
      })
    }
    match sgr {
      Some(s) if s != "" => {
        if s == "(" {
          position = end + 1
          continue
        }
        if s.has_suffix("m") {
          out.push({
            plain: "",
            sgr: Some(py_slice(s, 1, s.length() - 1)),
            osc,
          })
        }
      }
      _ => out.push({ plain: "", sgr, osc, })
    }
    position = end
  }
  if position < ansi_text.length() {
    out.push({
      plain: py_slice(ansi_text, position, ansi_text.length()),
      sgr: Some(""),
      osc: Some(""),
    })
  }
  out
}

///|
let sgr_style_map : Map[Int, String] = {
  1: "bold",
  2: "dim",
  3: "italic",
  4: "underline",
  5: "blink",
  6: "blink2",
  7: "reverse",
  8: "conceal",
  9: "strike",
  21: "underline2",
  22: "not dim not bold",
  23: "not italic",
  24: "not underline",
  25: "not blink",
  26: "not blink2",
  27: "not reverse",
  28: "not conceal",
  29: "not strike",
  30: "color(0)",
  31: "color(1)",
  32: "color(2)",
  33: "color(3)",
  34: "color(4)",
  35: "color(5)",
  36: "color(6)",
  37: "color(7)",
  39: "default",
  40: "on color(0)",
  41: "on color(1)",
  42: "on color(2)",
  43: "on color(3)",
  44: "on color(4)",
  45: "on color(5)",
  46: "on color(6)",
  47: "on color(7)",
  49: "on default",
  51: "frame",
  52: "encircle",
  53: "overline",
  54: "not frame not encircle",
  55: "not overline",
  90: "color(8)",
  91: "color(9)",
  92: "color(10)",
  93: "color(11)",
  94: "color(12)",
  95: "color(13)",
  96: "color(14)",
  97: "color(15)",
  100: "on color(8)",
  101: "on color(9)",
  102: "on color(10)",
  103: "on color(11)",
  104: "on color(12)",
  105: "on color(13)",
  106: "on color(14)",
  107: "on color(15)",
}

///|
/// Translate ANSI codes in to styled Text.
pub struct AnsiDecoder {
  priv mut style : Style
}

///|
pub fn AnsiDecoder::new() -> AnsiDecoder {
  { style: Style::null(), }
}

///|
/// Decode ANSI codes in terminal output, one Text per line.
pub fn AnsiDecoder::decode(
  self : AnsiDecoder,
  terminal_text : String,
) -> Array[Text] {
  // re.split(r"(?<=\n)", text)
  let lines = []
  let mut start = 0
  for i in 0.. {
    let mut end = line.length()
    while end > 0 && line[end - 1] == '\n' {
      end -= 1
    }
    self.decode_line(py_slice(line, 0, end))
  })
}

///|
fn is_py_digit_str(s : String) -> Bool {
  s != "" && s.iter().all(c => c >= '0' && c <= '9')
}

///|
/// Decode a line containing ANSI codes.
pub fn AnsiDecoder::decode_line(self : AnsiDecoder, line : String) -> Text {
  let text = Text::new()
  let line = match line.rev_find("\r") {
    Some(i) => py_slice(line, i + 1, line.length())
    None => line
  }
  for token in ansi_tokenize(line) {
    if token.plain != "" {
      text.append_str(
        token.plain,
        style?=if self.style.to_bool() { Some(self.style) } else { None },
      )
      |> ignore
    } else if token.osc is Some(osc) {
      if osc.has_prefix("8;") {
        let (_, semicolon, link) = partition(
          py_slice(osc, 2, osc.length()),
          ";",
        )
        if semicolon {
          self.style = self.style.update_link(
            if link == "" {
              None
            } else {
              Some(link)
            },
          )
        }
      }
    } else if token.sgr is Some(sgr) {
      let codes = []
      for code in sgr.split(";") {
        let code = code.to_owned()
        if code == "" {
          codes.push(0)
        } else if is_py_digit_str(code) {
          codes.push(@cmp.minimum(255, parse_int_or(code, 255)))
        }
      }
      let mut i = 0
      fn next() -> Int? {
        if i < codes.length() {
          let c = codes[i]
          i += 1
          Some(c)
        } else {
          None
        }
      }

      while next() is Some(code) {
        if code == 0 {
          self.style = Style::null()
        } else if sgr_style_map.get(code) is Some(definition) {
          self.style = self.style +
            (Style::parse(definition) catch { _ => Style::null() })
        } else if code == 38 || code == 48 {
          let foreground = code == 38
          match next() {
            Some(5) =>
              if next() is Some(n) {
                let c = @color.Color::from_ansi(n)
                self.style = self.style +
                  (if foreground {
                    Style::from_color(color=c)
                  } else {
                    Style::from_color(bgcolor=c)
                  })
              }
            Some(2) =>
              match (next(), next(), next()) {
                (Some(r), Some(g), Some(b)) => {
                  let c = @color.Color::from_rgb(
                    r.to_double(),
                    g.to_double(),
                    b.to_double(),
                  )
                  self.style = self.style +
                    (if foreground {
                      Style::from_color(color=c)
                    } else {
                      Style::from_color(bgcolor=c)
                    })
                }
                _ => ()
              }
            _ => ()
          }
        }
      }
    }
  }
  text
}