// 规范文本输出(不用 `Json::stringify`:Map 迭代序未定义)。
//
// 目的:报告要能被冻结、被 diff、被当成指纹。行尾统一 `\n`。
// 与 v1 的差别:多了计数器身份(谁算的、精不精确)、窗口模型三行,
// 以及 `[truncated]` 一节(截短了多少、截的哪一头)。
// 与 v2 的差别:多了 `[pools]` 一节(配额、需求、实际占用、借入借出),
// `[truncated]` 行尾多一列 `omitted=`(被省略量;`Compress` 的标记里也是这个数)。

///|
fn push_str(out : Array[Char], s : String) -> Unit {
  for c in s.to_array() {
    out.push(c)
  }
}

///|
fn push_i64(out : Array[Char], n : Int64) -> Unit {
  push_str(out, n.to_string())
}

///|
fn push_bool(out : Array[Char], b : Bool) -> Unit {
  push_str(out, if b { "true" } else { "false" })
}

///|
fn strategy_name(s : TruncateStrategy) -> String {
  match s {
    NoTruncate => "none"
    KeepPrefix => "prefix"
    KeepSuffix => "suffix"
    Compress => "compress"
  }
}

///|
/// 把计划渲染成规范文本。冻结为格式 `tally-plan v3`。
///
/// 两条格式约束(不新增转义,保持行结构可被空格切分):
/// * `counter` 的名字、段 `role` 与池名 **不得含空格或换行**——它们直接进行内;
/// * `overflow` 为真时 `[dropped]` 末尾有一行哨兵:`-1 pinned *overflow*  <原因>`,
///   它不是某个段,按段对账(求和、计数)时要按 `index == -1` 跳过。
///
/// `[pools]` 行格式:`<池名> <配额> <需求> <占用> <借入> <借出>`,按声明顺序。
pub fn Plan::render(self : Plan) -> String {
  let out : Array[Char] = []
  push_str(out, "tally-plan v3\n")
  push_str(out, "counter ")
  push_str(out, self.counter)
  push_str(out, " exact=")
  push_bool(out, self.exact)
  push_str(out, "\nwindow ")
  push_i64(out, self.window)
  push_str(out, "\nreserve ")
  push_i64(out, self.reserve)
  push_str(out, "\ninput_limit ")
  push_i64(out, self.limit)
  push_str(out, "\nused ")
  push_i64(out, self.tokens_used)
  push_str(out, "\npinned ")
  push_i64(out, self.pinned_tokens)
  push_str(out, "\noverflow ")
  push_bool(out, self.overflow)
  push_str(out, "\n[pools]\n")
  for p in self.pools {
    push_str(out, p.name)
    out.push(' ')
    push_i64(out, p.quota)
    out.push(' ')
    push_i64(out, p.demand)
    out.push(' ')
    push_i64(out, p.used)
    out.push(' ')
    push_i64(out, p.borrowed)
    out.push(' ')
    push_i64(out, p.lent)
    out.push('\n')
  }
  push_str(out, "[kept]\n")
  for k in self.kept {
    push_str(out, k.index.to_string())
    out.push(' ')
    push_str(out, k.priority.name())
    out.push(' ')
    push_str(out, k.role)
    out.push(' ')
    push_i64(out, k.tokens)
    out.push('\n')
  }
  push_str(out, "[truncated]\n")
  for t in self.truncated {
    push_str(out, t.index.to_string())
    out.push(' ')
    push_str(out, t.priority.name())
    out.push(' ')
    push_str(out, t.role)
    out.push(' ')
    push_i64(out, t.original_tokens)
    push_str(out, "->")
    push_i64(out, t.tokens)
    out.push(' ')
    push_str(out, strategy_name(t.strategy))
    push_str(out, " omitted=")
    push_i64(out, t.omitted)
    out.push('\n')
  }
  push_str(out, "[dropped]\n")
  for d in self.dropped {
    push_str(out, d.index.to_string())
    out.push(' ')
    push_str(out, d.priority.name())
    out.push(' ')
    push_str(out, d.role)
    out.push(' ')
    push_i64(out, d.tokens)
    out.push(' ')
    push_str(out, d.reason)
    out.push('\n')
  }
  String::from_array(out)
}