///|
pub struct ErasureReport {
  values : Array[Int32?]
  recovered : Int32
  unresolved : Array[Int32]
  complete : Bool
} derive(Debug)

///|
/// Queue-based BEC peeling; None is an erasure, not a guessed zero.
/// A stopping set is returned explicitly. Contradictory known observations raise.
pub fn ParityCheck::peel(
  self : ParityCheck,
  observations : Array[Int32?],
) -> ErasureReport raise CodeError {
  if observations.length() != self.n {
    raise LengthMismatch(self.n, observations.length())
  }
  let values = observations.copy()
  for i, value in values {
    match value {
      Some(bit) => if bit != 0 && bit != 1 { raise InvalidBit(i, bit) }
      None => ()
    }
  }
  let unknown = Array::make(self.checks(), 0)
  let parity = Array::make(self.checks(), 0)
  let queue : Array[Int32] = []
  for c, row in self.rows {
    for v in row {
      match values[v] {
        Some(bit) => parity[c] = parity[c] ^ bit
        None => unknown[c] += 1
      }
    }
    if unknown[c] == 0 && parity[c] != 0 {
      raise Inconsistent("known bits violate a check")
    }
    if unknown[c] == 1 {
      queue.push(c)
    }
  }
  let mut recovered = 0
  let mut head = 0
  while head < queue.length() {
    let c = queue[head]
    head += 1
    if unknown[c] != 1 {
      continue
    }
    let mut target = -1
    for v in self.rows[c] {
      if values[v] is None {
        target = v
        break
      }
    }
    if target < 0 {
      raise Inconsistent("peeling bookkeeping")
    }
    let bit = parity[c]
    values[target] = Some(bit)
    recovered += 1
    for check in self.columns[target] {
      parity[check] = parity[check] ^ bit
      unknown[check] -= 1
      if unknown[check] == 0 && parity[check] != 0 {
        raise Inconsistent("recovered bits contradict another check")
      }
      if unknown[check] == 1 {
        queue.push(check)
      }
    }
  }
  let unresolved : Array[Int32] = []
  for v, value in values {
    if value is None {
      unresolved.push(v)
    }
  }
  { values, recovered, unresolved, complete: unresolved.is_empty(), }
}

///|
pub struct BitFlipReport {
  word : Array[Int32]
  converged : Bool
  iterations : Int32
  syndrome_weights : Array[Int32]
} derive(Debug, ToJson)

///|
/// Parallel strict-majority bit flipping. Useful as a hard-decision baseline only.
/// A flip requires more unsatisfied than satisfied incident checks; ties do not flip.
pub fn ParityCheck::bit_flip(
  self : ParityCheck,
  received : Array[Int32],
  iterations~ : Int32 = 50,
) -> BitFlipReport raise CodeError {
  ensure_bits(received, self.n)
  if iterations < 0 || iterations > 10000 {
    raise InvalidNumber("bit-flip iterations")
  }
  if iterations.to_int64() * self.edges().to_int64() > 50000000L {
    raise ResourceLimit("bit-flip edge-iterations")
  }
  let word = received.copy()
  let weights = [self.syndrome_weight(word)]
  let mut completed = 0
  if weights[0] != 0 {
    for step in 1..<=iterations {
      let bad = Array::make(self.n, 0)
      for row in self.rows {
        let mut bit = 0
        for v in row {
          bit = bit ^ word[v]
        }
        if bit == 1 {
          for v in row {
            bad[v] += 1
          }
        }
      }
      let mut flips = 0
      for v in 0.. self.columns[v].length() {
          word[v] = 1 - word[v]
          flips += 1
        }
      }
      completed = step
      let weight = self.syndrome_weight(word)
      weights.push(weight)
      if weight == 0 || flips == 0 {
        break
      }
    }
  }
  {
    word,
    converged: weights[weights.length() - 1] == 0,
    iterations: completed,
    syndrome_weights: weights,
  }
}

///|
pub extend ErasureReport with @debug.Debug::{to_repr}

///|
pub extend ErasureReport with ToJson::{to_json}

///|
pub extend BitFlipReport with @debug.Debug::{to_repr}

///|
pub extend BitFlipReport with ToJson::{to_json}

///|
/// JSON erasures are null, known bits are numbers (not generic Option arrays).
pub impl ToJson for ErasureReport with fn to_json(self) {
  let values = self.values.map(fn(value) {
    match value {
      None => Json::null()
      Some(bit) => Json(bit)
    }
  })
  {
    "values": Json(values),
    "recovered": self.recovered,
    "unresolved": Json(self.unresolved),
    "complete": self.complete,
  }
}