///|
/// A soft constraint for automatic poster layout.
pub(all) enum Constraint {
  KeepInside(Rect)
  MinimumDistance(Double)
  AlignX(Double)
  AlignY(Double)
  CenterIn(Rect)
} derive(Eq, Debug, ToJson)

///|
pub fn constraint_penalty(
  point : Vec2,
  constraints : Array[Constraint],
) -> Double {
  let mut penalty = 0.0
  for constraint in constraints {
    match constraint {
      KeepInside(bounds) => {
        if point.x < bounds.x {
          penalty += bounds.x - point.x
        }
        if point.x > bounds.right() {
          penalty += point.x - bounds.right()
        }
        if point.y < bounds.y {
          penalty += bounds.y - point.y
        }
        if point.y > bounds.bottom() {
          penalty += point.y - bounds.bottom()
        }
      }
      MinimumDistance(distance) =>
        if point.length() < distance {
          penalty += distance - point.length()
        }
      AlignX(value) => penalty += (point.x - value).abs()
      AlignY(value) => penalty += (point.y - value).abs()
      CenterIn(bounds) => penalty += point.distance(bounds.center())
    }
  }
  penalty
}

///|
pub fn project_constraints(
  point : Vec2,
  constraints : Array[Constraint],
  iterations : Int,
) -> Vec2 {
  let mut current = point
  for _ in 0..
          current = Vec2::new(
            current.x.max(bounds.x).min(bounds.right()),
            current.y.max(bounds.y).min(bounds.bottom()),
          )
        MinimumDistance(distance) =>
          if current.length() < distance {
            current = current.normalized().scale(distance)
          }
        AlignX(value) => current = Vec2::new(value, current.y)
        AlignY(value) => current = Vec2::new(current.x, value)
        CenterIn(bounds) => current = current.lerp(bounds.center(), 0.5)
      }
    }
  }
  current
}

///|
pub(all) struct LayoutCandidate {
  position : Vec2
  score : Double
  constraints : Array[Constraint]
} derive(Eq, Debug, ToJson)

///|
pub fn optimize_position(
  start : Vec2,
  constraints : Array[Constraint],
  steps : Int,
  step_size : Double,
) -> LayoutCandidate {
  let mut position = start
  let mut best = constraint_penalty(position, constraints)
  for step in 0.. Array[LayoutCandidate] {
  let result = []
  for start in starts {
    result.push(optimize_position(start, constraints, steps, step_size))
  }
  result
}

///|
pub fn best_candidate(candidates : Array[LayoutCandidate]) -> LayoutCandidate? {
  if candidates.length() == 0 {
    None
  } else {
    let mut best = candidates[0]
    for candidate in candidates[1:] {
      if candidate.score > best.score {
        best = candidate
      }
    }
    Some(best)
  }
}