///|
pub struct AtlasOptions {
  max_width : Int
  padding : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct PackedFrame {
  filename : String
  source : Rect
  packed : Rect
  duration : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct AtlasPlan {
  image : String
  size : Size
  frames : Array[PackedFrame]
  occupancy : Double
} derive(Debug, Eq, ToJson, FromJson)

///|
pub suberror AtlasError {
  InvalidMaxWidth(Int)
  InvalidPadding(Int)
  FrameTooWide(String, Int, Int)
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn AtlasOptions::default() -> AtlasOptions {
  { max_width: 1024, padding: 1 }
}

///|
pub fn pack_rows(
  sheet : SpriteSheet,
  options? : AtlasOptions = AtlasOptions::default(),
) -> AtlasPlan {
  let mut x = options.padding
  let mut y = options.padding
  let mut row_h = 0
  let mut used_area = 0
  let packed : Array[PackedFrame] = []
  for frame in sheet.frames {
    let w = frame.source_size.w
    let h = frame.source_size.h
    if x + w + options.padding > options.max_width && x > options.padding {
      x = options.padding
      y += row_h + options.padding
      row_h = 0
    }
    let rect = { x, y, w, h }
    packed.push({
      filename: frame.filename,
      source: frame.frame,
      packed: rect,
      duration: frame.duration,
    })
    used_area += rect.area()
    x += w + options.padding
    if h > row_h {
      row_h = h
    }
  }
  let height = y + row_h + options.padding
  let area = options.max_width * height
  {
    image: sheet.image,
    size: { w: options.max_width, h: height },
    frames: packed,
    occupancy: if area == 0 {
      0.0
    } else {
      used_area.to_double() / area.to_double()
    },
  }
}

///|
pub fn pack_rows_checked(
  sheet : SpriteSheet,
  options? : AtlasOptions = AtlasOptions::default(),
) -> AtlasPlan raise {
  if options.max_width <= 0 {
    raise InvalidMaxWidth(options.max_width)
  }
  if options.padding < 0 {
    raise InvalidPadding(options.padding)
  }
  for frame in sheet.frames {
    if frame.source_size.w + options.padding * 2 > options.max_width {
      raise FrameTooWide(frame.filename, frame.source_size.w, options.max_width)
    }
  }
  pack_rows(sheet, options~)
}

///|
pub fn AtlasPlan::packed_area(self : AtlasPlan) -> Int {
  self.frames.fold(init=0, (total, frame) => total + frame.packed.area())
}

///|
pub fn AtlasPlan::to_json_string(self : AtlasPlan, indent? : Int = 2) -> String {
  self.to_json().stringify(indent~)
}