///|
/// 调用树的构建与表示。
///
/// 聚合刻意分两步:
///
///   1. **构建**:用可变、无序的 `Builder` 逐条样本插入,同父同名节点合并并累加权重;
///   2. **物化**:把 `Builder` 转成不可变、**已排序**的 `Node`。
///
/// 第二步不是多余的:`Map` 的遍历顺序不稳定,如果把 `Map` 直接暴露给渲染层,
/// 输出会随运行抖动、快照测试也会随机失败。排序键固定为
/// 「value 降序,同值按 name 升序」,保证同一份输入永远得到同一棵树。

///|
/// 调用树节点。`children` 已排序,可直接用于布局。
pub struct Node {
  name : String
  /// 累计权重,包含所有子节点
  value : Double
  /// 子节点:按 value 降序,同值按 name 升序
  children : Array[Node]
  /// 差异模式下的变化量(本剖面相对基线的差值);普通模式下为 `None`
  delta : Double?
} derive(Eq)

///|
/// 显式声明 `Eq` 的两个方法可被当作常规方法调用。
///
/// 新版编译器(moonc 0.10.14 起)不再自动把 `impl Eq` 的方法提升为常规方法,
/// 不声明就会报 `implicit_impl_as_method` 废弃警告。这条声明把该提升写明,
/// 行为与过去一致;配合 CI 的「警告即失败」,也避免未来默认行为变更后突然失效。
pub extend Node with Eq::{not_equal, equal}

///|
/// 聚合结果。
pub struct CallTree {
  /// 根节点,已排序。格式允许出现多个根(如多线程采样),真实数据通常只有一个
  roots : Array[Node]
  /// 所有样本权重之和
  total : Double
} derive(Eq)

///|
/// 显式声明 `Eq` 的方法可被当作常规方法调用。
///
/// 新版编译器不再自动完成这个提升,不声明会报 `implicit_impl_as_method`。
pub extend CallTree with Eq::{not_equal, equal}

///|
/// 构建期的可变节点。
///
/// 只在聚合过程中存在:用 `Map` 便于「查找或创建」,顺序统一留到物化阶段处理。
priv struct Builder {
  name : String
  mut value : Double
  children : Map[String, Builder]
}

///|
fn Builder::new(name : String) -> Builder {
  { name, value: 0.0, children: Map([]), }
}

///|
/// 深度超限时,用来收纳「更深的帧」的合成节点名。
pub fn deeper_placeholder() -> String {
  "(deeper)"
}

///|
/// 取出子节点,不存在则创建。用 `Map` 便于按名字「查找或创建」。
fn child_of(node : Builder, name : String) -> Builder {
  match node.children.get(name) {
    Some(existing) => existing
    None => {
      let created = Builder::new(name)
      node.children.set(name, created)
      created
    }
  }
}

///|
/// 把一条样本的调用链插入树中,逐层「查找或创建」子节点并累加权重。
///
/// `index` 从 `frames` 的第几层开始插入;`max_depth > 0` 时限制保留的帧数。
///
/// **深度超限时不是丢弃,而是合并**:把 `frames[max_depth..]` 整段收纳进一个
/// 名为 `(deeper)` 的合成节点。两种做法的区别在归因:
///
///   - 丢弃:最后一个可见帧看起来「自己消耗」了下面所有的时间,是误导;
///   - 合并:多出一个显式的 `(deeper)` 子节点,诚实标出「下面还有更多」。
///
/// 两者都不破坏不变量 `自身耗时 ≥ 0`;合并只是不掩盖信息。
fn insert_path(
  node : Builder,
  frames : Array[String],
  index : Int,
  weight : Double,
  max_depth : Int,
) -> Unit {
  node.value = node.value + weight
  if index >= frames.length() {
    return
  }
  if max_depth > 0 && index >= max_depth {
    // 已经越过深度上限:把这一整段链的价值累加进合成节点,不再逐帧展开
    let deeper = child_of(node, deeper_placeholder())
    deeper.value = deeper.value + weight
    return
  }
  let child = child_of(node, frames[index])
  insert_path(child, frames, index + 1, weight, max_depth)
}

///|
/// 排序规则:value 降序;同值时按 name 升序,消除排序的不确定性。
fn compare_nodes(a : Node, b : Node) -> Int {
  if a.value > b.value {
    -1
  } else if a.value < b.value {
    1
  } else {
    String::compare(a.name, b.name)
  }
}

///|
/// 把构建期节点物化成不可变、已排序的 `Node`。
fn Builder::materialize(self : Builder) -> Node {
  let children : Array[Node] = []
  for pair in self.children {
    children.push(pair.1.materialize())
  }
  children.sort_by(compare_nodes)
  { name: self.name, value: self.value, children, delta: None, }
}

///|
/// 按名字给一层子节点建索引,用于两份剖面的对齐。
fn index_by_name(nodes : Array[Node]) -> Map[String, Node] {
  let index : Map[String, Node] = Map([])
  for node in nodes {
    index.set(node.name, node)
  }
  index
}

///|
/// 递归标注变化量:以 `baseline` 为基线,给 `nodes` 中每个节点算出差值。
fn annotate(nodes : Array[Node], baseline : Array[Node]) -> Array[Node] {
  let index = index_by_name(baseline)
  let out : Array[Node] = []
  for node in nodes {
    let matched = index.get(node.name)
    let base_value = match matched {
      Some(other) => other.value
      None => 0.0
    }
    let base_children = match matched {
      Some(other) => other.children
      None => []
    }
    out.push({
      name: node.name,
      value: node.value,
      delta: Some(node.value - base_value),
      children: annotate(node.children, base_children),
    })
  }
  out
}

///|
/// 把两份剖面叠加,给 `tree` 的每个节点标注相对 `baseline` 的变化量。
///
/// **宽度仍按 `tree`(当前剖面)计算**,因此只在基线里出现的帧不会显示——
/// 这与参考实现 `difffolded.pl` 的取舍一致(它的建议是交换两份文件再生成一张,
/// 从另一个方向看消失的帧)。
pub fn diff_against(tree : CallTree, baseline : CallTree) -> CallTree {
  { roots: annotate(tree.roots, baseline.roots), total: tree.total, }
}

///|
/// 树中最大的变化量绝对值,用于把差异配色归一化到同一量级。
///
/// 不做归一化的话,只要有一处剧变,其它所有变化在颜色上就全糊成一片。
pub fn CallTree::max_delta(self : CallTree) -> Double {
  let mut largest = 0.0
  for root in self.roots {
    let magnitude = max_delta_of(root)
    if magnitude > largest {
      largest = magnitude
    }
  }
  largest
}

///|
fn max_delta_of(node : Node) -> Double {
  let mut largest = match node.delta {
    Some(value) => if value < 0.0 { -value } else { value }
    None => 0.0
  }
  for child in node.children {
    let magnitude = max_delta_of(child)
    if magnitude > largest {
      largest = magnitude
    }
  }
  largest
}

///|
/// 把样本聚合成调用树。
///
/// `max_depth` 限制每个根节点下保留的帧数:超出的部分会被合并进
/// `(deeper)` 合成节点(见 `insert_path`)。`max_depth <= 0` 或不传表示不限制。
///
/// 真实采样数据里递归栈可达数千层(实测最深 7254 层),按每层一行渲染会得到
/// 十几万像素高的图,因此渲染前必须限深——这是 CLI 默认行为。
pub fn aggregate(samples : Array[Sample], max_depth? : Int) -> CallTree {
  let limit = match max_depth {
    Some(depth) => depth
    None => 0
  }
  // 虚拟入口:只作为插入起点,不进入结果,避免把「取或建」逻辑写两遍
  let entry = Builder::new("")
  let mut total = 0.0
  for sample in samples {
    insert_path(entry, sample.frames, 0, sample.weight, limit)
    total = total + sample.weight
  }
  let roots : Array[Node] = []
  for pair in entry.children {
    roots.push(pair.1.materialize())
  }
  roots.sort_by(compare_nodes)
  { roots, total, }
}

///|
/// 自身耗时:累计权重减去所有子节点累计权重之和。
///
/// 采样栈可能停在中间(例如 `main;compute 100` 表示这 100 的权重归 compute
/// 自己),此时该节点没有对应子节点,差额即它自身消耗的时间。
pub fn Node::self_time(self : Node) -> Double {
  let mut children_total = 0.0
  for child in self.children {
    children_total = children_total + child.value
  }
  self.value - children_total
}

///|
/// 按名字查找直接子节点。
pub fn Node::child(self : Node, name : String) -> Node? {
  for child in self.children {
    if child.name == name {
      return Some(child)
    }
  }
  None
}

///|
/// 自身耗时占给定总量的比例,用于文本报告与图上的百分比标注。
pub fn Node::self_ratio(self : Node, total : Double) -> Double {
  if total <= 0.0 {
    0.0
  } else {
    self.self_time() / total
  }
}