///|
/// Explicit palette maps for reusable indexed-frame transformations.
pub(all) struct PaletteMap {
  source : Palette
  target : Palette
  mapping : Array[Byte]
} derive(Debug)

///|
pub fn PaletteMap::source(self : PaletteMap) -> Palette {
  self.source
}

///|
pub fn PaletteMap::target(self : PaletteMap) -> Palette {
  self.target
}

///|
pub fn PaletteMap::mapping(self : PaletteMap) -> ArrayView[Byte] {
  self.mapping[:]
}

///|
/// Build a nearest-color map between two palettes.
pub fn build_palette_map(source : Palette, target : Palette) -> PaletteMap {
  let mapping : Array[Byte] = []
  for color in source.colors {
    mapping.push(target.nearest_rgba(color).to_byte())
  }
  PaletteMap::{ source, target, mapping }
}

///|
/// Apply a palette map without touching pixel geometry.
pub fn QuantizedFrame::map_palette(
  self : QuantizedFrame,
  map : PaletteMap,
) -> QuantizedFrame {
  let indices : Array[Byte] = []
  let mapping = map.mapping()
  for index in self.indices() {
    let value = index.to_int()
    indices.push(if value < mapping.length() { mapping[value] } else { 0 })
  }
  QuantizedFrame::{ palette: map.target(), indices }
}

///|
/// Merge two palettes while removing exact duplicates.
pub fn merge_palettes(left : Palette, right : Palette, limit : Int) -> Palette {
  let colors : Array[Rgba] = []
  let safe_limit = clamp_palette_size(limit)
  for palette in [left, right] {
    for color in palette.colors {
      let mut found = false
      for existing in colors {
        if existing == color {
          found = true
        }
      }
      if !found && colors.length() < safe_limit {
        colors.push(color)
      }
    }
  }
  if colors.length() == 0 {
    colors.push(rgb(0, 0, 0))
  }
  Palette::new(colors~, transparent_index=None)
}

///|
/// Remap all frames through a shared target palette.
pub fn remap_animation_palette(
  animation : Animation,
  palette : Palette,
) -> Animation {
  let result = Animation::new(width=animation.width, height=animation.height)
    .with_loop_count(loop_count=animation.loop_count)
    .with_background(background=animation.background)
  for frame in animation.frames {
    let quantized = remap_to_palette(frame.canvas(), palette)
    ignore(
      result.add_frame(
        frame.with_canvas(
          quantized.to_canvas(
            width=frame.canvas().width,
            height=frame.canvas().height,
          ),
        ),
      ),
    )
  }
  result
}

///|
/// Count the number of source colors that map exactly to the target palette.
pub fn PaletteMap::exact_entries(self : PaletteMap) -> Int {
  let mut count = 0
  for i, color in self.source.colors {
    if self.target.color(self.mapping[i].to_int()) == Some(color) {
      count += 1
    }
  }
  count
}