///|
pub(all) enum BoxKind {
  BoxHit
  BoxHurt
  BoxCollision
  BoxOrigin
  BoxCustom(String)
} derive(Eq, @debug.Debug)

///|
pub fn BoxKind::from_slice_name(name : String) -> BoxKind {
  if string_has(name, "hit") || string_has(name, "Hit") {
    BoxHit
  } else if string_has(name, "hurt") || string_has(name, "Hurt") {
    BoxHurt
  } else if string_has(name, "collision") ||
    string_has(name, "Collision") ||
    string_has(name, "solid") ||
    string_has(name, "Solid") {
    BoxCollision
  } else if string_has(name, "origin") || string_has(name, "Origin") {
    BoxOrigin
  } else {
    BoxCustom(name)
  }
}

///|
pub fn BoxKind::to_string(self : BoxKind) -> String {
  match self {
    BoxHit => "hit"
    BoxHurt => "hurt"
    BoxCollision => "collision"
    BoxOrigin => "origin"
    BoxCustom(name) => name
  }
}

///|
pub(all) struct FrameBox {
  frame_index : Int
  frame_name : String
  name : String
  kind : BoxKind
  bounds : Rect
  pivot : Point
  has_pivot : Bool
} derive(Eq, @debug.Debug)

///|
pub fn FrameBox::new(
  frame_index : Int,
  frame_name : String,
  name : String,
  kind : BoxKind,
  bounds : Rect,
  pivot? : Point = Point::new(0, 0),
  has_pivot? : Bool = false,
) -> FrameBox {
  { frame_index, frame_name, name, kind, bounds, pivot, has_pivot }
}

///|
pub(all) struct FrameBoxTable {
  boxes : Array[FrameBox]
} derive(Eq, @debug.Debug)

///|
pub fn FrameBoxTable::empty() -> FrameBoxTable {
  { boxes: [] }
}

///|
pub fn FrameBoxTable::for_frame(
  self : FrameBoxTable,
  frame_index : Int,
) -> Array[FrameBox] {
  let out : Array[FrameBox] = []
  for box in self.boxes {
    if box.frame_index == frame_index {
      out.push(box)
    }
  }
  out
}

///|
pub fn FrameBoxTable::find_named(
  self : FrameBoxTable,
  frame_index : Int,
  name : String,
) -> FrameBox? {
  for box in self.boxes {
    if box.frame_index == frame_index && box.name == name {
      return Some(box)
    }
  }
  None
}

///|
pub fn FrameBoxTable::count_kind(self : FrameBoxTable, kind : BoxKind) -> Int {
  for box in self.boxes; total = 0 {
    if box.kind == kind {
      continue total + 1
    } else {
      continue total
    }
  } nobreak {
    total
  }
}

///|
pub fn build_frame_box_table(sheet : SpriteSheet) -> FrameBoxTable {
  let boxes : Array[FrameBox] = []
  for frame in sheet.frames {
    for slice in sheet.meta.slices {
      match slice.key_for_frame(frame.index) {
        Some(key) =>
          if string_is_box_name(slice.name) || key.has_pivot {
            boxes.push(
              FrameBox::new(
                frame.index,
                frame.name,
                slice.name,
                BoxKind::from_slice_name(slice.name),
                key.bounds,
                pivot=key.pivot,
                has_pivot=key.has_pivot,
              ),
            )
          }
        None => ()
      }
    }
  }
  { boxes, }
}

///|
pub fn has_required_box_kinds(
  table : FrameBoxTable,
  require_hit? : Bool = false,
  require_hurt? : Bool = false,
  require_collision? : Bool = false,
) -> Bool {
  (!require_hit || table.count_kind(BoxHit) > 0) &&
  (!require_hurt || table.count_kind(BoxHurt) > 0) &&
  (!require_collision || table.count_kind(BoxCollision) > 0)
}

///|
pub fn boxes_intersect(a : FrameBox, b : FrameBox) -> Bool {
  a.bounds.intersects(b.bounds)
}

///|
pub fn first_intersection(
  boxes : Array[FrameBox],
  kind_a : BoxKind,
  kind_b : BoxKind,
) -> (FrameBox, FrameBox)? {
  for i in 0..