///|
pub struct TransformOptions {
  offset : Point
  scale : Int
  duration_scale : Int
  rename_prefix : String
  clamp_to_sheet : Bool
  preserve_boxes : Bool
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn TransformOptions::default() -> TransformOptions {
  {
    offset: { x: 0, y: 0 },
    scale: 1,
    duration_scale: 100,
    rename_prefix: "",
    clamp_to_sheet: false,
    preserve_boxes: true,
  }
}

///|
pub fn TransformOptions::with_offset(
  self : TransformOptions,
  offset : Point,
) -> TransformOptions {
  { ..self, offset, }
}

///|
pub fn TransformOptions::with_scale(
  self : TransformOptions,
  scale : Int,
) -> TransformOptions {
  { ..self, scale: if scale <= 0 { 1 } else { scale } }
}

///|
pub fn TransformOptions::with_duration_scale(
  self : TransformOptions,
  percent : Int,
) -> TransformOptions {
  { ..self, duration_scale: if percent <= 0 { 1 } else { percent } }
}

///|
pub fn TransformOptions::with_prefix(
  self : TransformOptions,
  prefix : String,
) -> TransformOptions {
  { ..self, rename_prefix: prefix }
}

///|
fn transform_rect(rect : Rect, options : TransformOptions) -> Rect {
  {
    x: rect.x * options.scale + options.offset.x,
    y: rect.y * options.scale + options.offset.y,
    w: rect.w * options.scale,
    h: rect.h * options.scale,
  }
}

///|
fn transform_point(point : Point, options : TransformOptions) -> Point {
  {
    x: point.x * options.scale + options.offset.x,
    y: point.y * options.scale + options.offset.y,
  }
}

///|
fn transform_box(
  box : CollisionBox,
  options : TransformOptions,
) -> CollisionBox {
  { ..box, rect: transform_rect(box.rect, options) }
}

///|
pub fn transform_frame(
  frame : SourceFrame,
  options : TransformOptions,
) -> SourceFrame {
  let boxes = if options.preserve_boxes {
    frame.boxes.map(fn(box) { transform_box(box, options) })
  } else {
    []
  }
  {
    ..frame,
    frame: transform_rect(frame.frame, options),
    sprite_source_size: transform_rect(frame.sprite_source_size, options),
    source_size: frame.source_size.scale(options.scale),
    duration: frame.duration * options.duration_scale / 100,
    boxes,
  }
}

///|
pub fn transform_frame_name(
  frame : SourceFrame,
  prefix : String,
) -> SourceFrame {
  { ..frame, filename: prefix + frame.filename }
}

///|
pub fn transform_frame_duration(
  frame : SourceFrame,
  percent : Int,
) -> SourceFrame {
  { ..frame, duration: frame.duration * percent / 100 }
}

///|
pub fn transform_frame_offset(
  frame : SourceFrame,
  offset : Point,
) -> SourceFrame {
  { ..frame, frame: frame.frame.translate(offset.x, offset.y) }
}

///|
pub fn transform_frame_scale(frame : SourceFrame, scale : Int) -> SourceFrame {
  transform_frame(frame, TransformOptions::default().with_scale(scale))
}

///|
pub fn transform_frames(
  frames : Array[SourceFrame],
  options : TransformOptions,
) -> Array[SourceFrame] {
  frames.map(fn(frame) { transform_frame(frame, options) })
}

///|
pub fn transform_slice_key(
  key : SliceKey,
  options : TransformOptions,
) -> SliceKey {
  {
    ..key,
    bounds: transform_rect(key.bounds, options),
    center: key.center.map(fn(rect) { transform_rect(rect, options) }),
    pivot: key.pivot.map(fn(point) { transform_point(point, options) }),
  }
}

///|
pub fn transform_slice(slice : Slice, options : TransformOptions) -> Slice {
  {
    ..slice,
    bounds: transform_rect(slice.bounds, options),
    center: slice.center.map(fn(rect) { transform_rect(rect, options) }),
    pivot: slice.pivot.map(fn(point) { transform_point(point, options) }),
    keys: slice.keys.map(fn(key) { transform_slice_key(key, options) }),
  }
}

///|
pub fn transform_slices(
  slices : Array[Slice],
  options : TransformOptions,
) -> Array[Slice] {
  slices.map(fn(slice) { transform_slice(slice, options) })
}

///|
pub fn transform_sheet(
  sheet : SpriteSheet,
  options : TransformOptions,
) -> SpriteSheet {
  {
    ..sheet,
    size: sheet.size.scale(options.scale),
    frames: transform_frames(sheet.frames, options),
    slices: transform_slices(sheet.slices, options),
  }
}

///|
pub fn rename_sheet_frames(sheet : SpriteSheet, prefix : String) -> SpriteSheet {
  {
    ..sheet,
    frames: sheet.frames.map(fn(frame) { transform_frame_name(frame, prefix) }),
  }
}

///|
pub fn retime_sheet(sheet : SpriteSheet, percent : Int) -> SpriteSheet {
  {
    ..sheet,
    frames: sheet.frames.map(fn(frame) {
      transform_frame_duration(frame, percent)
    }),
  }
}

///|
pub fn offset_sheet(sheet : SpriteSheet, offset : Point) -> SpriteSheet {
  {
    ..sheet,
    frames: sheet.frames.map(fn(frame) { transform_frame_offset(frame, offset) }),
  }
}

///|
pub fn scale_sheet(sheet : SpriteSheet, scale : Int) -> SpriteSheet {
  transform_sheet(sheet, TransformOptions::default().with_scale(scale))
}

///|
pub fn strip_collision_boxes(sheet : SpriteSheet) -> SpriteSheet {
  { ..sheet, frames: sheet.frames.map(fn(frame) { { ..frame, boxes: [] } }) }
}

///|
pub fn strip_slices(sheet : SpriteSheet) -> SpriteSheet {
  { ..sheet, slices: [] }
}

///|
pub fn strip_tags(sheet : SpriteSheet) -> SpriteSheet {
  { ..sheet, tags: [] }
}

///|
pub fn select_frames(sheet : SpriteSheet, names : Array[String]) -> SpriteSheet {
  {
    ..sheet,
    frames: sheet.frames.filter(fn(frame) { names.contains(frame.filename) }),
  }
}

///|
pub fn select_frames_prefix(
  sheet : SpriteSheet,
  prefix : String,
) -> SpriteSheet {
  { ..sheet, frames: sheet_find_frames(sheet, prefix) }
}

///|
pub fn select_slices_prefix(
  sheet : SpriteSheet,
  prefix : String,
) -> SpriteSheet {
  {
    ..sheet,
    slices: sheet.slices.filter(fn(slice) { slice.name.has_prefix(prefix) }),
  }
}

///|
pub fn normalize_frame_positions(sheet : SpriteSheet) -> SpriteSheet {
  let min_x = sheet.frames.fold(init=0, (value, frame) => {
    if frame.frame.x < value {
      frame.frame.x
    } else {
      value
    }
  })
  let min_y = sheet.frames.fold(init=0, (value, frame) => {
    if frame.frame.y < value {
      frame.frame.y
    } else {
      value
    }
  })
  offset_sheet(sheet, { x: -min_x, y: -min_y })
}

///|
pub fn trim_empty_collision_boxes(sheet : SpriteSheet) -> SpriteSheet {
  {
    ..sheet,
    frames: sheet.frames.map(fn(frame) {
      { ..frame, boxes: frame.boxes.filter(fn(box) { !box.rect.is_empty() }) }
    }),
  }
}

///|
pub fn sort_frames_by_filename(sheet : SpriteSheet) -> SpriteSheet {
  let frames = sheet.frames.copy()
  frames.sort_by(fn(left, right) { left.filename.compare(right.filename) })
  { ..sheet, frames, }
}

///|
pub fn sort_slices_by_name(sheet : SpriteSheet) -> SpriteSheet {
  let slices = sheet.slices.copy()
  slices.sort_by(fn(left, right) { left.name.compare(right.name) })
  { ..sheet, slices, }
}

///|
pub fn canonicalize_sheet(sheet : SpriteSheet) -> SpriteSheet {
  sort_slices_by_name(
    sort_frames_by_filename(trim_empty_collision_boxes(sheet)),
  )
}

///|
pub fn transform_is_deterministic(
  sheet : SpriteSheet,
  options : TransformOptions,
) -> Bool {
  sheet_to_json(transform_sheet(sheet, options), indent=0) ==
  sheet_to_json(transform_sheet(sheet, options), indent=0)
}

///|
pub fn transform_area_delta(before : SpriteSheet, after : SpriteSheet) -> Int {
  sheet_metrics(after).total_source_area -
  sheet_metrics(before).total_source_area
}

///|
pub fn transform_duration_delta(
  before : SpriteSheet,
  after : SpriteSheet,
) -> Int {
  total_duration(after) - total_duration(before)
}

///|
pub fn transform_frame_count_delta(
  before : SpriteSheet,
  after : SpriteSheet,
) -> Int {
  after.frames.length() - before.frames.length()
}

///|
pub fn transform_preserves_frame_count(
  before : SpriteSheet,
  after : SpriteSheet,
) -> Bool {
  transform_frame_count_delta(before, after) == 0
}

///|
pub fn transform_preserves_tag_count(
  before : SpriteSheet,
  after : SpriteSheet,
) -> Bool {
  before.tags.length() == after.tags.length()
}

///|
pub fn transform_preserves_slice_count(
  before : SpriteSheet,
  after : SpriteSheet,
) -> Bool {
  before.slices.length() == after.slices.length()
}