///|
/// 语义 Diff:比较两个文档状态,输出人可读 + 机器可解析的变更描述。
///
/// Agent 在执行操作后调用 `diff_docs(before, after)` 获得:
/// - added:新增了哪些节点
/// - removed:删除了哪些节点
/// - moved:位置变化的节点
/// - resized:尺寸变化的节点
/// - restyled:样式变化的节点
/// - relayout:布局参数变化
/// Agent 把这个 diff 作为"我做了什么"的自省报告。
pub(all) struct DocDiff {
  added : Array[String]
  removed : Array[String]
  moved : Array[DiffEntry]
  resized : Array[DiffEntry]
  restyled : Array[DiffEntry]
  relayout : Array[DiffEntry]
}

///|
pub(all) struct DiffEntry {
  node_id : String
  before : String
  after : String
}

///|
/// 比较两个文档(通常 before = 操作前快照,after = 操作后)
pub fn diff_docs(before : Document, after : Document) -> DocDiff {
  let diff : DocDiff = {
    added: [],
    removed: [],
    moved: [],
    resized: [],
    restyled: [],
    relayout: [],
  }
  // 新增
  for id in after.nodes.keys() {
    if !before.has(id) {
      diff.added.push(id)
    }
  }
  // 删除
  for id in before.nodes.keys() {
    if !after.has(id) {
      diff.removed.push(id)
    }
  }
  // 变更
  let la = solve(after)
  let lb = solve(before)
  for id in after.nodes.keys() {
    if before.has(id) {
      match
        (
          before.nodes.get(id),
          after.nodes.get(id),
          lb.rect_of(id),
          la.rect_of(id),
        ) {
        (Some(nb), Some(na), Some(rb), Some(ra)) => {
          // 位置变化
          if rb.x != ra.x || rb.y != ra.y {
            diff.moved.push({
              node_id: id,
              before: "(\{rb.x.to_int()}, \{rb.y.to_int()})",
              after: "(\{ra.x.to_int()}, \{ra.y.to_int()})",
            })
          }
          // 尺寸变化
          if rb.width != ra.width || rb.height != ra.height {
            diff.resized.push({
              node_id: id,
              before: "\{rb.width.to_int()}×\{rb.height.to_int()}",
              after: "\{ra.width.to_int()}×\{ra.height.to_int()}",
            })
          }
          // 样式变化
          let style_changes : Array[String] = []
          if nb.style.fill != na.style.fill {
            style_changes.push("fill: \{nb.style.fill}→\{na.style.fill}")
          }
          if nb.style.corner_radius != na.style.corner_radius {
            style_changes.push(
              "radius: \{nb.style.corner_radius}→\{na.style.corner_radius}",
            )
          }
          if nb.style.font_size != na.style.font_size {
            style_changes.push(
              "font: \{nb.style.font_size}→\{na.style.font_size}",
            )
          }
          if nb.text != na.text {
            style_changes.push("text: \"\{nb.text}\"→\"\{na.text}\"")
          }
          if style_changes.length() > 0 {
            diff.restyled.push({
              node_id: id,
              before: "",
              after: jstr7(style_changes, "; "),
            })
          }
          // 布局变化
          let lb_layout = nb.layout
          let la_layout = na.layout
          let layout_changed = match (lb_layout, la_layout) {
            (None, None) => false
            (Some(b), Some(a)) => dir_neq(b.dir, a.dir) || b.gap != a.gap
            _ => true
          }
          if layout_changed {
            let before_desc = match lb_layout {
              Some(l) => "\{dir_name(l.dir)} gap=\{l.gap}"
              None => "absolute"
            }
            let after_desc = match la_layout {
              Some(l) => "\{dir_name(l.dir)} gap=\{l.gap}"
              None => "absolute"
            }
            diff.relayout.push({
              node_id: id,
              before: before_desc,
              after: after_desc,
            })
          }
        }
        _ => ()
      }
    }
  }
  diff
}

///|
fn dir_name(d : StackDir) -> String {
  match d {
    Vertical => "vertical"
    Horizontal => "horizontal"
  }
}

///|
fn jstr7(parts : Array[String], sep : String) -> String {
  let sb = StringBuilder::new()
  for i in 0.. 0 {
      sb.write_string(sep)
    }
    sb.write_string(parts[i])
  }
  sb.to_string()
}

///|
/// Diff 的 JSON 序列化(Agent 审阅用)
pub fn DocDiff::to_json(self : DocDiff) -> String {
  let parts : Array[String] = []
  // added
  if self.added.length() > 0 {
    let adds : Array[String] = []
    for id in self.added.iter() {
      adds.push("\"\{json_escape(id)}\"")
    }
    parts.push("\"added\":[" + jstr7(adds, ",") + "]")
  }
  // removed
  if self.removed.length() > 0 {
    let rems : Array[String] = []
    for id in self.removed.iter() {
      rems.push("\"\{json_escape(id)}\"")
    }
    parts.push("\"removed\":[" + jstr7(rems, ",") + "]")
  }
  // moved
  if self.moved.length() > 0 {
    let ms : Array[String] = []
    for e in self.moved.iter() {
      ms.push(
        "{\"id\":\"\{e.node_id}\",\"from\":\"\{e.before}\",\"to\":\"\{e.after}\"}",
      )
    }
    parts.push("\"moved\":[" + jstr7(ms, ",") + "]")
  }
  // resized
  if self.resized.length() > 0 {
    let rs : Array[String] = []
    for e in self.resized.iter() {
      rs.push(
        "{\"id\":\"\{e.node_id}\",\"from\":\"\{e.before}\",\"to\":\"\{e.after}\"}",
      )
    }
    parts.push("\"resized\":[" + jstr7(rs, ",") + "]")
  }
  // restyled
  if self.restyled.length() > 0 {
    let ss : Array[String] = []
    for e in self.restyled.iter() {
      ss.push(
        "{\"id\":\"\{json_escape(e.node_id)}\",\"changes\":\"\{json_escape(e.after)}\"}",
      )
    }
    parts.push("\"restyled\":[" + jstr7(ss, ",") + "]")
  }
  // relayout
  if self.relayout.length() > 0 {
    let ls : Array[String] = []
    for e in self.relayout.iter() {
      ls.push(
        "{\"id\":\"\{json_escape(e.node_id)}\",\"from\":\"\{json_escape(e.before)}\",\"to\":\"\{json_escape(e.after)}\"}",
      )
    }
    parts.push("\"relayout\":[" + jstr7(ls, ",") + "]")
  }
  if parts.length() == 0 {
    return "{\"unchanged\":true}"
  }
  let j = jstr7(parts, ",")
  "{\{j}}"
}

///|
/// 人可读摘要(一行)
pub fn DocDiff::summary(self : DocDiff) -> String {
  let parts : Array[String] = []
  if self.added.length() > 0 {
    parts.push("+\{self.added.length()}")
  }
  if self.removed.length() > 0 {
    parts.push("-\{self.removed.length()}")
  }
  if self.moved.length() > 0 {
    parts.push("↕\{self.moved.length()}")
  }
  if self.resized.length() > 0 {
    parts.push("⇲\{self.resized.length()}")
  }
  if self.restyled.length() > 0 {
    parts.push("◐\{self.restyled.length()}")
  }
  if self.relayout.length() > 0 {
    parts.push("☰\{self.relayout.length()}")
  }
  if parts.length() == 0 {
    "no changes"
  } else {
    jstr7(parts, " ")
  }
}

///|
fn dir_neq(a : StackDir, b : StackDir) -> Bool {
  match (a, b) {
    (Vertical, Horizontal) => true
    (Horizontal, Vertical) => true
    _ => false
  }
}