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

// Turning a picture by an angle that is not a right one.
//
// A quarter turn is a permutation and is done by `rotate`: every sample keeps
// its exact value. Any other angle has to ask where each output pixel came
// from, and the answer almost never lands on a sample, so the picture is
// resampled. The sampling is bilinear because the alternative — leaving the
// destination transparent wherever the source had no exact pixel — produces the
// staircase that a rotation is expected not to have.
//
// The inverse map is used, not the forward one. Walking the destination and
// pulling from the source visits every output pixel exactly once; walking the
// source and pushing forward leaves holes wherever the picture was stretched.

///|
/// The image turned clockwise by `degrees`.
///
/// `expand` grows the canvas to the rectangle the turned picture needs, which
/// is what a caller rotating a photograph wants; without it the picture is
/// turned inside the canvas it already had, and the corners are cut off, which
/// is what a caller animating a rotation wants.
///
/// A multiple of ninety degrees is exact and is handed to `rotate`; nothing is
/// resampled and no sample changes.
pub fn Image::rotate_by(
  self : Image,
  degrees : Double,
  expand? : Bool,
) -> Image raise Refused {
  let expand = expand.unwrap_or(false)
  let quarters = degrees / 90.0
  if absolute(quarters - quarters.floor()) < 1.0e-9 {
    let turned = self.rotate(quarters=quarters.floor().to_int())
    if !expand {
      return turned
    }
    // A quarter turn already swapped the canvas; expanding it further can only
    // add room, and a turned picture fits its own canvas exactly.
    return turned
  }
  let radians = degrees * pi / 180.0
  let cos = @math.cos(radians)
  let sin = @math.sin(radians)
  let source_width = self.width.to_double()
  let source_height = self.height.to_double()
  let (width, height) = if expand {
    let wide = absolute(source_width * cos) + absolute(source_height * sin)
    let tall = absolute(source_width * sin) + absolute(source_height * cos)
    (wide.ceil().to_int(), tall.ceil().to_int())
  } else {
    (self.width, self.height)
  }
  Limits::default().check(width, height)
  let source = self.working_copy()
  let pixels : FixedArray[Double] = FixedArray::make(width * height * 4, 0.0)
  let from_x = source_width / 2.0
  let from_y = source_height / 2.0
  let to_x = width.to_double() / 2.0
  let to_y = height.to_double() / 2.0
  for y in 0.. (Double, Double, Double, Double) {
  let left = x.floor().to_int()
  let top = y.floor().to_int()
  let across = x - left.to_double()
  let down = y - top.to_double()
  let (x0, x1) = (clamp_index(left, width), clamp_index(left + 1, width))
  let (y0, y1) = (clamp_index(top, height), clamp_index(top + 1, height))
  let top_left = (y0 * width + x0) * 4
  let top_right = (y0 * width + x1) * 4
  let bottom_left = (y1 * width + x0) * 4
  let bottom_right = (y1 * width + x1) * 4
  let weights = [
    (1.0 - across) * (1.0 - down),
    across * (1.0 - down),
    (1.0 - across) * down,
    across * down,
  ]
  let corners = [top_left, top_right, bottom_left, bottom_right]
  let mut out = (0.0, 0.0, 0.0, 0.0)
  for k in 0..<4 {
    let at = corners[k]
    let weight = weights[k]
    out = (
      out.0 + source[at] * weight,
      out.1 + source[at + 1] * weight,
      out.2 + source[at + 2] * weight,
      out.3 + source[at + 3] * weight,
    )
  }
  (clamp_unit(out.0), clamp_unit(out.1), clamp_unit(out.2), clamp_unit(out.3))
}

///|
fn clamp_index(index : Int32, limit : Int32) -> Int32 {
  if index < 0 {
    0
  } else if index >= limit {
    limit - 1
  } else {
    index
  }
}

///|
let pi : Double = 3.141592653589793