///|
pub(all) struct Affine2D {
  sx : Double
  sy : Double
  tx : Double
  ty : Double
} derive(Debug, ToJson)

///|
pub fn Affine2D::identity() -> Affine2D {
  { sx: 1.0, sy: 1.0, tx: 0.0, ty: 0.0 }
}

///|
pub fn Affine2D::scale(sx~ : Double, sy~ : Double) -> Affine2D {
  { sx, sy, tx: 0.0, ty: 0.0 }
}

///|
pub fn Affine2D::translate(tx~ : Double, ty~ : Double) -> Affine2D {
  { sx: 1.0, sy: 1.0, tx, ty }
}

///|
pub fn Affine2D::then(self : Affine2D, next : Affine2D) -> Affine2D {
  {
    sx: self.sx * next.sx,
    sy: self.sy * next.sy,
    tx: self.tx * next.sx + next.tx,
    ty: self.ty * next.sy + next.ty,
  }
}

///|
pub fn Affine2D::apply_point(self : Affine2D, point : Point) -> Point {
  Point::new(x=point.x * self.sx + self.tx, y=point.y * self.sy + self.ty)
}

///|
pub fn Affine2D::apply_rect(self : Affine2D, rect : Rect) -> Rect {
  Rect::new(
    x=rect.x * self.sx + self.tx,
    y=rect.y * self.sy + self.ty,
    width=rect.width * self.sx,
    height=rect.height * self.sy,
  )
}

///|
pub fn Overlay::transform(self : Overlay, affine : Affine2D) -> Overlay {
  match self {
    BBox(rect~, label~, color~) =>
      BBox(rect=affine.apply_rect(rect), label~, color~)
    Mask(polygons~, label~, color~) =>
      Mask(
        polygons=polygons.map(poly => poly.map(p => affine.apply_point(p))),
        label~,
        color~,
      )
    Keypoints(points~, color~) =>
      Keypoints(
        points=points.map(point => {
          point: affine.apply_point(point.point),
          name: point.name,
          visible: point.visible,
        }),
        color~,
      )
    Trajectory(path~, color~) =>
      Trajectory(
        path=Trajectory::new(
          id=path.id,
          points=path.points.map(p => affine.apply_point(p)),
        ),
        color~,
      )
    Heatmap(cells~, low~, high~) =>
      Heatmap(
        cells=cells.map(cell => {
          HeatCell::new(rect=affine.apply_rect(cell.rect), value=cell.value)
        }),
        low~,
        high~,
      )
    ErrorRegion(rect~, expected~, actual~, severity~) =>
      ErrorRegion(rect=affine.apply_rect(rect), expected~, actual~, severity~)
  }
}

///|
pub fn Layer::transform(self : Layer, affine : Affine2D) -> Layer {
  {
    id: self.id,
    visible: self.visible,
    opacity: self.opacity,
    overlays: self.overlays.map(overlay => overlay.transform(affine)),
  }
}

///|
pub fn DebugDocument::resize(
  self : DebugDocument,
  width~ : Int,
  height~ : Int,
) -> DebugDocument {
  let sx = width.to_double() / self.image.width.to_double()
  let sy = height.to_double() / self.image.height.to_double()
  let affine = Affine2D::scale(sx~, sy~)
  {
    title: self.title,
    image: ImageSpec::new(width~, height~),
    image_href: self.image_href,
    layers: self.layers.map(layer => layer.transform(affine)),
  }
}

///|
pub fn Overlay::bounds(self : Overlay) -> Rect? {
  match self {
    BBox(rect~, ..) => Some(rect)
    Mask(polygons~, ..) => bounds_of_polygons(polygons)
    Keypoints(points~, ..) =>
      bounds_of_points_public(
        points.filter_map(kp => if kp.visible { Some(kp.point) } else { None }),
      )
    Trajectory(path~, ..) => bounds_of_points_public(path.points)
    Heatmap(cells~, ..) => bounds_of_rects(cells.map(cell => cell.rect))
    ErrorRegion(rect~, ..) => Some(rect)
  }
}

///|
pub fn Layer::bounds(self : Layer) -> Rect? {
  bounds_of_rects(self.overlays.filter_map(overlay => overlay.bounds()))
}

///|
pub fn DebugDocument::content_bounds(self : DebugDocument) -> Rect? {
  bounds_of_rects(self.layers.filter_map(layer => layer.bounds()))
}

///|
fn bounds_of_polygons(polygons : Array[Array[Point]]) -> Rect? {
  let points : Array[Point] = []
  for polygon in polygons {
    points.append(polygon)
  }
  bounds_of_points_public(points)
}

///|
fn bounds_of_points_public(points : Array[Point]) -> Rect? {
  match points.get(0) {
    None => None
    Some(first) => {
      let mut min_x = first.x
      let mut min_y = first.y
      let mut max_x = first.x
      let mut max_y = first.y
      for point in points {
        if point.x < min_x {
          min_x = point.x
        }
        if point.y < min_y {
          min_y = point.y
        }
        if point.x > max_x {
          max_x = point.x
        }
        if point.y > max_y {
          max_y = point.y
        }
      }
      Some(
        Rect::new(x=min_x, y=min_y, width=max_x - min_x, height=max_y - min_y),
      )
    }
  }
}

///|
fn bounds_of_rects(rects : Array[Rect]) -> Rect? {
  match rects.get(0) {
    None => None
    Some(first) => {
      let mut min_x = first.x
      let mut min_y = first.y
      let mut max_x = first.right()
      let mut max_y = first.bottom()
      for rect in rects {
        if rect.x < min_x {
          min_x = rect.x
        }
        if rect.y < min_y {
          min_y = rect.y
        }
        if rect.right() > max_x {
          max_x = rect.right()
        }
        if rect.bottom() > max_y {
          max_y = rect.bottom()
        }
      }
      Some(
        Rect::new(x=min_x, y=min_y, width=max_x - min_x, height=max_y - min_y),
      )
    }
  }
}