// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// A theme from one seed colour: a scale, a second series mixed from the first,
/// text colours picked by contrast, and the same palette for a terminal.
///
/// This is the shape a theme generator has — one colour in, a hundred out — and
/// each step is one of the operations this library exists for.
fn main raise {
  let seed = @moonrgba.Color::decode("#0891b2")
  let accent = @moonrgba.Color::decode("#f59e0b")
  let background = seed.mix(
    @moonrgba.Color::white(),
    space=@moonrgba.Space::Oklch,
    weight=0.94,
  )
  let surface = seed.mix(
    @moonrgba.Color::white(),
    space=@moonrgba.Space::Oklch,
    weight=0.88,
  )
  let ink = seed.mix(
    @moonrgba.Color::black(),
    space=@moonrgba.Space::Oklch,
    weight=0.2,
  )

  println("/* the seed colours */")
  println("--brand:      " + seed.encode())
  println("--accent:     " + accent.encode())
  println("")

  // A scale: the seed's hue and chroma at ten lightnesses, each one brought into
  // sRGB. The two ends leave the gamut first, which is why `to_gamut` is here:
  // written as hexadecimal without it, the dark end would clip and lose its hue.
  println("/* --brand-0 is the lightest, --brand-9 the darkest */")
  for step in 0..<10 {
    let lightness = 0.95 - 0.09 * step.to_double()
    let base = seed.to_oklch()
    let color = base.with_components(lightness, base.y, base.z)
    let color = color.to_gamut(space=@moonrgba.Space::Srgb)
    println(
      "--brand-" +
      step.to_string() +
      ": " +
      color.encode(format=@moonrgba.Format::Hex) +
      "  " +
      describe(color),
    )
  }
  println("")

  // A second series halfway between the two brand colours, which is what a chart
  // needs when it has four series and two brand colours.
  // A weight is how much of the first colour the mix has, so these two are the
  // same pair of colours pulled towards each other. Halfway between two hues
  // this far apart is grey — mixing in Oklab keeps a mix *useful* by keeping it
  // even, not by inventing a hue that neither colour had.
  println("/* the brand pulled towards the accent, and back the other way */")
  let toward_accent = seed.mix(
    accent,
    space=@moonrgba.Space::Oklab,
    weight=0.75,
  )
  let toward_brand = seed.mix(accent, space=@moonrgba.Space::Oklab, weight=0.25)
  println(
    "--blend:      " +
    hex_of(toward_accent) +
    "  /* " +
    toward_accent.encode() +
    " */",
  )
  println(
    "--blend-soft: " +
    hex_of(toward_brand) +
    "  /* " +
    toward_brand.encode() +
    " */",
  )
  println("")

  // Text on each background: whichever of black and white reads better, then
  // nudged towards the background so that it is not pure contrast.
  println("/* text on a background, by WCAG 2 contrast */")
  for
    pair in [("background", background), ("surface", surface), ("brand", seed)] {
    let (label, color) = pair
    let on_white = @moonrgba.Color::white().contrast(color)
    let on_black = @moonrgba.Color::black().contrast(color)
    let (chosen, ratio) = if on_black >= on_white {
      (ink, on_black)
    } else {
      (
        color.mix(
          @moonrgba.Color::white(),
          space=@moonrgba.Space::Oklch,
          weight=0.06,
        ),
        on_white,
      )
    }
    println(
      "--on-" +
      pad(label, 11) +
      hex_of(chosen) +
      "  /* " +
      round_ratio(ratio) +
      " */",
    )
  }
  println("")

  // The same palette for a terminal that has sixteen colours and no more.
  println("/* the terminal's palette, nearest slot by OK colour difference */")
  println(
    "--ansi-brand:  " +
    seed.to_ansi16().to_string() +
    " (" +
    seed.to_ansi256().to_string() +
    " in 256)",
  )
  println(
    "--ansi-accent: " +
    accent.to_ansi16().to_string() +
    " (" +
    accent.to_ansi256().to_string() +
    " in 256)",
  )
  let (r, g, b) = seed.rgb8()
  println(
    "--ansi-true:   " +
    r.to_string() +
    ";" +
    g.to_string() +
    ";" +
    b.to_string(),
  )
}

///|
/// The colour as a theme file would carry it: sRGB, in a byte per channel, with
/// the gamut mapping in front so that a colour from a wider space arrives here
/// as something a screen can show rather than as a clipped version of itself.
fn hex_of(color : @moonrgba.Color) -> String {
  color
  .to_gamut(space=@moonrgba.Space::Srgb)
  .encode(format=@moonrgba.Format::Hex)
}

///|
/// What a person would call the colour, for a comment beside the value.
fn describe(color : @moonrgba.Color) -> String {
  let oklch = color.to_oklch()
  let chroma = if oklch.y < 0.02 {
    "grey"
  } else if oklch.z < 45.0 || oklch.z >= 345.0 {
    "red"
  } else if oklch.z < 90.0 {
    "orange"
  } else if oklch.z < 150.0 {
    "yellow"
  } else if oklch.z < 200.0 {
    "green"
  } else if oklch.z < 260.0 {
    "cyan"
  } else if oklch.z < 300.0 {
    "blue"
  } else {
    "purple"
  }
  "/* " + chroma + ", L " + round_two(oklch.x * 100.0) + " */"
}

///|
fn round_two(value : Double) -> String {
  let hundredths = 0.01
  let scaled = value / hundredths
  let rounded = if scaled >= 0.0 { scaled + 0.5 } else { scaled - 0.5 }
  (rounded.to_int().to_double() * hundredths).to_string()
}

///|
fn round_ratio(value : Double) -> String {
  round_two_hundredths(value) + ":1"
}

///|
fn round_two_hundredths(value : Double) -> String {
  let scaled = value * 100.0
  let rounded = if scaled >= 0.0 { scaled + 0.5 } else { scaled - 0.5 }
  let whole = rounded.to_int()
  (whole / 100).to_string() + "." + pad((whole % 100).to_string(), 2)
}

///|
fn pad(text : String, width : Int32) -> String {
  if text.length() >= width {
    return text
  }
  let out = StringBuilder()
  out.write_string(text)
  for _ in 0..<(width - text.length()) {
    out.write_char(' ')
  }
  out.to_string()
}