// Port of rich/_ratio.py.

///|
/// An edge for `ratio_resolve`.
pub(all) struct Edge {
  size : Int?
  ratio : Int
  minimum_size : Int
} derive(Eq, Debug)

///|
/// A rational number p/q with q > 0.
priv struct Frac {
  num : Int64
  den : Int64
}

///|
fn gcd64(a : Int64, b : Int64) -> Int64 {
  let mut a = if a < 0L { -a } else { a }
  let mut b = if b < 0L { -b } else { b }
  while b != 0L {
    let t = a % b
    a = b
    b = t
  }
  a
}

///|
fn Frac::make(num : Int64, den : Int64) -> Frac {
  let g = gcd64(num, den)
  let g = if g == 0L { 1L } else { g }
  let (num, den) = if den < 0L {
    (-num / g, -den / g)
  } else {
    (num / g, den / g)
  }
  { num, den, }
}

///|
fn Frac::add(a : Frac, b : Frac) -> Frac {
  Frac::make(a.num * b.den + b.num * a.den, a.den * b.den)
}

///|
fn Frac::mul_int(a : Frac, n : Int) -> Frac {
  Frac::make(a.num * n.to_int64(), a.den)
}

///|
fn Frac::le_int(a : Frac, n : Int) -> Bool {
  a.num <= n.to_int64() * a.den
}

///|
/// Python `divmod(frac, 1)`.
fn Frac::divmod1(a : Frac) -> (Int, Frac) {
  let q = if a.num >= 0L {
    a.num / a.den
  } else {
    -((-a.num + a.den - 1L) / a.den)
  }
  (q.to_int(), Frac::make(a.num - q * a.den, a.den))
}

///|
/// Divide total space to satisfy size, ratio, and minimum_size constraints.
pub fn ratio_resolve(total : Int, edges : ArrayView[Edge]) -> Array[Int] {
  let sizes : Array[Int?] = edges.map(e => {
    match e.size {
      Some(0) | None => None
      s => s
    }
  })
  while sizes.iter().any(s => s is None) {
    let flexible_edges = []
    for index, size in sizes {
      if size is None {
        flexible_edges.push((index, edges[index]))
      }
    }
    let mut used = 0
    for s in sizes {
      used += s.unwrap_or(0)
    }
    let remaining = total - used
    if remaining <= 0 {
      return sizes.mapi((i, size) => {
        match size {
          None =>
            if edges[i].minimum_size == 0 {
              1
            } else {
              edges[i].minimum_size
            }
          Some(s) => s
        }
      })
    }
    let mut ratio_sum = 0
    for fe in flexible_edges {
      ratio_sum += if fe.1.ratio == 0 { 1 } else { fe.1.ratio }
    }
    let portion = Frac::make(remaining.to_int64(), ratio_sum.to_int64())
    let mut broke = false
    for fe in flexible_edges {
      let (index, edge) = fe
      if portion.mul_int(edge.ratio).le_int(edge.minimum_size) {
        sizes[index] = Some(edge.minimum_size)
        broke = true
        break
      }
    }
    if !broke {
      let mut remainder = Frac::make(0L, 1L)
      for fe in flexible_edges {
        let (index, edge) = fe
        let (size, rem) = portion.mul_int(edge.ratio).add(remainder).divmod1()
        remainder = rem
        sizes[index] = Some(size)
      }
      break
    }
  }
  sizes.map(s => s.unwrap_or(0))
}

///|
/// Divide an integer total in to parts based on ratios (reducing values).
pub fn ratio_reduce(
  total : Int,
  ratios : ArrayView[Int],
  maximums : ArrayView[Int],
  values : ArrayView[Int],
) -> Array[Int] {
  let ratios = ratios.mapi((i, r) => if maximums[i] != 0 { r } else { 0 })
  let mut total_ratio = 0
  for r in ratios {
    total_ratio += r
  }
  if total_ratio == 0 {
    return values.to_owned()
  }
  let mut total_remaining = total
  let result = []
  for i, ratio in ratios {
    let maximum = maximums[i]
    let value = values[i]
    if ratio != 0 && total_ratio > 0 {
      let distributed = @cmp.minimum(
        maximum,
        @color.py_round(
          ratio.to_double() *
          total_remaining.to_double() /
          total_ratio.to_double(),
        ),
      )
      result.push(value - distributed)
      total_remaining -= distributed
      total_ratio -= ratio
    } else {
      result.push(value)
    }
  }
  result
}

///|
/// Distribute an integer total in to parts based on ratios.
pub fn ratio_distribute(
  total : Int,
  ratios : ArrayView[Int],
  minimums? : ArrayView[Int],
) -> Array[Int] {
  let ratios = match minimums {
    Some(m) if m.length() != 0 =>
      ratios.mapi((i, r) => if m[i] != 0 { r } else { 0 })
    _ => ratios.to_owned()
  }
  let mut total_ratio = 0
  for r in ratios {
    total_ratio += r
  }
  guard total_ratio > 0 else { abort("Sum of ratios must be > 0") }
  let mut total_remaining = total
  let distributed_total = []
  for i, ratio in ratios {
    let minimum = match minimums {
      Some(m) => m[i]
      None => 0
    }
    let distributed = if total_ratio > 0 {
      @cmp.maximum(
        minimum,
        (ratio.to_double() *
        total_remaining.to_double() /
        total_ratio.to_double())
        .ceil()
        .to_int(),
      )
    } else {
      total_remaining
    }
    distributed_total.push(distributed)
    total_ratio -= ratio
    total_remaining -= distributed
  }
  distributed_total
}