///|
pub enum AtlasObjective {
  MinimizeArea
  MinimizeWidth
  MinimizeHeight
  MaximizeOccupancy
  Balanced
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct AtlasCandidate {
  strategy : AtlasStrategy
  size : Size
  occupancy : Double
  packed_area : Int
  score : Double
  valid : Bool
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct AtlasRecommendation {
  best : AtlasCandidate
  candidates : Array[AtlasCandidate]
  objective : AtlasObjective
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn atlas_strategy_name(strategy : AtlasStrategy) -> String {
  match strategy {
    Rows => "rows"
    CompactRows => "compact_rows"
    PowerOfTwo => "power_of_two"
  }
}

///|
fn candidate_score(plan : AtlasPlan, objective : AtlasObjective) -> Double {
  let area = (plan.size.w * plan.size.h).to_double()
  match objective {
    MinimizeArea => 0.0 - area
    MinimizeWidth => 0.0 - plan.size.w.to_double()
    MinimizeHeight => 0.0 - plan.size.h.to_double()
    MaximizeOccupancy => plan.occupancy
    Balanced => plan.occupancy * 1000.0 - area / 1000.0
  }
}

///|
fn make_candidate(
  sheet : SpriteSheet,
  strategy : AtlasStrategy,
  options : AtlasOptions,
  objective : AtlasObjective,
) -> AtlasCandidate {
  let plan = pack_strategy(sheet, strategy~, options~)
  {
    strategy,
    size: plan.size,
    occupancy: plan.occupancy,
    packed_area: plan.packed_area(),
    score: candidate_score(plan, objective),
    valid: plan.contains_all_frames(sheet),
  }
}

///|
pub fn recommend_atlas(
  sheet : SpriteSheet,
  max_width~ : Int,
  padding~ : Int,
  objective? : AtlasObjective = Balanced,
) -> AtlasRecommendation {
  let options = { max_width, padding }
  let candidates = [
    make_candidate(sheet, Rows, options, objective),
    make_candidate(sheet, CompactRows, options, objective),
    make_candidate(sheet, PowerOfTwo, options, objective),
  ]
  let mut best = candidates[0]
  for candidate in candidates[1:] {
    if candidate.score > best.score && candidate.valid {
      best = candidate
    }
  }
  { best, candidates, objective }
}

///|
pub fn AtlasRecommendation::candidate_named(
  self : AtlasRecommendation,
  strategy : AtlasStrategy,
) -> AtlasCandidate? {
  match
    self.candidates.search_by(fn(candidate) { candidate.strategy == strategy }) {
    Some(index) => Some(self.candidates[index])
    None => None
  }
}

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

///|
pub fn AtlasRecommendation::valid_candidates(
  self : AtlasRecommendation,
) -> Array[AtlasCandidate] {
  self.candidates.filter(fn(candidate) { candidate.valid })
}

///|
pub fn AtlasRecommendation::occupancy_gain(
  self : AtlasRecommendation,
) -> Double {
  match self.candidate_named(Rows) {
    None => 0.0
    Some(rows) => self.best.occupancy - rows.occupancy
  }
}

///|
pub fn AtlasCandidate::area(self : AtlasCandidate) -> Int {
  self.size.area()
}

///|
pub fn AtlasCandidate::aspect_ratio(self : AtlasCandidate) -> Double {
  if self.size.h <= 0 {
    0.0
  } else {
    self.size.w.to_double() / self.size.h.to_double()
  }
}

///|
pub fn AtlasCandidate::is_square(self : AtlasCandidate) -> Bool {
  self.size.w == self.size.h
}

///|
pub fn AtlasCandidate::score_line(self : AtlasCandidate) -> String {
  "\{atlas_strategy_name(self.strategy)}: score=\{self.score}, occupancy=\{self.occupancy}, size=\{self.size.w}x\{self.size.h}"
}

///|
pub fn atlas_candidates_for_widths(
  sheet : SpriteSheet,
  widths : Array[Int],
  padding : Int,
) -> Array[AtlasCandidate] {
  let result : Array[AtlasCandidate] = []
  for width in widths {
    if width > 0 {
      result.push(
        make_candidate(
          sheet,
          CompactRows,
          { max_width: width, padding },
          Balanced,
        ),
      )
    }
  }
  result
}

///|
pub fn best_width_for_occupancy(
  sheet : SpriteSheet,
  widths : Array[Int],
  padding : Int,
) -> AtlasCandidate? {
  let candidates = atlas_candidates_for_widths(sheet, widths, padding)
  match candidates.get(0) {
    None => None
    Some(first) => {
      let mut best = first
      for candidate in candidates[1:] {
        if candidate.occupancy > best.occupancy {
          best = candidate
        }
      }
      Some(best)
    }
  }
}

///|
pub fn best_width_for_area(
  sheet : SpriteSheet,
  widths : Array[Int],
  padding : Int,
) -> AtlasCandidate? {
  let candidates = atlas_candidates_for_widths(sheet, widths, padding)
  match candidates.get(0) {
    None => None
    Some(first) => {
      let mut best = first
      for candidate in candidates[1:] {
        if candidate.area() < best.area() {
          best = candidate
        }
      }
      Some(best)
    }
  }
}

///|
pub fn atlas_is_power_of_two(size : Size) -> Bool {
  let power = fn(value : Int) {
    let mut n = 1
    while n < value {
      n *= 2
    }
    n == value
  }
  power(size.w) && power(size.h)
}

///|
pub fn atlas_waste(candidate : AtlasCandidate) -> Int {
  candidate.area() - candidate.packed_area
}

///|
pub fn atlas_waste_ratio(candidate : AtlasCandidate) -> Double {
  if candidate.area() == 0 {
    0.0
  } else {
    atlas_waste(candidate).to_double() / candidate.area().to_double()
  }
}

///|
pub fn optimize_atlas(
  sheet : SpriteSheet,
  objective : AtlasObjective,
) -> AtlasRecommendation {
  recommend_atlas(sheet, max_width=4096, padding=1, objective~)
}

///|
pub fn optimize_for_mobile(sheet : SpriteSheet) -> AtlasRecommendation {
  recommend_atlas(sheet, max_width=2048, padding=1, objective=MinimizeArea)
}

///|
pub fn optimize_for_web(sheet : SpriteSheet) -> AtlasRecommendation {
  recommend_atlas(sheet, max_width=4096, padding=2, objective=MaximizeOccupancy)
}

///|
pub fn optimize_for_ui(sheet : SpriteSheet) -> AtlasRecommendation {
  recommend_atlas(sheet, max_width=1024, padding=2, objective=MinimizeWidth)
}

///|
pub fn atlas_recommendation_summary(
  recommendation : AtlasRecommendation,
) -> String {
  "best=\{atlas_strategy_name(recommendation.best.strategy)}, size=\{recommendation.best.size.w}x\{recommendation.best.size.h}, occupancy=\{recommendation.best.occupancy}, candidates=\{recommendation.candidates.length()}"
}