///|
/// Palette strategy used by still-image and animation export paths.
pub(all) enum PalettePlanMode {
  LocalMedianCut
  SharedMedianCut
  OrderedShared
  FloydShared
} derive(Eq, Debug)

///|
pub(all) struct PalettePlan {
  mode : PalettePlanMode
  max_colors : Int
  transparent : Rgba?
} derive(Debug)

///|
pub fn PalettePlan::new(
  mode : PalettePlanMode,
  max_colors : Int,
) -> PalettePlan {
  {
    mode,
    max_colors: if max_colors < 2 {
      2
    } else if max_colors > 256 {
      256
    } else {
      max_colors
    },
    transparent: None,
  }
}

///|
pub fn PalettePlan::with_transparent(
  self : PalettePlan,
  color : Rgba?,
) -> PalettePlan {
  { ..self, transparent: color }
}

///|
pub fn PalettePlan::colors(self : PalettePlan) -> Int {
  self.max_colors
}

///|
pub fn PalettePlan::mode(self : PalettePlan) -> PalettePlanMode {
  self.mode
}

///|
pub(all) struct PaletteQuality {
  palette_size : Int
  pixels : Int
  mean_error : Int
  maximum_error : Int
  exact_pixels : Int
} derive(Debug)

///|
pub fn PaletteQuality::palette_size(self : PaletteQuality) -> Int {
  self.palette_size
}

///|
pub fn PaletteQuality::pixels(self : PaletteQuality) -> Int {
  self.pixels
}

///|
pub fn PaletteQuality::mean_error(self : PaletteQuality) -> Int {
  self.mean_error
}

///|
pub fn PaletteQuality::maximum_error(self : PaletteQuality) -> Int {
  self.maximum_error
}

///|
pub fn PaletteQuality::exact_pixels(self : PaletteQuality) -> Int {
  self.exact_pixels
}

///|
fn color_distance_squared(left : Rgba, right : Rgba) -> Int {
  let dr = left.red().to_int() - right.red().to_int()
  let dg = left.green().to_int() - right.green().to_int()
  let db = left.blue().to_int() - right.blue().to_int()
  let da = left.alpha().to_int() - right.alpha().to_int()
  dr * dr + dg * dg + db * db + da * da
}

///|
/// Return the closest palette entry using squared Euclidean RGBA distance.
pub fn Palette::nearest_rgba(self : Palette, color : Rgba) -> Int {
  let mut best = 0
  let mut score = 1 << 30
  for i, candidate in self.colors {
    let distance = color_distance_squared(color, candidate)
    if distance < score {
      best = i
      score = distance
    }
  }
  best
}

///|
/// Remap pixels to a supplied palette without changing canvas dimensions.
pub fn remap_to_palette(canvas : Canvas, palette : Palette) -> QuantizedFrame {
  let indices : Array[Byte] = []
  for color in canvas.pixels {
    indices.push(palette.nearest_rgba(color).to_byte())
  }
  QuantizedFrame::{ palette, indices }
}

///|
/// Reconstruct a canvas from a quantized frame.
pub fn QuantizedFrame::to_canvas(
  self : QuantizedFrame,
  width~ : Int,
  height~ : Int,
) -> Canvas {
  let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
  let palette = self.palette()
  for i in 0.. PaletteQuality {
  let palette = quantized.palette()
  let mut total = 0
  let mut maximum = 0
  let mut exact = 0
  for i in 0.. maximum {
      maximum = root
    }
    if root == 0 {
      exact += 1
    }
  }
  PaletteQuality::{
    palette_size: palette.len(),
    pixels: canvas.pixels.length(),
    mean_error: if canvas.pixels.length() == 0 {
      0
    } else {
      total / canvas.pixels.length()
    },
    maximum_error: maximum,
    exact_pixels: exact,
  }
}

///|
fn plan_one(
  canvas : Canvas,
  plan : PalettePlan,
  shared : Palette?,
) -> QuantizedFrame {
  match plan.mode {
    LocalMedianCut =>
      match plan.transparent {
        None => quantize(canvas, max_colors=plan.max_colors)
        Some(color) =>
          quantize(canvas, max_colors=plan.max_colors, transparent=color)
      }
    SharedMedianCut =>
      match shared {
        None => quantize(canvas, max_colors=plan.max_colors)
        Some(palette) => remap_to_palette(canvas, palette)
      }
    OrderedShared =>
      match shared {
        None =>
          quantize_dithered(
            canvas,
            plan.max_colors,
            plan.transparent,
            Ordered2x2,
          )
        Some(palette) => remap_to_palette(canvas, palette)
      }
    FloydShared =>
      match shared {
        None =>
          quantize_dithered(
            canvas,
            plan.max_colors,
            plan.transparent,
            FloydSteinberg,
          )
        Some(palette) => remap_to_palette(canvas, palette)
      }
  }
}

///|
fn shared_palette(animation : Animation, plan : PalettePlan) -> Palette? {
  if animation.frames.length() == 0 {
    None
  } else {
    let samples : Array[Rgba] = []
    for frame in animation.frames {
      let canvas = frame.canvas()
      let stride = if canvas.len() > 4096 { canvas.len() / 4096 } else { 1 }
      let mut i = 0
      while i < canvas.len() {
        samples.push(canvas.pixels[i])
        i += stride
      }
    }
    let width = if samples.length() > 0 { samples.length() } else { 1 }
    let sample_canvas = Canvas::{ width, height: 1, pixels: samples }
    Some(
      match plan.transparent {
        None => quantize(sample_canvas, max_colors=plan.max_colors).palette()
        Some(color) =>
          quantize(sample_canvas, max_colors=plan.max_colors, transparent=color).palette()
      },
    )
  }
}

///|
/// Quantize every frame while making the palette-sharing decision explicit.
pub fn quantize_animation(
  animation : Animation,
  plan : PalettePlan,
) -> Array[QuantizedFrame] {
  let shared = match plan.mode {
    LocalMedianCut => None
    _ => shared_palette(animation, plan)
  }
  let result : Array[QuantizedFrame] = []
  for frame in animation.frames {
    result.push(plan_one(frame.canvas(), plan, shared))
  }
  result
}

///|
/// Return per-frame palette distortion for export reports.
pub fn quantify_animation(
  animation : Animation,
  plan : PalettePlan,
) -> Array[PaletteQuality] {
  let quantized = quantize_animation(animation, plan)
  let result : Array[PaletteQuality] = []
  for i in 0.. Palette {
  let safe_size = if size < 2 { 2 } else if size > 256 { 256 } else { size }
  let colors : Array[Rgba] = []
  for i in 0.. Palette {
  let safe_size = if size < 2 { 2 } else if size > 256 { 256 } else { size }
  let colors : Array[Rgba] = []
  for i in 0.. Palette {
  match self.transparent_index() {
    Some(_) => self
    None => {
      let colors : Array[Rgba] = []
      for candidate in self.colors {
        colors.push(candidate)
      }
      colors.push(color.with_alpha(0))
      Palette::new(colors~, transparent_index=Some(colors.length() - 1))
    }
  }
}

///|
/// Report whether a quantized frame obeys GIF's 8-bit palette contract.
pub fn QuantizedFrame::is_gif_compatible(self : QuantizedFrame) -> Bool {
  self.palette().len() > 0 &&
  self.palette().len() <= 256 &&
  self.indices.length() >= 0
}

///|
/// Clamp an arbitrary palette budget to GIF's legal range.
pub fn clamp_palette_size(size : Int) -> Int {
  if size < 2 {
    2
  } else if size > 256 {
    256
  } else {
    size
  }
}