///|
pub(all) suberror TallyError {
  Invalid(String)
  Limit(String)
} derive(Eq, @debug.Debug)

///|
pub(all) struct Ballot {
  weight : Int32
  ranking : Array[Array[String]]
} derive(Eq, @debug.Debug)

///|
pub struct Election {
  priv candidates : Array[String]
  priv ballots : Array[Ballot]
  priv pairwise : Array[Array[Int32]]
  priv total : Int32
  priv nonblank : Int32
}

///|
pub(all) struct Beatpath {
  from : String
  to : String
  strength : Int32
  path : Array[String]
} derive(Eq, ToJson, @debug.Debug)

///|
pub(all) struct SchulzeResult {
  winners : Array[String]
  strengths : Array[Array[Int32]]
  witnesses : Array[Beatpath]
} derive(Eq, ToJson, @debug.Debug)

///|
pub(all) struct LockedEdge {
  from : String
  to : String
  margin : Int32
  winning_votes : Int32
  locked : Bool
  cycle_path : Array[String]
} derive(Eq, ToJson, @debug.Debug)

///|
pub(all) struct RankedPairsResult {
  winners : Array[String]
  decisions : Array[LockedEdge]
} derive(Eq, ToJson, @debug.Debug)

///|
pub(all) struct Round {
  active : Array[String]
  counts : Map[String, Int32]
  continuing_weight : Int32
  exhausted_weight : Int32
  tied_lowest : Array[String]
  eliminated : String
} derive(Eq, ToJson, @debug.Debug)

///|
pub(all) struct IrvResult {
  winners : Array[String]
  rounds : Array[Round]
  needs_tiebreak : Bool
} derive(Eq, ToJson, @debug.Debug)

///|
pub(all) struct TieWitness {
  winner : String
  eliminations : Array[String]
} derive(Eq, ToJson, @debug.Debug)

///|
pub(all) struct IrvPossibilities {
  possible_winners : Array[String]
  explored_states : Int32
  witnesses : Array[TieWitness]
} derive(Eq, ToJson, @debug.Debug)

///|
pub fn election(
  candidates : Array[String],
  ballots : Array[Ballot],
) -> Election raise TallyError {
  guard candidates.length() > 0 &&
    candidates.length() <= 32 &&
    ballots.length() <= 10000 else {
    raise Limit("use 1..32 candidates and at most 10000 weighted ballot types")
  }
  let ids = candidates.copy()
  ids.sort()
  let positions : Map[String, Int32] = Map([])
  for i, id in ids {
    guard id.length() > 0 && id.length() <= 128 && !positions.contains(id) else {
      raise Invalid("candidate IDs must be nonempty and unique")
    }
    positions[id] = i
  }
  let matrix = ids.map(_ => Array::make(ids.length(), 0))
  let mut total = 0
  let mut nonblank = 0
  let stored : Array[Ballot] = []
  for ballot in ballots {
    guard ballot.weight > 0 && ballot.weight <= 1000000000 - total else {
      raise Invalid("positive weights must total at most 1000000000")
    }
    total = total + ballot.weight
    if ballot.ranking.length() > 0 {
      nonblank = nonblank + ballot.weight
    }
    let ranks = Array::make(ids.length(), 1000)
    for level, group in ballot.ranking {
      guard group.length() > 0 else {
        raise Invalid("empty rank groups are not permitted")
      }
      for id in group {
        guard positions.get(id) is Some(i) else {
          raise Invalid("unknown ranked candidate")
        }
        guard ranks[i] == 1000 else {
          raise Invalid("candidate repeated in ballot")
        }
        ranks[i] = level
      }
    }
    for i = 0; i < ids.length(); i = i + 1 {
      for j = 0; j < ids.length(); j = j + 1 {
        if ranks[i] < ranks[j] {
          matrix[i][j] = matrix[i][j] + ballot.weight
        }
      }
    }
    stored.push({
      weight: ballot.weight,
      ranking: ballot.ranking.map(group => group.copy()),
    })
  }
  { candidates: ids, ballots: stored, pairwise: matrix, total, nonblank, }
}

///|
pub fn Election::candidates(self : Election) -> Array[String] {
  self.candidates.copy()
}

///|
pub fn Election::pairwise(self : Election) -> Array[Array[Int32]] {
  self.pairwise.map(row => row.copy())
}

///|
pub fn Election::total_weight(self : Election) -> Int32 {
  self.total
}

///|
pub fn Election::condorcet(self : Election) -> Array[String] {
  if self.nonblank == 0 {
    return []
  }
  let winners : Array[String] = []
  for i, id in self.candidates {
    let mut beats_all = true
    for j = 0; j < self.candidates.length(); j = j + 1 {
      if i != j && self.pairwise[i][j] <= self.pairwise[j][i] {
        beats_all = false
      }
    }
    if beats_all {
      winners.push(id)
    }
  }
  winners
}