///|
/// Border characters used to render a box around styled content.
pub(all) struct Border {
  top_left : String
  top_right : String
  bottom_left : String
  bottom_right : String
  horizontal : String
  vertical : String
} derive(Debug, Eq)

///|
/// Horizontal alignment within a styled width.
pub(all) enum Align {
  Left
  Center
  Right
} derive(Debug, Eq)

///|
/// Rich color representation used by the styling APIs.
pub(all) enum Color {
  Ansi(Int)
  RGB(Int, Int, Int)
  Hex(String)
  Adaptive(Color, Color)
} derive(Debug, Eq)

///|
/// Vertical alignment for layout helpers.
pub(all) enum VerticalAlign {
  Top
  Middle
  Bottom
} derive(Debug, Eq)

///|
/// A composable style description for terminal strings.
pub(all) struct Style {
  fg : Int?
  bg : Int?
  fg_ext : Color?
  bg_ext : Color?
  border_fg : Color?
  border_bg : Color?
  bold : Bool
  dim : Bool
  italic : Bool
  underline : Bool
  blink : Bool
  reverse : Bool
  strikethrough : Bool
  border : Border?
  border_top : Bool
  border_right : Bool
  border_bottom : Bool
  border_left : Bool
  padding_top : Int
  padding_bottom : Int
  padding_left : Int
  padding_right : Int
  margin_top : Int
  margin_bottom : Int
  margin_left : Int
  margin_right : Int
  width : Int
  height : Int?
  min_width : Int
  min_height : Int
  max_width : Int?
  max_height : Int?
  align : Align
} derive(Debug, Eq)

///|
pub fn border_normal() -> Border {
  {
    top_left: "┌",
    top_right: "┐",
    bottom_left: "└",
    bottom_right: "┘",
    horizontal: "─",
    vertical: "│",
  }
}

///|
pub fn border_rounded() -> Border {
  {
    top_left: "╭",
    top_right: "╮",
    bottom_left: "╰",
    bottom_right: "╯",
    horizontal: "─",
    vertical: "│",
  }
}

///|
pub fn border_thick() -> Border {
  {
    top_left: "┏",
    top_right: "┓",
    bottom_left: "┗",
    bottom_right: "┛",
    horizontal: "━",
    vertical: "┃",
  }
}

///|
pub fn border_double() -> Border {
  {
    top_left: "╔",
    top_right: "╗",
    bottom_left: "╚",
    bottom_right: "╝",
    horizontal: "═",
    vertical: "║",
  }
}

///|
pub fn border_ascii() -> Border {
  {
    top_left: "+",
    top_right: "+",
    bottom_left: "+",
    bottom_right: "+",
    horizontal: "-",
    vertical: "|",
  }
}

///|
pub fn color_ansi(n : Int) -> Color {
  Ansi(@internal.clamp_non_negative(n))
}

///|
pub fn color_rgb(r : Int, g : Int, b : Int) -> Color {
  RGB(clamp_channel(r), clamp_channel(g), clamp_channel(b))
}

///|
pub fn color_hex(hex : String) -> Color {
  Hex(hex)
}

///|
pub fn color_adaptive(light : Color, dark : Color) -> Color {
  Adaptive(light, dark)
}

///|
pub fn style() -> Style {
  {
    fg: None,
    bg: None,
    fg_ext: None,
    bg_ext: None,
    border_fg: None,
    border_bg: None,
    bold: false,
    dim: false,
    italic: false,
    underline: false,
    blink: false,
    reverse: false,
    strikethrough: false,
    border: None,
    border_top: true,
    border_right: true,
    border_bottom: true,
    border_left: true,
    padding_top: 0,
    padding_bottom: 0,
    padding_left: 0,
    padding_right: 0,
    margin_top: 0,
    margin_bottom: 0,
    margin_left: 0,
    margin_right: 0,
    width: 0,
    height: None,
    min_width: 0,
    min_height: 0,
    max_width: None,
    max_height: None,
    align: Left,
  }
}

///|
pub fn Style::fg_color(self : Style, n : Int) -> Style {
  let color = color_ansi(n)
  { ..self, fg: Some(@internal.clamp_non_negative(n)), fg_ext: Some(color) }
}

///|
pub fn Style::bg_color(self : Style, n : Int) -> Style {
  let color = color_ansi(n)
  { ..self, bg: Some(@internal.clamp_non_negative(n)), bg_ext: Some(color) }
}

///|
pub fn Style::foreground(self : Style, color : Color) -> Style {
  {
    ..self,
    fg_ext: Some(color),
    fg: match color {
      Ansi(n) => Some(@internal.clamp_non_negative(n))
      Adaptive(_, _) => None
      _ => None
    },
  }
}

///|
pub fn Style::background(self : Style, color : Color) -> Style {
  {
    ..self,
    bg_ext: Some(color),
    bg: match color {
      Ansi(n) => Some(@internal.clamp_non_negative(n))
      Adaptive(_, _) => None
      _ => None
    },
  }
}

///|
pub fn Style::foreground_rgb(self : Style, r : Int, g : Int, b : Int) -> Style {
  self.foreground(color_rgb(r, g, b))
}

///|
pub fn Style::background_rgb(self : Style, r : Int, g : Int, b : Int) -> Style {
  self.background(color_rgb(r, g, b))
}

///|
pub fn Style::foreground_hex(self : Style, hex : String) -> Style {
  self.foreground(color_hex(hex))
}

///|
pub fn Style::background_hex(self : Style, hex : String) -> Style {
  self.background(color_hex(hex))
}

///|
pub fn Style::border_foreground(self : Style, color : Color) -> Style {
  { ..self, border_fg: Some(color) }
}

///|
pub fn Style::border_background(self : Style, color : Color) -> Style {
  { ..self, border_bg: Some(color) }
}

///|
pub fn Style::bold(self : Style) -> Style {
  { ..self, bold: true }
}

///|
pub fn Style::dim(self : Style) -> Style {
  { ..self, dim: true }
}

///|
pub fn Style::faint(self : Style) -> Style {
  self.dim()
}

///|
pub fn Style::italic(self : Style) -> Style {
  { ..self, italic: true }
}

///|
pub fn Style::underline(self : Style) -> Style {
  { ..self, underline: true }
}

///|
pub fn Style::blink(self : Style) -> Style {
  { ..self, blink: true }
}

///|
pub fn Style::reverse(self : Style) -> Style {
  { ..self, reverse: true }
}

///|
pub fn Style::strikethrough(self : Style) -> Style {
  { ..self, strikethrough: true }
}

///|
pub fn Style::border(self : Style, b : Border) -> Style {
  { ..self, border: Some(b) }
}

///|
pub fn Style::border_sides(
  self : Style,
  top : Bool,
  right : Bool,
  bottom : Bool,
  left : Bool,
) -> Style {
  {
    ..self,
    border_top: top,
    border_right: right,
    border_bottom: bottom,
    border_left: left,
  }
}

///|
pub fn Style::padding(
  self : Style,
  top : Int,
  right : Int,
  bottom : Int,
  left : Int,
) -> Style {
  {
    ..self,
    padding_top: @internal.clamp_non_negative(top),
    padding_right: @internal.clamp_non_negative(right),
    padding_bottom: @internal.clamp_non_negative(bottom),
    padding_left: @internal.clamp_non_negative(left),
  }
}

///|
pub fn Style::margin(
  self : Style,
  top : Int,
  right : Int,
  bottom : Int,
  left : Int,
) -> Style {
  {
    ..self,
    margin_top: @internal.clamp_non_negative(top),
    margin_right: @internal.clamp_non_negative(right),
    margin_bottom: @internal.clamp_non_negative(bottom),
    margin_left: @internal.clamp_non_negative(left),
  }
}

///|
pub fn Style::width(self : Style, w : Int) -> Style {
  { ..self, width: @internal.clamp_non_negative(w) }
}

///|
pub fn Style::height(self : Style, h : Int) -> Style {
  { ..self, height: Some(@internal.clamp_non_negative(h)) }
}

///|
pub fn Style::min_width(self : Style, width : Int) -> Style {
  { ..self, min_width: @internal.clamp_non_negative(width) }
}

///|
pub fn Style::min_height(self : Style, height : Int) -> Style {
  { ..self, min_height: @internal.clamp_non_negative(height) }
}

///|
pub fn Style::max_width(self : Style, width : Int) -> Style {
  { ..self, max_width: Some(@internal.clamp_non_negative(width)) }
}

///|
pub fn Style::max_height(self : Style, height : Int) -> Style {
  { ..self, max_height: Some(@internal.clamp_non_negative(height)) }
}

///|
pub fn Style::align(self : Style, a : Align) -> Style {
  { ..self, align: a }
}

///|
pub fn Style::render(self : Style, content : String) -> String {
  self.render_with(content, @profile.ambient_context())
}

///|
pub fn Style::render_with(
  self : Style,
  content : String,
  ctx : @profile.RenderContext,
) -> String {
  let effective_style = {
    ..self,
    fg: self.fg.map(@internal.clamp_non_negative),
    bg: self.bg.map(@internal.clamp_non_negative),
    padding_top: @internal.clamp_non_negative(self.padding_top),
    padding_bottom: @internal.clamp_non_negative(self.padding_bottom),
    padding_left: @internal.clamp_non_negative(self.padding_left),
    padding_right: @internal.clamp_non_negative(self.padding_right),
    margin_top: @internal.clamp_non_negative(self.margin_top),
    margin_bottom: @internal.clamp_non_negative(self.margin_bottom),
    margin_left: @internal.clamp_non_negative(self.margin_left),
    margin_right: @internal.clamp_non_negative(self.margin_right),
    width: @internal.clamp_non_negative(self.width),
    height: self.height.map(@internal.clamp_non_negative),
    min_width: @internal.clamp_non_negative(self.min_width),
    min_height: @internal.clamp_non_negative(self.min_height),
    max_width: self.max_width.map(@internal.clamp_non_negative),
    max_height: self.max_height.map(@internal.clamp_non_negative),
  }
  let content_lines = split_lines(content)
  let (content_widths, natural_width) = measure_line_widths(content_lines)
  let target_width = resolve_width_from_natural(effective_style, natural_width)
  let base_lines = shape_content_lines_with_widths(
    content_lines,
    content_widths,
    target_width,
    effective_style.align,
  )
  let target_height = resolve_height(effective_style, base_lines.length())
  let height_fitted = fit_height(base_lines, target_width, target_height)
  let padded_width = target_width +
    effective_style.padding_left +
    effective_style.padding_right
  let padded = apply_padding_with_inner_width(
    height_fitted, effective_style, target_width,
  )
  let bordered = match effective_style.border {
    Some(border) =>
      render_border_block(border, padded, padded_width, effective_style, ctx)
    None => @internal.join_lines(padded)
  }
  let margined = apply_margin(
    bordered,
    effective_style.margin_top,
    effective_style.margin_right,
    effective_style.margin_bottom,
    effective_style.margin_left,
  )
  apply_style_prefix(effective_style, margined, ctx)
}

///|
pub fn join_horizontal(
  blocks : Array[String],
  valign? : VerticalAlign = Top,
) -> String {
  if blocks.is_empty() {
    return ""
  }
  let split_blocks : Array[Array[String]] = []
  let widths : Array[Int] = []
  let heights : Array[Int] = []
  let mut max_height = 0
  for block in blocks {
    let lines = split_lines(block)
    let width = @internal.max_visible_width(lines, visible_width)
    split_blocks.push(lines)
    widths.push(width)
    heights.push(lines.length())
    if lines.length() > max_height {
      max_height = lines.length()
    }
  }
  let rows : Array[String] = []
  for row in 0..= top_pad + heights[i] {
        buf.write_string(String::make(widths[i], ' '))
      } else {
        buf.write_string(fit_line_width(lines[row - top_pad], widths[i], Left))
      }
    }
    rows.push(buf.to_string())
  }
  @internal.join_lines(rows)
}

///|
pub fn join_vertical(blocks : Array[String], align? : Align = Left) -> String {
  if blocks.is_empty() {
    return ""
  }
  let split_blocks : Array[Array[String]] = []
  let mut max_width = 0
  for block in blocks {
    let lines = split_lines(block)
    let width = @internal.max_visible_width(lines, visible_width)
    if width > max_width {
      max_width = width
    }
    split_blocks.push(lines)
  }
  let rows : Array[String] = []
  for block_lines in split_blocks {
    for line in block_lines {
      rows.push(fit_line_width(line, max_width, align))
    }
  }
  @internal.join_lines(rows)
}

///|
pub fn place(
  width : Int,
  height : Int,
  content : String,
  align? : Align = Left,
  valign? : VerticalAlign = Top,
) -> String {
  let safe_width = @internal.clamp_non_negative(width)
  let safe_height = @internal.clamp_non_negative(height)
  let lines = split_lines(content)
  let trimmed : Array[String] = []
  for i in 0.. Int {
  if n < 0 {
    0
  } else if n > 255 {
    255
  } else {
    n
  }
}

///|
fn effective_foreground(style : Style) -> Color? {
  match style.fg_ext {
    Some(color) => Some(color)
    None => style.fg.map(color_ansi)
  }
}

///|
fn effective_background(style : Style) -> Color? {
  match style.bg_ext {
    Some(color) => Some(color)
    None => style.bg.map(color_ansi)
  }
}

///|
fn resolve_width_from_natural(style : Style, natural : Int) -> Int {
  let from_width = if style.width > 0 { style.width } else { natural }
  let with_min = from_width.max(style.min_width)
  match style.max_width {
    Some(max_width) => with_min.min(max_width)
    None => with_min
  }
}

///|
fn measure_line_widths(lines : Array[String]) -> (Array[Int], Int) {
  let widths : Array[Int] = []
  let mut max_width = 0
  for line in lines {
    let width = style_visible_width(line)
    widths.push(width)
    if width > max_width {
      max_width = width
    }
  }
  (widths, max_width)
}

///|
fn style_visible_width(line : String) -> Int {
  let mut width = 0
  for ch in line {
    let code = ch.to_int()
    if code < 0x20 || code >= 0x7F {
      return visible_width(line)
    }
    width = width + 1
  }
  width
}

///|
fn resolve_height(style : Style, line_count : Int) -> Int {
  let from_height = match style.height {
    Some(height) => height
    None => line_count
  }
  let with_min = from_height.max(style.min_height)
  match style.max_height {
    Some(max_height) => with_min.min(max_height)
    None => with_min
  }
}

///|
fn shape_content_lines_with_widths(
  lines : Array[String],
  widths : Array[Int],
  target_width : Int,
  align : Align,
) -> Array[String] {
  let shaped : Array[String] = []
  for i, line in lines {
    shaped.push(fit_line_width_known(line, widths[i], target_width, align))
  }
  shaped
}

///|
fn fit_line_width(line : String, target_width : Int, align : Align) -> String {
  let width = style_visible_width(line)
  fit_line_width_known(line, width, target_width, align)
}

///|
fn fit_line_width_known(
  line : String,
  width : Int,
  target_width : Int,
  align : Align,
) -> String {
  if target_width <= 0 || width >= target_width {
    let truncated = truncate(line, target_width)
    let truncated_width = style_visible_width(truncated)
    return pad_line_known_width(truncated, truncated_width, target_width, align)
  }
  pad_line_known_width(line, width, target_width, align)
}

///|
fn pad_line_known_width(
  line : String,
  width : Int,
  target_width : Int,
  align : Align,
) -> String {
  if width >= target_width {
    return line
  }
  let total_pad = target_width - width
  match align {
    Left => pad_right_known_width(line, width, target_width)
    Center => {
      let left_pad = total_pad / 2
      let right_pad = total_pad - left_pad
      write_padded_line(
        String::make(left_pad, ' '),
        line,
        String::make(right_pad, ' '),
      )
    }
    Right => write_padded_line(String::make(total_pad, ' '), line, "")
  }
}

///|
fn pad_right_known_width(
  line : String,
  width : Int,
  target_width : Int,
) -> String {
  if width >= target_width {
    return line
  }
  if line.contains("\u001b") {
    return pad_right(line, target_width)
  }
  line + String::make(target_width - width, ' ')
}

///|
fn write_padded_line(left : String, line : String, right : String) -> String {
  if left == "" && right == "" {
    return line
  }
  let buf = StringBuilder::new(
    size_hint=left.length() + line.length() + right.length(),
  )
  buf.write_string(left)
  buf.write_string(line)
  buf.write_string(right)
  buf.to_string()
}

///|
fn fit_height(
  lines : Array[String],
  target_width : Int,
  target_height : Int,
) -> Array[String] {
  let fitted : Array[String] = []
  for i in 0.. Array[String] {
  let padded : Array[String] = []
  let row_width = inner_width + self.padding_left + self.padding_right
  let blank = String::make(row_width, ' ')
  let left = String::make(self.padding_left, ' ')
  let right = String::make(self.padding_right, ' ')
  for _ in 0.. String {
  if top == 0 && right == 0 && bottom == 0 && left == 0 {
    return block
  }
  let lines = split_lines(block)
  let (widths, inner_width) = measure_line_widths(lines)
  let full_width = inner_width + left + right
  let blank = String::make(full_width, ' ')
  let left_pad = String::make(left, ' ')
  let right_pad = String::make(right, ' ')
  let rows : Array[String] = []
  for _ in 0.. String {
  if style.border_fg is None && style.border_bg is None {
    return @internal.render_border(
      border.top_left,
      border.top_right,
      border.bottom_left,
      border.bottom_right,
      border.horizontal,
      border.vertical,
      lines,
      inner_width,
      style_visible_width,
      style.border_top,
      style.border_right,
      style.border_bottom,
      style.border_left,
    )
  }
  let top_left = @internal.normalize_border_piece(
    border.top_left,
    style_visible_width,
  )
  let top_right = @internal.normalize_border_piece(
    border.top_right,
    style_visible_width,
  )
  let bottom_left = @internal.normalize_border_piece(
    border.bottom_left,
    style_visible_width,
  )
  let bottom_right = @internal.normalize_border_piece(
    border.bottom_right,
    style_visible_width,
  )
  let horizontal_piece = @internal.normalize_border_piece(
    border.horizontal,
    style_visible_width,
  )
  let vertical_piece = @internal.normalize_border_piece(
    border.vertical,
    style_visible_width,
  )
  let horizontal = @internal.repeat_string(horizontal_piece, inner_width)
  let rows : Array[String] = []
  let border_prefix = build_border_ansi_prefix(style, ctx)
  let content_prefix = build_ansi_prefix(style, ctx)
  if style.border_top {
    rows.push(
      decorate_segment(
        border_prefix,
        (if style.border_left { top_left } else { "" }) +
        horizontal +
        (if style.border_right { top_right } else { "" }),
      ),
    )
  }
  for line in lines {
    rows.push(
      decorate_segment(
        border_prefix,
        if style.border_left {
          vertical_piece
        } else {
          ""
        },
      ) +
      decorate_segment(content_prefix, line) +
      decorate_segment(
        border_prefix,
        if style.border_right {
          vertical_piece
        } else {
          ""
        },
      ),
    )
  }
  if style.border_bottom {
    rows.push(
      decorate_segment(
        border_prefix,
        (if style.border_left { bottom_left } else { "" }) +
        horizontal +
        (if style.border_right { bottom_right } else { "" }),
      ),
    )
  }
  @internal.join_lines(rows)
}

///|
fn decorate_segment(prefix : String, segment : String) -> String {
  if prefix == "" || segment == "" {
    segment
  } else {
    prefix + segment + reset()
  }
}

///|
fn build_border_ansi_prefix(
  style : Style,
  ctx : @profile.RenderContext,
) -> String {
  build_ansi_prefix_with_colors(
    style,
    ctx,
    match style.border_fg {
      Some(color) => Some(color)
      None => effective_foreground(style)
    },
    match style.border_bg {
      Some(color) => Some(color)
      None => effective_background(style)
    },
  )
}

///|
fn apply_style_prefix(
  style : Style,
  block : String,
  ctx : @profile.RenderContext,
) -> String {
  if style.border is Some(_) &&
    (style.border_fg is Some(_) || style.border_bg is Some(_)) {
    return block
  }
  let prefix = build_ansi_prefix(style, ctx)
  if prefix == "" {
    block
  } else {
    prefix + block + reset()
  }
}

///|
fn build_ansi_prefix(style : Style, ctx : @profile.RenderContext) -> String {
  build_ansi_prefix_with_colors(
    style,
    ctx,
    effective_foreground(style),
    effective_background(style),
  )
}

///|
fn build_ansi_prefix_with_colors(
  style : Style,
  ctx : @profile.RenderContext,
  fg : Color?,
  bg : Color?,
) -> String {
  let buf = StringBuilder::new()
  fg
  .iter()
  .each(fn(color) { buf.write_string(foreground_sequence(ctx, color)) })
  bg
  .iter()
  .each(fn(color) { buf.write_string(background_sequence(ctx, color)) })
  if style.bold {
    buf.write_string(bold())
  }
  if style.dim {
    buf.write_string(dim())
  }
  if style.italic {
    buf.write_string("\{CSI}3m")
  }
  if style.underline {
    buf.write_string(underline())
  }
  if style.blink {
    buf.write_string(blink())
  }
  if style.reverse {
    buf.write_string(reverse())
  }
  if style.strikethrough {
    buf.write_string(strikethrough())
  }
  buf.to_string()
}

///|
fn color_value(
  ctx : @profile.RenderContext,
  color : Color,
) -> @profile.ColorValue? {
  match color {
    Ansi(n) => Some(@profile.Palette(@internal.clamp_non_negative(n)))
    RGB(r, g, b) => Some(@profile.Rgb(r, g, b))
    Hex(hex) =>
      match hex_to_rgb(hex) {
        Some((r, g, b)) => Some(@profile.Rgb(r, g, b))
        None => None
      }
    Adaptive(light, dark) =>
      match ctx.background {
        Light => color_value(ctx, light)
        Dark | Unknown => color_value(ctx, dark)
      }
  }
}

///|
fn foreground_sequence(ctx : @profile.RenderContext, color : Color) -> String {
  match color_value(ctx, color) {
    Some(value) => @profile.render_fg(ctx, value)
    None => ""
  }
}

///|
fn background_sequence(ctx : @profile.RenderContext, color : Color) -> String {
  match color_value(ctx, color) {
    Some(value) => @profile.render_bg(ctx, value)
    None => ""
  }
}

///|
fn hex_to_rgb(hex : String) -> (Int, Int, Int)? {
  let view = if hex.length() > 0 && hex[0] == '#' { hex[:][1:] } else { hex[:] }
  let normalized = if view.length() == 3 {
    let buf = StringBuilder::new(size_hint=6)
    for ch in view.iter() {
      buf.write_char(ch)
      buf.write_char(ch)
    }
    Some(buf.to_string())
  } else if view.length() == 6 {
    Some(view.to_owned())
  } else {
    None
  }
  match normalized {
    Some(value) => {
      let parts = value[:]
      let r = try? @string.parse_uint(parts[0:2], base=16)
      let g = try? @string.parse_uint(parts[2:4], base=16)
      let b = try? @string.parse_uint(parts[4:6], base=16)
      match (r, g, b) {
        (Ok(rv), Ok(gv), Ok(bv)) =>
          Some(
            (
              rv.reinterpret_as_int(),
              gv.reinterpret_as_int(),
              bv.reinterpret_as_int(),
            ),
          )
        _ => None
      }
    }
    None => None
  }
}

///|
fn vertical_offset(
  container : Int,
  content : Int,
  align : VerticalAlign,
) -> Int {
  if content >= container {
    return 0
  }
  match align {
    Top => 0
    Middle => (container - content) / 2
    Bottom => container - content
  }
}