// Port of `Console.export_svg` (rich/console.py) and the SVG template
// from rich/_export_format.py.

///|
/// The default SVG template.
pub let console_svg_format : String =
  #|
  #|    
  #|    
  #|
  #|    
  #|    
  #|      
  #|    
  #|    {lines}
  #|    
  #|
  #|    {chrome}
  #|    
  #|    {backgrounds}
  #|    
  #|    {matrix}
  #|    
  #|    
  #|
  #|

///|
/// zlib's Adler-32 checksum.
pub fn adler32(data : Bytes) -> UInt {
  let mod_adler = 65521U
  let mut a = 1U
  let mut b = 0U
  for byte in data {
    a = (a + byte.to_uint()) % mod_adler
    b = (b + a) % mod_adler
  }
  (b << 16) | a
}

///|
/// A value for an SVG attribute: ints are printed as is, floats with
/// Python's `format(value, "g")`.
priv enum SvgValue {
  I(Int)
  F(Double)
  S(String)
}

///|
fn SvgValue::stringify(self : SvgValue) -> String {
  match self {
    I(i) => i.to_string()
    F(f) => py_format_g(f)
    S(s) => s
  }
}

///|
fn make_tag(
  name : String,
  attribs : Array[(String, SvgValue)],
  content? : String,
) -> String {
  let tag_attribs = attribs
    .map(kv => {
      let mut k = kv.0
      while k.strip_prefix("_") is Some(rest) {
        k = rest.to_owned()
      }
      "\{k.replace_all(old="_", new="-")}=\"\{kv.1.stringify()}\""
    })
    .join(" ")
  match content {
    Some(c) if c != "" => "<\{name} \{tag_attribs}>\{c}"
    _ => "<\{name} \{tag_attribs}/>"
  }
}

///|
fn svg_escape_text(text : String) -> String {
  html_escape(text).replace_all(old=" ", new=" ")
}

///|
/// Generate an SVG from the console contents (requires `record=true`).
pub fn Console::export_svg(
  self : Console,
  title? : String = "Rich",
  theme? : @color.TerminalTheme,
  clear? : Bool = true,
  code_format? : String = console_svg_format,
  font_aspect_ratio? : Double = 0.61,
  unique_id? : String,
) -> String {
  let theme = theme.unwrap_or(@color.svg_export_theme)
  let style_cache : Map[Style, String] = Map([])
  fn get_svg_style(style : Style) -> String {
    match style_cache.get(style) {
      Some(css) => return css
      None => ()
    }
    let css_rules = []
    let mut color = match style.color() {
      Some(c) if !c.is_default() => c.get_truecolor(theme~)
      _ => theme.foreground_color
    }
    let mut bgcolor = match style.bgcolor() {
      Some(c) if !c.is_default() => c.get_truecolor(theme~)
      _ => theme.background_color
    }
    if style.reverse() is Some(true) {
      let tmp = color
      color = bgcolor
      bgcolor = tmp
    }
    if style.dim() is Some(true) {
      color = @color.blend_rgb(color, bgcolor, cross_fade=0.4)
    }
    css_rules.push("fill: \{color.hex()}")
    if style.bold() is Some(true) {
      css_rules.push("font-weight: bold")
    }
    if style.italic() is Some(true) {
      css_rules.push("font-style: italic;")
    }
    if style.underline() is Some(true) {
      css_rules.push("text-decoration: underline;")
    }
    if style.strike() is Some(true) {
      css_rules.push("text-decoration: line-through;")
    }
    let css = css_rules.join(";")
    style_cache[style] = css
    css
  }

  let width = self.width()
  let char_height = 20
  let char_width = char_height.to_double() * font_aspect_ratio
  let line_height = char_height.to_double() * 1.22
  let margin_top = 1
  let margin_right = 1
  let margin_bottom = 1
  let margin_left = 1
  let padding_top = 40
  let padding_right = 8
  let padding_bottom = 8
  let padding_left = 8
  let padding_width = padding_left + padding_right
  let padding_height = padding_top + padding_bottom
  let margin_width = margin_left + margin_right
  let margin_height = margin_top + margin_bottom
  let text_backgrounds = []
  let text_group = []
  let classes : Map[String, Int] = Map([])
  let mut style_no = 1
  let segments = Segment::filter_control(self.record_buffer)
  if clear {
    self.record_buffer.clear()
  }
  let unique_id = match unique_id {
    Some(u) => u
    None => {
      let reprs = segments.map(s => s.repr()).join("")
      let data = @utf8.encode(reprs + title)
      "terminal-\{adler32(data)}"
    }
  }
  let mut y = 0
  for line_index, line in Segment::split_and_crop_lines(segments, width) {
    y = line_index
    let mut x = 0
    for segment in line {
      let style = match segment.style {
        Some(s) => s
        None => Style::new()
      }
      let text = segment.text
      let rules = get_svg_style(style)
      if classes.get(rules) is None {
        classes[rules] = style_no
        style_no += 1
      }
      let class_name = "r\{classes[rules]}"
      let (has_background, background) = if style.reverse() is Some(true) {
        (
          true,
          match style.color() {
            None => theme.foreground_color.hex()
            Some(c) => c.get_truecolor(theme~).hex()
          },
        )
      } else {
        let bgcolor = style.bgcolor()
        (
          bgcolor is Some(c) && !c.is_default(),
          match bgcolor {
            None => theme.background_color.hex()
            Some(c) => c.get_truecolor(theme~, foreground=false).hex()
          },
        )
      }
      let text_length = @cells.cell_len(text)
      if has_background {
        text_backgrounds.push(
          make_tag("rect", [
            ("fill", S(background)),
            ("x", F(x.to_double() * char_width)),
            ("y", F(y.to_double() * line_height + 1.5)),
            ("width", F(char_width * text_length.to_double())),
            ("height", F(line_height + 0.25)),
            ("shape_rendering", S("crispEdges")),
          ]),
        )
      }
      let code_points = text.char_length()
      if text != " ".repeat(code_points) {
        text_group.push(
          make_tag(
            "text",
            [
              ("_class", S("\{unique_id}-\{class_name}")),
              ("x", F(x.to_double() * char_width)),
              ("y", F(y.to_double() * line_height + char_height.to_double())),
              ("textLength", F(char_width * code_points.to_double())),
              ("clip_path", S("url(#\{unique_id}-line-\{y})")),
            ],
            content=svg_escape_text(text),
          ),
        )
      }
      x += @cells.cell_len(text)
    }
  }
  let lines = []
  for line_no in 0..\n    \{rect}\n            ",
    )
  }
  let styles = []
  for css, rule_no in classes {
    styles.push(".\{unique_id}-r\{rule_no} { \{css} }")
  }
  let backgrounds = text_backgrounds.join("")
  let matrix = text_group.join("")
  let terminal_width = (width.to_double() * char_width +
    padding_width.to_double())
    .ceil()
    .to_int()
  let terminal_height = (y + 1).to_double() * line_height +
    padding_height.to_double()
  let mut chrome = make_tag("rect", [
    ("fill", S(theme.background_color.hex())),
    ("stroke", S("rgba(255,255,255,0.35)")),
    ("stroke_width", S("1")),
    ("x", I(margin_left)),
    ("y", I(margin_top)),
    ("width", I(terminal_width)),
    ("height", F(terminal_height)),
    ("rx", I(8)),
  ])
  let title_color = theme.foreground_color.hex()
  if title != "" {
    chrome = chrome +
      make_tag(
        "text",
        [
          ("_class", S("\{unique_id}-title")),
          ("fill", S(title_color)),
          ("text_anchor", S("middle")),
          ("x", I(terminal_width / 2)),
          ("y", I(margin_top + char_height + 6)),
        ],
        content=svg_escape_text(title),
      )
  }
  chrome = chrome +
    (
      #|
      #|            
      #|            
      #|            
      #|            
      #|            
      #|        
    )
  py_format(code_format, {
    "unique_id": unique_id,
    "char_width": py_float_repr(char_width),
    "char_height": char_height.to_string(),
    "line_height": py_float_repr(line_height),
    "terminal_width": py_float_repr(char_width * width.to_double() - 1.0),
    "terminal_height": py_float_repr((y + 1).to_double() * line_height - 1.0),
    "width": (terminal_width + margin_width).to_string(),
    "height": py_float_repr(terminal_height + margin_height.to_double()),
    "terminal_x": (margin_left + padding_left).to_string(),
    "terminal_y": (margin_top + padding_top).to_string(),
    "styles": styles.join("\n"),
    "chrome": chrome,
    "backgrounds": backgrounds,
    "matrix": matrix,
    "lines": lines.join("\n"),
  })
}

///|
/// Generate an SVG from the console contents and save it to a file.
pub fn Console::save_svg(
  self : Console,
  path : String,
  title? : String = "Rich",
  theme? : @color.TerminalTheme,
  clear? : Bool = true,
  code_format? : String = console_svg_format,
  font_aspect_ratio? : Double = 0.61,
  unique_id? : String,
) -> Unit raise {
  let svg = self.export_svg(
    title~,
    theme?,
    clear~,
    code_format~,
    font_aspect_ratio~,
    unique_id?,
  )
  if !@term.write_file(path, svg) {
    raise WriteError("unable to write \{path}")
  }
}