///|
pub enum AtlasStrategy {
  Rows
  CompactRows
  PowerOfTwo
} derive(Debug, Eq, ToJson, FromJson)

///|
fn next_power_of_two(value : Int) -> Int {
  let mut result = 1
  let target = if value < 1 { 1 } else { value }
  while result < target {
    result *= 2
  }
  result
}

///|
fn compact_frames(sheet : SpriteSheet) -> SpriteSheet {
  let frames = sheet.frames.copy()
  frames.sort_by(fn(left, right) {
    let area_order = right.source_size.area().compare(left.source_size.area())
    if area_order == 0 {
      left.filename.compare(right.filename)
    } else {
      area_order
    }
  })
  { ..sheet, frames, }
}

///|
pub fn pack_strategy(
  sheet : SpriteSheet,
  strategy~ : AtlasStrategy,
  options? : AtlasOptions = AtlasOptions::default(),
) -> AtlasPlan {
  let input = match strategy {
    CompactRows => compact_frames(sheet)
    _ => sheet
  }
  let base = pack_rows(input, options~)
  match strategy {
    PowerOfTwo => {
      let width = next_power_of_two(base.size.w)
      let height = next_power_of_two(base.size.h)
      {
        ..base,
        size: { w: width, h: height },
        occupancy: if width * height == 0 {
          0.0
        } else {
          base.packed_area().to_double() / (width * height).to_double()
        },
      }
    }
    _ => base
  }
}

///|
pub fn pack_strategy_checked(
  sheet : SpriteSheet,
  strategy~ : AtlasStrategy,
  options? : AtlasOptions = AtlasOptions::default(),
) -> AtlasPlan raise {
  let plan = pack_rows_checked(sheet, options~)
  match strategy {
    Rows => plan
    CompactRows => pack_strategy(sheet, strategy=CompactRows, options~)
    PowerOfTwo => pack_strategy(sheet, strategy=PowerOfTwo, options~)
  }
}

///|
pub fn AtlasPlan::fits(self : AtlasPlan, bounds : Size) -> Bool {
  self.size.w <= bounds.w && self.size.h <= bounds.h
}

///|
pub fn AtlasPlan::max_frame_width(self : AtlasPlan) -> Int {
  self.frames.fold(init=0, (current, frame) => {
    if frame.packed.w > current {
      frame.packed.w
    } else {
      current
    }
  })
}

///|
pub fn AtlasPlan::max_frame_height(self : AtlasPlan) -> Int {
  self.frames.fold(init=0, (current, frame) => {
    if frame.packed.h > current {
      frame.packed.h
    } else {
      current
    }
  })
}

///|
pub fn AtlasPlan::frame_named(self : AtlasPlan, name : String) -> PackedFrame? {
  match self.frames.search_by(fn(frame) { frame.filename == name }) {
    Some(index) => Some(self.frames[index])
    None => None
  }
}

///|
pub fn AtlasPlan::contains_all_frames(
  self : AtlasPlan,
  sheet : SpriteSheet,
) -> Bool {
  if self.frames.length() != sheet.frames.length() {
    return false
  }
  for frame in self.frames {
    if frame.packed.x < 0 ||
      frame.packed.y < 0 ||
      !self.size_rect().contains_rect(frame.packed) {
      return false
    }
  }
  true
}

///|
fn AtlasPlan::size_rect(self : AtlasPlan) -> Rect {
  { x: 0, y: 0, w: self.size.w, h: self.size.h }
}

///|
pub fn AtlasPlan::padding_gaps(self : AtlasPlan) -> Int {
  if self.frames.length() < 2 {
    return 0
  }
  let mut gaps = 0
  for i in 1.. previous.right() {
      gaps += current.x - previous.right()
    }
  }
  gaps
}