///|
/// A positioned image layer used to compose stickers, captions, and overlays.
pub(all) struct ImageLayer {
  name : String
  canvas : Canvas
  left : Int
  top : Int
  opacity : Byte
  blend_mode : BlendMode
  visible : Bool
} derive(Debug)

///|
pub fn ImageLayer::new(name : String, canvas : Canvas) -> ImageLayer {
  {
    name,
    canvas,
    left: 0,
    top: 0,
    opacity: 255,
    blend_mode: Normal,
    visible: true,
  }
}

///|
pub fn ImageLayer::name(self : ImageLayer) -> String {
  self.name
}

///|
pub fn ImageLayer::canvas(self : ImageLayer) -> Canvas {
  self.canvas
}

///|
pub fn ImageLayer::offset(self : ImageLayer) -> (Int, Int) {
  (self.left, self.top)
}

///|
pub fn ImageLayer::opacity(self : ImageLayer) -> Byte {
  self.opacity
}

///|
pub fn ImageLayer::blend_mode(self : ImageLayer) -> BlendMode {
  self.blend_mode
}

///|
pub fn ImageLayer::move_to(
  self : ImageLayer,
  left : Int,
  top : Int,
) -> ImageLayer {
  { ..self, left, top }
}

///|
pub fn ImageLayer::with_opacity(
  self : ImageLayer,
  opacity : Byte,
) -> ImageLayer {
  { ..self, opacity, }
}

///|
pub fn ImageLayer::with_blend(
  self : ImageLayer,
  mode : BlendMode,
) -> ImageLayer {
  { ..self, blend_mode: mode }
}

///|
pub fn ImageLayer::hidden(self : ImageLayer) -> ImageLayer {
  { ..self, visible: false }
}

///|
pub fn ImageLayer::shown(self : ImageLayer) -> ImageLayer {
  { ..self, visible: true }
}

///|
pub(all) struct LayerStack {
  width : Int
  height : Int
  background : Rgba
  layers : Array[ImageLayer]
} derive(Debug)

///|
pub fn LayerStack::new(width~ : Int, height~ : Int) -> LayerStack {
  { width, height, background: rgba(0, 0, 0, 0), layers: [] }
}

///|
pub fn LayerStack::background(self : LayerStack, color : Rgba) -> LayerStack {
  { ..self, background: color }
}

///|
pub fn LayerStack::add(self : LayerStack, layer : ImageLayer) -> LayerStack {
  self.layers.push(layer)
  self
}

///|
pub fn LayerStack::insert(
  self : LayerStack,
  index : Int,
  layer : ImageLayer,
) -> LayerStack {
  let safe_index = if index < 0 {
    0
  } else if index > self.layers.length() {
    self.layers.length()
  } else {
    index
  }
  let result : Array[ImageLayer] = []
  for i in 0..<(self.layers.length() + 1) {
    if i == safe_index {
      result.push(layer)
    } else if i < safe_index {
      result.push(self.layers[i])
    } else {
      result.push(self.layers[i - 1])
    }
  }
  { ..self, layers: result }
}

///|
pub fn LayerStack::remove(self : LayerStack, index : Int) -> LayerStack {
  if index < 0 || index >= self.layers.length() {
    self
  } else {
    let result : Array[ImageLayer] = []
    for i, layer in self.layers {
      if i != index {
        result.push(layer)
      }
    }
    { ..self, layers: result }
  }
}

///|
pub fn LayerStack::layers(self : LayerStack) -> ArrayView[ImageLayer] {
  self.layers[:]
}

///|
fn layer_canvas(stack : LayerStack, layer : ImageLayer) -> Canvas {
  let prepared = layer.canvas().scale_alpha(layer.opacity())
  let positioned = Canvas::new(
    width=stack.width,
    height=stack.height,
    fill=rgba(0, 0, 0, 0),
  )
  ignore(
    positioned.blit(prepared, destination_x=layer.left, destination_y=layer.top),
  )
  positioned
}

///|
/// Composite the visible layers from back to front.
pub fn LayerStack::render(self : LayerStack) -> Canvas {
  let mut result = Canvas::new(
    width=self.width,
    height=self.height,
    fill=self.background,
  )
  for layer in self.layers {
    if layer.visible {
      let positioned = layer_canvas(self, layer)
      result = result.blend(positioned, layer.blend_mode())
    }
  }
  result
}

///|
/// Render each layer stack into an animation frame.
pub fn render_layers(
  width : Int,
  height : Int,
  stacks : ArrayView[LayerStack],
  delay_cs? : Int = 10,
) -> Animation {
  let result = Animation::new(width~, height~)
  let safe_delay = if delay_cs < 0 {
    0
  } else if delay_cs > 65535 {
    65535
  } else {
    delay_cs
  }
  for stack in stacks {
    ignore(
      result.add_frame(Frame::new(canvas=stack.render(), delay_cs=safe_delay)),
    )
  }
  result
}

///|
/// Construct a stack from one base canvas and ordered overlays.
pub fn compose_layers(
  base : Canvas,
  overlays : ArrayView[ImageLayer],
) -> Canvas {
  let stack = LayerStack::new(width=base.width, height=base.height).add(
    ImageLayer::new("base", base),
  )
  let mut result = stack
  for overlay in overlays {
    result = result.add(overlay)
  }
  result.render()
}

///|
/// Apply a single layer at all frame times while keeping the base timeline.
pub fn Animation::overlay_layer(
  self : Animation,
  layer : ImageLayer,
) -> Animation {
  let result = Animation::new(width=self.width, height=self.height)
    .with_loop_count(loop_count=self.loop_count)
    .with_background(background=self.background)
  for frame in self.frames {
    let stack = LayerStack::new(width=self.width, height=self.height)
      .add(ImageLayer::new("frame", frame.canvas()))
      .add(layer)
    ignore(result.add_frame(frame.with_canvas(stack.render())))
  }
  result
}

///|
/// Render a moving overlay across a timeline using integer interpolation.
pub fn Animation::moving_layer(
  self : Animation,
  layer : ImageLayer,
  start_left : Int,
  start_top : Int,
  end_left : Int,
  end_top : Int,
) -> Animation {
  let result = Animation::new(width=self.width, height=self.height)
    .with_loop_count(loop_count=self.loop_count)
    .with_background(background=self.background)
  let count = self.frames.length()
  for index, frame in self.frames {
    let denominator = if count <= 1 { 1 } else { count - 1 }
    let left = start_left + (end_left - start_left) * index / denominator
    let top = start_top + (end_top - start_top) * index / denominator
    let positioned = layer.move_to(left, top)
    let stack = LayerStack::new(width=self.width, height=self.height)
      .add(ImageLayer::new("frame", frame.canvas()))
      .add(positioned)
    ignore(result.add_frame(frame.with_canvas(stack.render())))
  }
  result
}

///|
/// Return the bounding rectangle of all visible layer pixels.
pub fn LayerStack::visible_bounds(self : LayerStack) -> GifRect? {
  let mut found = false
  let mut left = self.width
  let mut top = self.height
  let mut right = -1
  let mut bottom = -1
  for layer in self.layers {
    if layer.visible && layer.opacity > 0 {
      match layer.canvas().opaque_bounds() {
        None => ()
        Some(bounds) => {
          let layer_left = layer.left + bounds.left()
          let layer_top = layer.top + bounds.top()
          let layer_right = layer_left + bounds.width() - 1
          let layer_bottom = layer_top + bounds.height() - 1
          if layer_left < left {
            left = layer_left
          }
          if layer_top < top {
            top = layer_top
          }
          if layer_right > right {
            right = layer_right
          }
          if layer_bottom > bottom {
            bottom = layer_bottom
          }
          found = true
        }
      }
    }
  }
  if found {
    Some(
      GifRect::new(left~, top~, width=right - left + 1, height=bottom - top + 1),
    )
  } else {
    None
  }
}

///|
/// Scale every layer around the stack center for a poster-style effect.
pub fn LayerStack::scale_layers(
  self : LayerStack,
  scale_per_mille : Int,
) -> LayerStack {
  let safe_scale = if scale_per_mille < 0 { 0 } else { scale_per_mille }
  let result : Array[ImageLayer] = []
  for layer in self.layers {
    let scaled = layer
      .canvas()
      .resize_nearest(
        width=layer.canvas().width * safe_scale / 1000,
        height=layer.canvas().height * safe_scale / 1000,
      )
    let left = self.width / 2 - scaled.width / 2
    let top = self.height / 2 - scaled.height / 2
    result.push({ ..layer, canvas: scaled, left, top })
  }
  { ..self, layers: result }
}