///|
/// Structural analysis for choosing palettes, delta encoding, and previews.
pub(all) struct CanvasComplexity {
  pixels : Int
  opaque_pixels : Int
  unique_colors : Int
  edge_pixels : Int
  entropy_milli : Int
  luma_range : Int
} derive(Debug)

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

///|
pub fn CanvasComplexity::opaque_pixels(self : CanvasComplexity) -> Int {
  self.opaque_pixels
}

///|
pub fn CanvasComplexity::unique_colors(self : CanvasComplexity) -> Int {
  self.unique_colors
}

///|
pub fn CanvasComplexity::edge_pixels(self : CanvasComplexity) -> Int {
  self.edge_pixels
}

///|
pub fn CanvasComplexity::entropy_milli(self : CanvasComplexity) -> Int {
  self.entropy_milli
}

///|
pub fn CanvasComplexity::luma_range(self : CanvasComplexity) -> Int {
  self.luma_range
}

///|
fn entropy_component(count : Int, total : Int) -> Int {
  if count <= 0 || total <= 0 {
    0
  } else {
    let ratio = count * 1000 / total
    let complement = 1000 - ratio
    ratio * complement / 1000
  }
}

///|
/// Estimate visual complexity using exact colors, edges, and luma spread.
pub fn Canvas::complexity(self : Canvas) -> CanvasComplexity {
  let colors : Map[Int, Bool] = Map([])
  let histogram = Array::make(256, 0)
  let mut visible_count = 0
  let mut edges = 0
  let mut minimum = 255
  let mut maximum = 0
  for y in 0.. 0 {
        visible_count += 1
      }
      if luma < minimum {
        minimum = luma
      }
      if luma > maximum {
        maximum = luma
      }
      if x > 0 {
        let left = self.pixels[x - 1 + y * self.width]
        if color.distance(left) > 24 {
          edges += 1
        }
      }
      if y > 0 {
        let above = self.pixels[x + (y - 1) * self.width]
        if color.distance(above) > 24 {
          edges += 1
        }
      }
    }
  }
  let total = self.pixels.length()
  let mut entropy = 0
  for count in histogram {
    entropy += entropy_component(count, total)
  }
  CanvasComplexity::{
    pixels: total,
    opaque_pixels: visible_count,
    unique_colors: colors.length(),
    edge_pixels: edges,
    entropy_milli: entropy,
    luma_range: if total == 0 {
      0
    } else {
      maximum - minimum
    },
  }
}

///|
pub(all) struct AnimationComplexity {
  frame_count : Int
  total_pixels : Int
  changed_pixels : Int
  mean_unique_colors : Int
  active_frames : Int
  peak_entropy : Int
} derive(Debug)

///|
pub fn AnimationComplexity::frame_count(self : AnimationComplexity) -> Int {
  self.frame_count
}

///|
pub fn AnimationComplexity::total_pixels(self : AnimationComplexity) -> Int {
  self.total_pixels
}

///|
pub fn AnimationComplexity::changed_pixels(self : AnimationComplexity) -> Int {
  self.changed_pixels
}

///|
pub fn AnimationComplexity::mean_unique_colors(
  self : AnimationComplexity,
) -> Int {
  self.mean_unique_colors
}

///|
pub fn AnimationComplexity::active_frames(self : AnimationComplexity) -> Int {
  self.active_frames
}

///|
pub fn AnimationComplexity::peak_entropy(self : AnimationComplexity) -> Int {
  self.peak_entropy
}

///|
/// Analyze frame-to-frame activity and palette diversity.
pub fn Animation::complexity(self : Animation) -> AnimationComplexity {
  let mut total_pixels = 0
  let mut unique_total = 0
  let mut active = 0
  let mut peak_entropy = 0
  let mut changed = 0
  let mut previous = Canvas::new(
    width=self.width,
    height=self.height,
    fill=self.background,
  )
  for frame in self.frames {
    let current = frame.canvas()
    let summary = current.complexity()
    total_pixels += summary.pixels()
    unique_total += summary.unique_colors()
    if summary.entropy_milli() > peak_entropy {
      peak_entropy = summary.entropy_milli()
    }
    let difference = pixel_difference(previous, current)
    if difference > 0 {
      active += 1
    }
    changed += difference
    previous = current
  }
  AnimationComplexity::{
    frame_count: self.frames.length(),
    total_pixels,
    changed_pixels: changed,
    mean_unique_colors: if self.frames.length() == 0 {
      0
    } else {
      unique_total / self.frames.length()
    },
    active_frames: active,
    peak_entropy,
  }
}

///|
/// Return an indexed color frequency table sorted by descending frequency.
pub fn Canvas::color_frequencies(
  self : Canvas,
  limit : Int,
) -> Array[(Rgba, Int)] {
  let histogram : Map[Int, (Rgba, Int)] = Map([])
  for color in self.pixels {
    let key = color.packed_rgb()
    match histogram.get(key) {
      None => histogram[key] = (color, 1)
      Some((stored, count)) => histogram[key] = (stored, count + 1)
    }
  }
  let result : Array[(Rgba, Int)] = []
  for _, value in histogram {
    result.push(value)
  }
  let safe_limit = if limit < 0 { 0 } else { limit }
  while result.length() > safe_limit {
    let mut smallest = 0
    for i in 1.. Int {
  let threshold = if minimum_count < 1 { 1 } else { minimum_count }
  let mut count = 0
  for pair in self.color_frequencies(1000000) {
    if pair.1 >= threshold {
      count += 1
    }
  }
  count
}

///|
/// Return a normalized activity score in the range 0..1000.
pub fn Animation::activity_per_mille(self : Animation) -> Int {
  let complexity = self.complexity()
  let total = complexity.total_pixels()
  if total <= 0 {
    0
  } else {
    let value = complexity.changed_pixels() * 1000 / total
    if value > 1000 {
      1000
    } else {
      value
    }
  }
}