///|
/// Terminal color capability used when rendering style color escapes.
pub(all) enum ColorProfile {
  TrueColor
  Ansi256
  Ansi16
  NoColor
} derive(Debug, Eq)

///|
/// Detected terminal background. Unknown resolves like Dark for adaptive colors.
pub(all) enum Background {
  Dark
  Light
  Unknown
} derive(Debug, Eq)

///|
/// User color policy layered on top of detected terminal capability.
pub(all) enum ColorPolicy {
  Auto
  Always
  Never
} derive(Debug, Eq)

///|
/// Immutable render-time color facts consumed by style renderers.
pub(all) struct RenderContext {
  color_profile : ColorProfile
  background : Background
  color_policy : ColorPolicy
} derive(Debug, Eq)

///|
/// Resolved color value before profile degradation.
pub(all) enum ColorValue {
  Palette(Int)
  Rgb(Int, Int, Int)
} derive(Debug, Eq)

///|
pub impl Show for ColorProfile with fn output(self, logger) {
  match self {
    TrueColor => logger.write_string("TrueColor")
    Ansi256 => logger.write_string("Ansi256")
    Ansi16 => logger.write_string("Ansi16")
    NoColor => logger.write_string("NoColor")
  }
}

///|
pub impl Show for Background with fn output(self, logger) {
  match self {
    Dark => logger.write_string("Dark")
    Light => logger.write_string("Light")
    Unknown => logger.write_string("Unknown")
  }
}

///|
pub impl Show for ColorPolicy with fn output(self, logger) {
  match self {
    Auto => logger.write_string("Auto")
    Always => logger.write_string("Always")
    Never => logger.write_string("Never")
  }
}

///|
pub impl Show for RenderContext with fn output(self, logger) {
  logger.write_string(
    "RenderContext(color_profile=\{self.color_profile}, background=\{self.background}, color_policy=\{self.color_policy})",
  )
}

///|
const CSI : String = "\u001b["

///|
let ambient_render_context : Ref[RenderContext] = Ref(RenderContext::default())

///|
pub fn RenderContext::default() -> RenderContext {
  { color_profile: TrueColor, background: Unknown, color_policy: Auto }
}

///|
pub fn RenderContext::with_color_profile(
  self : RenderContext,
  color_profile : ColorProfile,
) -> RenderContext {
  { ..self, color_profile, }
}

///|
pub fn RenderContext::with_background(
  self : RenderContext,
  background : Background,
) -> RenderContext {
  { ..self, background, }
}

///|
pub fn RenderContext::with_color_policy(
  self : RenderContext,
  color_policy : ColorPolicy,
) -> RenderContext {
  { ..self, color_policy, }
}

///|
pub fn RenderContext::detect() -> RenderContext {
  let no_color = @env.get_env_var("NO_COLOR")
  let clicolor_force = @env.get_env_var("CLICOLOR_FORCE")
  let clicolor = @env.get_env_var("CLICOLOR")
  let colorterm = @env.get_env_var("COLORTERM")
  let term = @env.get_env_var("TERM")
  let policy = match no_color {
    Some(value) if value != "" => Never
    _ =>
      match clicolor_force {
        Some(value) if value != "0" => Always
        _ =>
          match clicolor {
            Some("0") => Never
            _ => Auto
          }
      }
  }
  let profile = match colorterm {
    Some(value) if is_truecolor_colorterm(value) => TrueColor
    _ => detect_profile_from_term(term)
  }
  { color_profile: profile, background: Unknown, color_policy: policy }
}

///|
pub fn ambient_context() -> RenderContext {
  ambient_render_context.val
}

///|
pub fn set_render_context(ctx : RenderContext) -> Unit {
  ambient_render_context.val = ctx
}

///|
/// Run `f` with a scoped ambient render context, restoring on return or raise.
pub fn[T] with_render_context(
  ctx : RenderContext,
  f : () -> T raise?,
) -> T raise? {
  let previous = ambient_render_context.val
  ambient_render_context.val = ctx
  try f() catch {
    err => {
      ambient_render_context.val = previous
      raise err
    }
  } noraise {
    result => {
      ambient_render_context.val = previous
      result
    }
  }
}

///|
fn is_truecolor_colorterm(value : String) -> Bool {
  let lower = value.to_lower()
  lower == "truecolor" || lower == "24bit"
}

///|
fn detect_profile_from_term(term : String?) -> ColorProfile {
  match term {
    Some(value) if value == "" => NoColor
    Some(value) if value == "dumb" => NoColor
    Some(value) if value.to_lower().contains("256color") => Ansi256
    Some(_) => Ansi16
    None => NoColor
  }
}

///|
fn clamp_channel(n : Int) -> Int {
  if n < 0 {
    0
  } else if n > 255 {
    255
  } else {
    n
  }
}

///|
fn clamp_non_negative(n : Int) -> Int {
  n.max(0)
}

///|
fn clamp_palette(n : Int) -> Int {
  if n < 0 {
    0
  } else if n > 255 {
    255
  } else {
    n
  }
}

///|
fn color_level(level : Int) -> Int {
  match level {
    0 => 0
    1 => 95
    2 => 135
    3 => 175
    4 => 215
    _ => 255
  }
}

///|
fn standard16_rgb(n : Int) -> (Int, Int, Int) {
  match n {
    0 => (0, 0, 0)
    1 => (128, 0, 0)
    2 => (0, 128, 0)
    3 => (128, 128, 0)
    4 => (0, 0, 128)
    5 => (128, 0, 128)
    6 => (0, 128, 128)
    7 => (192, 192, 192)
    8 => (128, 128, 128)
    9 => (255, 0, 0)
    10 => (0, 255, 0)
    11 => (255, 255, 0)
    12 => (0, 0, 255)
    13 => (255, 0, 255)
    14 => (0, 255, 255)
    _ => (255, 255, 255)
  }
}

///|
fn ansi256_rgb(n : Int) -> (Int, Int, Int) {
  let index = clamp_palette(n)
  if index < 16 {
    standard16_rgb(index)
  } else if index < 232 {
    let i = index - 16
    (color_level(i / 36), color_level(i / 6 % 6), color_level(i % 6))
  } else {
    let value = 8 + (index - 232) * 10
    (value, value, value)
  }
}

///|
fn rgb_distance_sq(
  r1 : Int,
  g1 : Int,
  b1 : Int,
  r2 : Int,
  g2 : Int,
  b2 : Int,
) -> Int {
  let dr = r1 - r2
  let dg = g1 - g2
  let db = b1 - b2
  dr * dr + dg * dg + db * db
}

///|
pub fn rgb_to_ansi256(r : Int, g : Int, b : Int) -> Int {
  let cr = clamp_channel(r)
  let cg = clamp_channel(g)
  let cb = clamp_channel(b)
  let mut best = 16
  let mut best_distance = 1_000_000
  for index in 16..<256 {
    let (pr, pg, pb) = ansi256_rgb(index)
    let distance = rgb_distance_sq(cr, cg, cb, pr, pg, pb)
    if distance < best_distance {
      best = index
      best_distance = distance
    }
  }
  best
}

///|
pub fn ansi256_to_ansi16(n : Int) -> Int {
  let (r, g, b) = ansi256_rgb(n)
  let mut best = 0
  let mut best_distance = 1_000_000
  for index in 0..<16 {
    let (pr, pg, pb) = standard16_rgb(index)
    let distance = rgb_distance_sq(r, g, b, pr, pg, pb)
    if distance < best_distance {
      best = index
      best_distance = distance
    }
  }
  best
}

///|
fn effective_profile(ctx : RenderContext) -> ColorProfile {
  match ctx.color_policy {
    Never => NoColor
    Always =>
      match ctx.color_profile {
        NoColor => Ansi16
        _ => ctx.color_profile
      }
    Auto => ctx.color_profile
  }
}

///|
fn sgr16(is_fg : Bool, n : Int) -> String {
  let non_negative = if n < 0 { 0 } else { n }
  let index = non_negative.min(15)
  if is_fg {
    if index < 8 {
      "\{CSI}3\{index}m"
    } else {
      "\{CSI}9\{index - 8}m"
    }
  } else if index < 8 {
    "\{CSI}4\{index}m"
  } else {
    "\{CSI}10\{index - 8}m"
  }
}

///|
fn render_color(
  ctx : RenderContext,
  value : ColorValue,
  is_fg : Bool,
) -> String {
  match effective_profile(ctx) {
    NoColor => ""
    Ansi16 => {
      let n = match value {
        Palette(index) =>
          if index < 16 {
            if index < 0 {
              0
            } else {
              index
            }
          } else {
            ansi256_to_ansi16(index)
          }
        Rgb(r, g, b) => ansi256_to_ansi16(rgb_to_ansi256(r, g, b))
      }
      sgr16(is_fg, n)
    }
    Ansi256 => {
      let n = match value {
        Palette(index) => clamp_palette(index)
        Rgb(r, g, b) => rgb_to_ansi256(r, g, b)
      }
      if is_fg {
        "\{CSI}38;5;\{n}m"
      } else {
        "\{CSI}48;5;\{n}m"
      }
    }
    TrueColor =>
      match value {
        Palette(index) =>
          if is_fg {
            "\{CSI}38;5;\{clamp_non_negative(index)}m"
          } else {
            "\{CSI}48;5;\{clamp_non_negative(index)}m"
          }
        Rgb(r, g, b) => {
          let cr = clamp_channel(r)
          let cg = clamp_channel(g)
          let cb = clamp_channel(b)
          if is_fg {
            "\{CSI}38;2;\{cr};\{cg};\{cb}m"
          } else {
            "\{CSI}48;2;\{cr};\{cg};\{cb}m"
          }
        }
      }
  }
}

///|
pub fn render_fg(ctx : RenderContext, value : ColorValue) -> String {
  render_color(ctx, value, true)
}

///|
pub fn render_bg(ctx : RenderContext, value : ColorValue) -> String {
  render_color(ctx, value, false)
}