///|
/// Basic ANSI color names used by the public color model.
pub enum BasicColor {
  Black
  Red
  Green
  Yellow
  Blue
  Magenta
  Cyan
  White
} derive(Eq, Show)

///|
/// Public color abstraction for terminal styling.
pub enum Color {
  Default
  Ansi(BasicColor, Bool)
  Rgb(Int, Int, Int)
} derive(Eq, Show)

///|
/// Style metadata attached to text or braille content.
pub struct Style {
  fg : Color
  bg : Color
} derive(Eq, Show)

///|
/// Rendering controls for terminal output.
pub struct RenderOptions {
  line_ending : String
  enable_ansi : Bool
  reset_each_line : Bool
} derive(Eq, Show)

///|
/// Logical pixel and text surface.
///
/// Use a Map to store cell content. Each key is (cell_x, cell_y).
/// Each cell contains a bitmask for braille, optional text, and optional style.
pub struct Canvas {
  cells : Map[(Int, Int), Cell]
}

///|
struct Cell {
  mut braille : Int // 8-bit mask
  mut text : String
  mut style : Style
}

///|
test "braille_mapping" {
  let c = Canvas::new()
  c.set(0, 0)
  c.set(1, 0)
  c.set(0, 1)
  c.set(1, 1)
  c.set(0, 2)
  c.set(1, 2)
  c.set(0, 3)
  c.set(1, 3)
  let s = c.render(options={ ..default_render_options(), enable_ansi: false })
  // All dots in a 2x4 cell should be 0x28FF
  assert_eq(s, "\u28FF\n")
}

///|
test "coordinates" {
  let c = Canvas::new()
  c.set(-1, -1) // cx=-1, cy=-1, px=1, py=3 -> bit 7
  let s = c.render(options={ ..default_render_options(), enable_ansi: false })
  assert_eq(s, "\u2880\n")
}

///|
test "text_overlay" {
  let c = Canvas::new()
  c.set(0, 0)
  c.set_text(0, 0, "HI")
  let s = c.render(options={ ..default_render_options(), enable_ansi: false })
  // Text should replace braille in the same cell
  assert_eq(s, "HI\n") // Min X is 0, Max X is 1 (where I is)
}

///|
test "ansi_colors" {
  let c = Canvas::new()
  let red_style = { ..default_style(), fg: Ansi(Red, false) }
  c.set(0, 0)
  c.set_style(0, 0, red_style)
  let s = c.render(options={ ..default_render_options(), enable_ansi: true })
  assert_true(s.contains("\u001b[0;31m"))
  assert_true(s.contains("\u2801"))
  assert_true(s.contains("\u001b[0m"))
}

///|
test "basic_palette_coverage" {
  let c = Canvas::new()
  let colors = [Black, Red, Green, Yellow, Blue, Magenta, Cyan, White]
  for i = 0; i < colors.length(); i = i + 1 {
    let style = with_fg(default_style(), Ansi(colors[i], false))
    c.set(i * 2, 0)
    c.set_style(i * 2, 0, style)
  }
  let s = c.render(options={ ..default_render_options(), enable_ansi: true })
  assert_true(s.contains("\u001b[0;30m"))
  assert_true(s.contains("\u001b[0;37m"))
}

///|
test "line_drawing" {
  let c = Canvas::new()
  c.stroke_line(0, 0, 2, 0) // (0,0), (1,0), (2,0)
  let s = c.render(options={ ..default_render_options(), enable_ansi: false })
  // (0,0) and (1,0) in cell (0,0) -> bit 0, 3 -> 1|8 = 9
  // (2,0) in cell (1,0) -> px=0, py=0 -> bit 0
  assert_eq(s, "\u2809\u2801\n")
}

///|
/// Default style with terminal default foreground/background and all flags off.
pub fn default_style() -> Style {
  { fg: Default, bg: Default }
}

///|
/// Default rendering options for ANSI-capable terminal output.
pub fn default_render_options() -> RenderOptions {
  { line_ending: "\n", enable_ansi: true, reset_each_line: true }
}

///|
/// Return a copy of a style with a different foreground color.
pub fn with_fg(style : Style, fg : Color) -> Style {
  { fg, bg: style.bg }
}

///|
/// Return rendering options with ANSI output disabled.
pub fn without_ansi(options : RenderOptions) -> RenderOptions {
  {
    line_ending: options.line_ending,
    enable_ansi: false,
    reset_each_line: options.reset_each_line,
  }
}

///|
/// Clamp an RGB channel to the inclusive range `[0, 255]`.
pub fn clamp_rgb(channel : Int) -> Int {
  if channel < 0 {
    0
  } else if channel > 255 {
    255
  } else {
    channel
  }
}

///|
/// Construct a truecolor value after clamping each channel.
pub fn rgb(r : Int, g : Int, b : Int) -> Color {
  Rgb(clamp_rgb(r), clamp_rgb(g), clamp_rgb(b))
}

///|
/// Create an empty logical canvas.
pub fn Canvas::new() -> Canvas {
  { cells: Map::new() }
}

///|
/// Clear all canvas content.
pub fn Canvas::clear(self : Canvas) -> Unit {
  self.cells.clear()
}

///|
fn Canvas::get_or_create_cell(self : Canvas, cx : Int, cy : Int) -> Cell {
  match self.cells.get((cx, cy)) {
    Some(cell) => cell
    None => {
      let cell = { braille: 0, text: "", style: default_style() }
      self.cells.set((cx, cy), cell)
      cell
    }
  }
}

// Braille dot mapping within a 2x4 cell:
// (0,0) (1,0)  -> bit 0, 3
// (0,1) (1,1)  -> bit 1, 4
// (0,2) (1,2)  -> bit 2, 5
// (0,3) (1,3)  -> bit 6, 7

///|
fn pixel_to_bit(px : Int, py : Int) -> Int {
  match (px, py) {
    (0, 0) => 0
    (0, 1) => 1
    (0, 2) => 2
    (1, 0) => 3
    (1, 1) => 4
    (1, 2) => 5
    (0, 3) => 6
    (1, 3) => 7
    _ => 0
  }
}

///|
/// Activate one logical pixel.
pub fn Canvas::set(self : Canvas, x : Int, y : Int) -> Unit {
  let cx = x / 2
  let cy = y / 4
  let px = x % 2
  let py = y % 4
  // Handle negatives for modulo
  let (px, cx) = if px < 0 { (px + 2, cx - 1) } else { (px, cx) }
  let (py, cy) = if py < 0 { (py + 4, cy - 1) } else { (py, cy) }

  let cell = self.get_or_create_cell(cx, cy)
  cell.braille = cell.braille | (1 << pixel_to_bit(px, py))
}

///|
/// Deactivate one logical pixel.
pub fn Canvas::unset(self : Canvas, x : Int, y : Int) -> Unit {
  let cx = x / 2
  let cy = y / 4
  let px = x % 2
  let py = y % 4
  let (px, cx) = if px < 0 { (px + 2, cx - 1) } else { (px, cx) }
  let (py, cy) = if py < 0 { (py + 4, cy - 1) } else { (py, cy) }

  match self.cells.get((cx, cy)) {
    Some(cell) =>
      cell.braille = cell.braille & (1 << pixel_to_bit(px, py)).lnot()
    None => ()
  }
}

///|
/// Write text at a logical position with optional style.
pub fn Canvas::set_text(
  self : Canvas,
  x : Int,
  y : Int,
  text : String,
  style? : Style = default_style(),
) -> Unit {
  let cx = x / 2
  let cy = y / 4
  // We place text cell-by-cell. Multi-character text advances horizontally.
  for i, char in text {
    let cell = self.get_or_create_cell(cx + i, cy)
    cell.text = char.to_string()
    cell.style = style
  }
}

///|
/// Apply a style to content located at a logical position.
pub fn Canvas::set_style(
  self : Canvas,
  x : Int,
  y : Int,
  style : Style,
) -> Unit {
  let cx = x / 2
  let cy = y / 4
  let cell = self.get_or_create_cell(cx, cy)
  cell.style = style
}

///|
/// Draw a styled line segment on the canvas.
/// Use Bresenham's algorithm for drawing lines.
pub fn Canvas::stroke_line(
  self : Canvas,
  x1 : Int,
  y1 : Int,
  x2 : Int,
  y2 : Int,
  style? : Style = default_style(),
) -> Unit {
  let dx = (x2 - x1).abs()
  let dy = -(y2 - y1).abs()
  let sx = if x1 < x2 { 1 } else { -1 }
  let sy = if y1 < y2 { 1 } else { -1 }
  let mut err = dx + dy
  let mut cur_x = x1
  let mut cur_y = y1

  while true {
    self.set(cur_x, cur_y)
    self.set_style(cur_x, cur_y, style)
    if cur_x == x2 && cur_y == y2 {
      break
    }
    let e2 = 2 * err
    if e2 >= dy {
      err += dy
      cur_x += sx
    }
    if e2 <= dx {
      err += dx
      cur_y += sy
    }
  }
}

///|
/// Render the current canvas into a terminal string.
pub fn Canvas::render(
  self : Canvas,
  options? : RenderOptions = default_render_options(),
) -> String {
  let mut min_x = 0
  let mut max_x = 0
  let mut min_y = 0
  let mut max_y = 0
  let mut first = true

  self.cells.each(fn(k, _) {
    let (cx, cy) = k
    if first {
      min_x = cx
      max_x = cx
      min_y = cy
      max_y = cy
      first = false
    } else {
      if cx < min_x {
        min_x = cx
      }
      if cx > max_x {
        max_x = cx
      }
      if cy < min_y {
        min_y = cy
      }
      if cy > max_y {
        max_y = cy
      }
    }
  })

  let sb = StringBuilder::new()
  let mut current_style = default_style()

  for cy = min_y; cy <= max_y; cy = cy + 1 {
    for cx = min_x; cx <= max_x; cx = cx + 1 {
      match self.cells.get((cx, cy)) {
        Some(cell) => {
          if options.enable_ansi {
            let ansi = get_ansi_sequence(cell.style, current_style)
            sb.write_string(ansi)
            current_style = cell.style
          }
          if cell.text != "" {
            sb.write_string(cell.text)
          } else if cell.braille != 0 {
            let char_code = 0x2800 + cell.braille
            sb.write_char(char_code.unsafe_to_char())
          } else {
            sb.write_string(" ")
          }
        }
        None => {
          if options.enable_ansi && current_style != default_style() {
            sb.write_string("\u001b[0m")
            current_style = default_style()
          }
          sb.write_string(" ")
        }
      }
    }
    if options.enable_ansi && options.reset_each_line {
      sb.write_string("\u001b[0m")
      current_style = default_style()
    }
    sb.write_string(options.line_ending)
  }

  if options.enable_ansi && current_style != default_style() {
    sb.write_string("\u001b[0m")
  }

  sb.to_string()
}

///|
fn get_ansi_sequence(style : Style, prev : Style) -> String {
  if style == prev {
    return ""
  }
  if style == default_style() {
    return "\u001b[0m"
  }

  let sb = StringBuilder::new()
  sb.write_string("\u001b[0") // Reset or start fresh

  match style.fg {
    Default => ()
    Ansi(color, bright) => {
      let code = match color {
        Black => 30
        Red => 31
        Green => 32
        Yellow => 33
        Blue => 34
        Magenta => 35
        Cyan => 36
        White => 37
      }
      let final_code = if bright { code + 60 } else { code }
      sb.write_string(";\{final_code}")
    }
    Rgb(r, g, b) => sb.write_string(";38;2;\{r};\{g};\{b}")
  }

  match style.bg {
    Default => ()
    Ansi(color, bright) => {
      let code = match color {
        Black => 40
        Red => 41
        Green => 42
        Yellow => 43
        Blue => 44
        Magenta => 45
        Cyan => 46
        White => 47
      }
      let final_code = if bright { code + 60 } else { code }
      sb.write_string(";\{final_code}")
    }
    Rgb(r, g, b) => sb.write_string(";48;2;\{r};\{g};\{b}")
  }

  sb.write_string("m")
  sb.to_string()
}