///|
pub struct Rect {
  x : Int
  y : Int
  w : Int
  h : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct Size {
  w : Int
  h : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct Point {
  x : Int
  y : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct CollisionBox {
  name : String
  rect : Rect
} derive(Debug, Eq, ToJson, FromJson)

///|
pub enum Direction {
  Forward
  Reverse
  PingPong
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct FrameTag {
  name : String
  from : Int
  to : Int
  direction : Direction
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct Slice {
  name : String
  color : String?
  bounds : Rect
  center : Rect?
  pivot : Point?
  keys : Array[SliceKey]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct SliceKey {
  frame : Int
  bounds : Rect
  center : Rect?
  pivot : Point?
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct SourceFrame {
  filename : String
  frame : Rect
  rotated : Bool
  trimmed : Bool
  sprite_source_size : Rect
  source_size : Size
  duration : Int
  boxes : Array[CollisionBox]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct SpriteSheet {
  image : String
  size : Size
  frames : Array[SourceFrame]
  tags : Array[FrameTag]
  slices : Array[Slice]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn Rect::area(self : Rect) -> Int {
  self.w * self.h
}

///|
pub fn Rect::right(self : Rect) -> Int {
  self.x + self.w
}

///|
pub fn Rect::bottom(self : Rect) -> Int {
  self.y + self.h
}

///|
pub fn Rect::is_empty(self : Rect) -> Bool {
  self.w <= 0 || self.h <= 0
}

///|
pub fn Rect::contains_rect(self : Rect, other : Rect) -> Bool {
  other.x >= self.x &&
  other.y >= self.y &&
  other.right() <= self.right() &&
  other.bottom() <= self.bottom()
}

///|
pub fn Size::area(self : Size) -> Int {
  self.w * self.h
}

///|
pub fn SpriteSheet::frame_at(self : SpriteSheet, index : Int) -> SourceFrame? {
  self.frames.get(index)
}

///|
pub fn SpriteSheet::find_frame(
  self : SpriteSheet,
  filename : String,
) -> SourceFrame? {
  match self.frames.search_by(fn(frame) { frame.filename == filename }) {
    Some(index) => Some(self.frames[index])
    None => None
  }
}

///|
pub fn parse_sheet(text : String) -> SpriteSheet raise {
  let value = @json.parse(text)
  @json.from_json(value)
}

///|
pub fn sheet_to_json(sheet : SpriteSheet, indent? : Int = 2) -> String {
  sheet.to_json().stringify(indent~)
}

///|
pub fn total_duration(sheet : SpriteSheet) -> Int {
  let mut total = 0
  for frame in sheet.frames {
    total += frame.duration
  }
  total
}