///|
/// Resampling filters selected explicitly for quality or speed.
pub(all) enum ResampleFilter {
  NearestPixel
  BoxAverage
  BilinearInteger
} derive(Eq, Debug)

///|
pub(all) struct ResampleReport {
  input_width : Int
  input_height : Int
  output_width : Int
  output_height : Int
  filter : ResampleFilter
  input_pixels : Int
  output_pixels : Int
} derive(Debug)

///|
pub fn ResampleReport::input_size(self : ResampleReport) -> (Int, Int) {
  (self.input_width, self.input_height)
}

///|
pub fn ResampleReport::output_size(self : ResampleReport) -> (Int, Int) {
  (self.output_width, self.output_height)
}

///|
pub fn ResampleReport::filter(self : ResampleReport) -> ResampleFilter {
  self.filter
}

///|
pub fn ResampleReport::input_pixels(self : ResampleReport) -> Int {
  self.input_pixels
}

///|
pub fn ResampleReport::output_pixels(self : ResampleReport) -> Int {
  self.output_pixels
}

///|
fn weighted_channel(
  left : Int,
  right : Int,
  numerator : Int,
  denominator : Int,
) -> Byte {
  if denominator <= 0 {
    left.to_byte()
  } else {
    ((left * (denominator - numerator) + right * numerator) / denominator).to_byte()
  }
}

///|
fn bilinear_sample(
  canvas : Canvas,
  x_num : Int,
  y_num : Int,
  width : Int,
  height : Int,
) -> Rgba {
  let source_x = x_num * canvas.width / width
  let source_y = y_num * canvas.height / height
  let next_x = if source_x + 1 >= canvas.width {
    source_x
  } else {
    source_x + 1
  }
  let next_y = if source_y + 1 >= canvas.height {
    source_y
  } else {
    source_y + 1
  }
  let x_fraction = x_num * canvas.width % width
  let y_fraction = y_num * canvas.height % height
  let top_left = canvas.pixels[source_x + source_y * canvas.width]
  let top_right = canvas.pixels[next_x + source_y * canvas.width]
  let bottom_left = canvas.pixels[source_x + next_y * canvas.width]
  let bottom_right = canvas.pixels[next_x + next_y * canvas.width]
  let top = rgba(
    weighted_channel(
      top_left.red().to_int(),
      top_right.red().to_int(),
      x_fraction,
      width,
    ),
    weighted_channel(
      top_left.green().to_int(),
      top_right.green().to_int(),
      x_fraction,
      width,
    ),
    weighted_channel(
      top_left.blue().to_int(),
      top_right.blue().to_int(),
      x_fraction,
      width,
    ),
    weighted_channel(
      top_left.alpha().to_int(),
      top_right.alpha().to_int(),
      x_fraction,
      width,
    ),
  )
  let bottom = rgba(
    weighted_channel(
      bottom_left.red().to_int(),
      bottom_right.red().to_int(),
      x_fraction,
      width,
    ),
    weighted_channel(
      bottom_left.green().to_int(),
      bottom_right.green().to_int(),
      x_fraction,
      width,
    ),
    weighted_channel(
      bottom_left.blue().to_int(),
      bottom_right.blue().to_int(),
      x_fraction,
      width,
    ),
    weighted_channel(
      bottom_left.alpha().to_int(),
      bottom_right.alpha().to_int(),
      x_fraction,
      width,
    ),
  )
  rgba(
    weighted_channel(
      top.red().to_int(),
      bottom.red().to_int(),
      y_fraction,
      height,
    ),
    weighted_channel(
      top.green().to_int(),
      bottom.green().to_int(),
      y_fraction,
      height,
    ),
    weighted_channel(
      top.blue().to_int(),
      bottom.blue().to_int(),
      y_fraction,
      height,
    ),
    weighted_channel(
      top.alpha().to_int(),
      bottom.alpha().to_int(),
      y_fraction,
      height,
    ),
  )
}

///|
fn box_sample(
  canvas : Canvas,
  left : Int,
  top : Int,
  right : Int,
  bottom : Int,
) -> Rgba {
  let mut red = 0
  let mut green = 0
  let mut blue = 0
  let mut alpha = 0
  let mut count = 0
  for y in top.. Canvas {
  if width < 0 || height < 0 {
    panic()
  }
  let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
  if width == 0 || height == 0 || self.width == 0 || self.height == 0 {
    result
  } else {
    for y in 0..
            self.pixels[x * self.width / width +
            y * self.height / height * self.width]
          BilinearInteger => bilinear_sample(self, x, y, width, height)
          BoxAverage => {
            let left = x * self.width / width
            let right = (x + 1) * self.width / width + 1
            let top = y * self.height / height
            let bottom = (y + 1) * self.height / height + 1
            box_sample(
              self,
              left,
              top,
              if right > self.width {
                self.width
              } else {
                right
              },
              if bottom > self.height {
                self.height
              } else {
                bottom
              },
            )
          }
        }
        result.pixels[x + y * width] = color
      }
    }
    result
  }
}

///|
/// Resize and return metrics useful for export logs.
pub fn Canvas::resample_with_report(
  self : Canvas,
  width~ : Int,
  height~ : Int,
  filter? : ResampleFilter = BilinearInteger,
) -> (Canvas, ResampleReport) {
  let result = self.resample(width~, height~, filter~)
  (
    result,
    ResampleReport::{
      input_width: self.width,
      input_height: self.height,
      output_width: result.width,
      output_height: result.height,
      filter,
      input_pixels: self.len(),
      output_pixels: result.len(),
    },
  )
}

///|
/// Downsample by an integer factor using box averaging.
pub fn Canvas::downsample(self : Canvas, factor : Int) -> Canvas {
  let safe = if factor < 1 { 1 } else { factor }
  self.resample(
    width=self.width / safe,
    height=self.height / safe,
    filter=BoxAverage,
  )
}

///|
/// Upsample by an integer factor using nearest pixels for crisp sprites.
pub fn Canvas::upscale(self : Canvas, factor : Int) -> Canvas {
  let safe = if factor < 1 { 1 } else { factor }
  self.resample(
    width=self.width * safe,
    height=self.height * safe,
    filter=NearestPixel,
  )
}

///|
/// Fit an image to an exact output size using the chosen filter.
pub fn Canvas::fit_resample(
  self : Canvas,
  width : Int,
  height : Int,
  filter : ResampleFilter,
) -> Canvas {
  self.resample(width~, height~, filter~)
}

///|
/// Resample every frame while keeping the animation timeline.
pub fn Animation::resample_frames_with_filter(
  self : Animation,
  width : Int,
  height : Int,
  filter? : ResampleFilter = BilinearInteger,
) -> Animation {
  let result = Animation::new(width~, height~)
    .with_loop_count(loop_count=self.loop_count)
    .with_background(background=self.background)
  for frame in self.frames {
    ignore(
      result.add_frame(
        frame
        .with_canvas(frame.canvas().resample(width~, height~, filter~))
        .with_offset(left=0, top=0),
      ),
    )
  }
  result
}

///|
/// Use pixel aspect compensation for low-resolution GIF previews.
pub fn Canvas::pixel_aspect(
  self : Canvas,
  horizontal_per_mille : Int,
  vertical_per_mille : Int,
) -> Canvas {
  let horizontal = if horizontal_per_mille < 1 {
    1
  } else {
    horizontal_per_mille
  }
  let vertical = if vertical_per_mille < 1 { 1 } else { vertical_per_mille }
  self.resample(
    width=self.width * horizontal / 1000,
    height=self.height * vertical / 1000,
    filter=BilinearInteger,
  )
}