///|
/// drop_out:一个人掉线,全房还能不能继续?
///
/// 4 人局,3 号玩家在第 `GONE` 帧之后拔网线(不再发包)。示例跑两遍:
///
/// - `announce_drop = false`:没人宣布掉线,确认帧永远停在 3 号最后发包的位置,
///   其余三人只能一直靠预测往前跑,回滚窗口很快用尽;
/// - `announce_drop = true`:房主(0 号)发现 3 号超过 `TIMEOUT` 帧没动静,
///   把「3 号掉线」写进包里广播,其余人收到后同样把他移出在线集合,
///   确认帧随即恢复推进,剩下三人逐帧哈希仍然一致。
///
///   moon run examples/drop_out
const PLAYERS : Int32 = 4

///|
const TOTAL : Int32 = 480

///|
const RESEND : Int32 = 8

///|
const DELAY : Int32 = 3

///|
const JITTER : Int32 = 1

///|
/// 拔网线的那位。
const GONE_PLAYER : Int32 = 3

///|
/// 从这一帧起他不再发包。
const GONE : Int32 = 200

///|
/// 房主的判据:连续这么多帧没收到他的包就宣布掉线。
const TIMEOUT : Int32 = 30

///|
/// 一局跑完的结果。
pub(all) struct DropStats {
  /// 四个玩家各自的确认帧(3 号掉线,所以他会停住)。
  confirmed : Array[Int32]
  /// 其余三人的回滚次数之和。
  rollbacks : Int64
  /// 其余三人的重放帧数之和。
  replayed : Int64
  /// 其余三人之间首个分叉帧;-1 表示全程一致。
  first_divergence : Int32
  /// 校验和帧比对失配次数。
  mismatches : Int32
  /// 房主宣布掉线的帧;0 表示整局都没宣布。
  declared_at : Int32
}

///|
/// 把某个玩家最近 `RESEND` 帧的输入、确认帧、校验和(以及房主的掉线声明)
/// 打成一个包。
fn outbound(
  s : @session.Session[@scene.Scene],
  me : Int32,
  back : Int32,
  f : Int32,
  announce : Bool,
) -> Bytes {
  let msgs : Array[@protocol.Message] = []
  let mut m = back
  while m <= f {
    match s.player_input(me, m) {
      Some(bits) =>
        msgs.push(
          @protocol.Message::Input(
            @protocol.InputFrame::new_from(me, m, [bits]),
          ),
        )
      None => ()
    }
    m = m + 1
  }
  let c = s.confirmed()
  msgs.push(@protocol.Message::Confirm(@protocol.ConfirmFrame::new(c)))
  match s.hash_at(c) {
    Some(h) =>
      msgs.push(@protocol.Message::Checksum(@protocol.ChecksumFrame::new(c, h)))
    None => ()
  }
  if announce {
    msgs.push(
      @protocol.Message::Active(@protocol.ActiveFrame::new(GONE_PLAYER, false)),
    )
  }
  @protocol.encode_batch(msgs)
}

///|
/// 在 `keep` 这几个会话的共同确认帧上逐帧比对哈希,找首个分叉帧。
fn scan_divergence(
  sessions : Array[@session.Session[@scene.Scene]],
  keep : Int32,
) -> Int32 {
  let mut c = sessions[0].confirmed()
  let mut i = 1
  while i < keep {
    if sessions[i].confirmed() < c {
      c = sessions[i].confirmed()
    }
    i = i + 1
  }
  let mut f = 1
  while f <= c {
    let base = sessions[0].hash_at(f)
    let mut j = 1
    while j < keep {
      if sessions[j].hash_at(f) != base {
        return f
      }
      j = j + 1
    }
    f = f + 1
  }
  -1
}

///|
/// 跑一局 `total` 帧的对局;`announce_drop` 决定房主是否广播掉线。
pub fn run(announce_drop : Bool, total : Int32, verbose : Bool) -> DropStats {
  let sessions : Array[@session.Session[@scene.Scene]] = []
  let mut i = 0
  while i < PLAYERS {
    sessions.push(
      @session.Session::with_players(@scene.Scene::new(), PLAYERS, i, 128),
    )
    i = i + 1
  }
  // nets[from][to] 是一条单向链路,from == to 的那格不会被使用。
  let nets : Array[Array[@sim_net.NetSim]] = []
  let mut from = 0
  while from < PLAYERS {
    let row : Array[@sim_net.NetSim] = []
    let mut to = 0
    while to < PLAYERS {
      row.push(
        @sim_net.NetSim::new(
          @sim_net.Link::new(
            DELAY,
            JITTER,
            0,
            0,
            ((from * 977 + to).to_uint64() + 1UL) * 7919UL,
          ),
        ),
      )
      to = to + 1
    }
    nets.push(row)
    from = from + 1
  }
  // 房主视角的「最后一次听到某人的包」——用到达帧近似,够用且确定性。
  let heard : Array[Int32] = Array::make(PLAYERS, 0)
  let mut decided = false
  let mut declared_at = 0
  let mut mismatches = 0
  let mut f = 0
  while f < total {
    f = f + 1
    let mut p = 0
    while p < PLAYERS {
      ignore(sessions[p].advance(@scene.scripted_input(f, p)))
      p = p + 1
    }
    // 收包:先收,再判断掉线,最后发包,保证声明能随本帧的包发出去。
    let mut target = 0
    while target < PLAYERS {
      let mut src = 0
      while src < PLAYERS {
        if src != target {
          for payload in nets[src][target].recv(f) {
            mismatches = mismatches + @scene.apply(payload, sessions[target])
            if target == 0 {
              heard[src] = f
            }
          }
        }
        src = src + 1
      }
      target = target + 1
    }
    // 掉线判定只由房主做:其他人收到 `Active` 帧后照做,避免各判各的。
    if !decided && announce_drop && f - heard[GONE_PLAYER] > TIMEOUT {
      decided = true
      declared_at = f
      ignore(sessions[0].set_player_active(GONE_PLAYER, false))
    }
    let mut back = f - RESEND + 1
    if back < 1 {
      back = 1
    }
    let mut sender = 0
    while sender < PLAYERS {
      // 拔了网线的那位:本地还在跑,包一个也发不出去。
      if sender == GONE_PLAYER && f > GONE {
        ()
      } else {
        let payload = outbound(sessions[sender], sender, back, f, decided)
        let mut dst = 0
        while dst < PLAYERS {
          // 拔网线是双向的:既发不出去,也收不到。
          if dst != sender && !(dst == GONE_PLAYER && f > GONE) {
            nets[sender][dst].send(f, payload)
          }
          dst = dst + 1
        }
      }
      sender = sender + 1
    }
    if verbose && f % 60 == 0 {
      let mut line = "\{pad(f.to_string(), 6)}"
      let mut k = 0
      while k < PLAYERS {
        line = line + pad(sessions[k].confirmed().to_string(), 8)
        k = k + 1
      }
      line = line + pad(sessions[0].online_count().to_string(), 6)
      println(line)
    }
  }
  // 收尾:最后一轮的包还在路上,多收 64 帧让确认追上。
  let mut t = total
  while t < total + 64 {
    t = t + 1
    let mut target = 0
    while target < PLAYERS {
      let mut src = 0
      while src < PLAYERS {
        if src != target {
          for payload in nets[src][target].recv(t) {
            mismatches = mismatches + @scene.apply(payload, sessions[target])
          }
        }
        src = src + 1
      }
      target = target + 1
    }
  }
  let confirmed : Array[Int32] = []
  let mut rollbacks = 0L
  let mut replayed = 0L
  let mut k = 0
  while k < PLAYERS {
    confirmed.push(sessions[k].confirmed())
    // 3 号掉线后已经不在同一个世界里,只统计其余三人。
    if k != GONE_PLAYER {
      rollbacks = rollbacks + sessions[k].rollbacks().to_int64()
      replayed = replayed + sessions[k].replayed()
    }
    k = k + 1
  }
  {
    confirmed,
    rollbacks,
    replayed,
    first_divergence: scan_divergence(sessions, GONE_PLAYER),
    mismatches,
    declared_at,
  }
}

///|
fn main {
  println(
    "drop_out:\{PLAYERS} 人局,\{GONE_PLAYER} 号在第 \{GONE} 帧后停止发包,超时阈值 \{TIMEOUT} 帧",
  )
  println("")
  println("frame  confirm(p0..p3)              online")
  let stuck = run(false, TOTAL, true)
  println("")
  println("—— 不宣布掉线 ——")
  println(
    "  确认帧 = \{stuck.confirmed[0]}/\{stuck.confirmed[1]}/\{stuck.confirmed[2]}/\{stuck.confirmed[3]},其余三人一致 = \{stuck.first_divergence < 0},校验失配 = \{stuck.mismatches}",
  )
  println(
    "  回滚 = \{stuck.rollbacks},重放帧数 = \{stuck.replayed}(一直在追一个永远不会到的输入)",
  )
  println("")
  println("frame  confirm(p0..p3)              online")
  let live = run(true, TOTAL, true)
  println("")
  println("—— 房主在第 \{live.declared_at} 帧宣布掉线 ——")
  println(
    "  确认帧 = \{live.confirmed[0]}/\{live.confirmed[1]}/\{live.confirmed[2]}/\{live.confirmed[3]},其余三人一致 = \{live.first_divergence < 0},校验失配 = \{live.mismatches}",
  )
  println("  回滚 = \{live.rollbacks},重放帧数 = \{live.replayed}")
  println("")
  println(
    "结论:确认帧等的是「所有在线玩家」,不是「所有玩家」——",
  )
  println(
    "      掉线语义必须由上层给出(这里是一个超时阈值),本库只负责把它同步给全房。",
  )
}

///|
/// 右侧补空格,让表格在等宽字体里对齐。
fn pad(s : String, width : Int32) -> String {
  let mut out = s
  while out.length() < width {
    out = out + " "
  }
  out
}