// unified diff 的数据模型。
//
// 这里只有纯数据和纯函数,不碰文件、不碰网络、不碰大模型,
// 所以整套结构可以脱离 LLM 单独做断言测试。

///|
/// 一行源码在 diff 里扮演的角色。
enum LineKind {
  Context
  Added
  Removed
} derive(Eq, Debug)

///|
/// diff 中的单行。
///
/// 行号在对应一侧不存在时为 `None`:新增行没有「改动前」的行号,
/// 删除行没有「改动后」的行号,上下文行两侧都有。
struct Line {
  kind : LineKind
  /// 去掉行首 `-` / `+` / 空格之后的原始内容
  content : String
  old_lineno : Int?
  new_lineno : Int?
} derive(Eq, Debug)

///|
/// 一个 `@@` 块。
struct Hunk {
  old_start : Int
  old_count : Int
  new_start : Int
  new_count : Int
  /// `@@ ... @@` 后面的函数名提示,git 自动生成,可能为空
  heading : String
  lines : Array[Line]
  /// 标记 `\ No newline at end of file` 落在改动前的文件上
  no_newline_old : Bool
  /// 标记 `\ No newline at end of file` 落在改动后的文件上
  no_newline_new : Bool
} derive(Eq, Debug)

///|
/// 这个文件整体发生了什么。
enum FileStatus {
  Modified
  NewFile
  Deleted
  Renamed
  Copied
  /// 只改了权限,内容一行没动
  ModeOnly
} derive(Eq, Debug)

///|
/// 一个文件的完整改动记录。
struct FileDiff {
  /// 新增文件时为 `/dev/null`
  old_path : String
  /// 删除文件时为 `/dev/null`
  new_path : String
  status : FileStatus
  binary : Bool
  /// 形如 `100644`;diff 里没写就是 `None`
  old_mode : String?
  new_mode : String?
  hunks : Array[Hunk]
} derive(Eq, Debug)

///|
/// 一份完整的 diff,也就是 `git diff` 一次的输出。
struct Diff {
  files : Array[FileDiff]
} derive(Eq, Debug)

///|
// `derive` 出来的 trait 方法不会自动变成点号方法,要显式提升一次,
// 调用方才能写 `a == b`。
pub extend LineKind with Eq::{equal, not_equal}

///|
pub extend Line with Eq::{equal, not_equal}

///|
pub extend Hunk with Eq::{equal, not_equal}

///|
pub extend FileStatus with Eq::{equal, not_equal}

///|
pub extend FileDiff with Eq::{equal, not_equal}

///|
pub extend Diff with Eq::{equal, not_equal}

///|
pub extend LineKind with Debug::{to_repr}

///|
pub extend Line with Debug::{to_repr}

///|
pub extend Hunk with Debug::{to_repr}

///|
pub extend FileStatus with Debug::{to_repr}

///|
pub extend FileDiff with Debug::{to_repr}

///|
pub extend Diff with Debug::{to_repr}

///|
/// 是不是改动行(上下文行不算)。
pub fn Line::is_change(self : Line) -> Bool {
  self.kind != Context
}

///|
pub fn Hunk::added(self : Hunk) -> Array[Line] {
  self.lines.filter(fn(l) { l.kind == Added })
}

///|
pub fn Hunk::removed(self : Hunk) -> Array[Line] {
  self.lines.filter(fn(l) { l.kind == Removed })
}

///|
/// 只看真正改动的行,跳过上下文
pub fn Hunk::changed(self : Hunk) -> Array[Line] {
  self.lines.filter(fn(l) { l.kind != Context })
}

///|
pub fn Hunk::added_count(self : Hunk) -> Int {
  self.added().length()
}

///|
pub fn Hunk::removed_count(self : Hunk) -> Int {
  self.removed().length()
}

///|
/// 重建「改动前」的代码片段:上下文 + 删除行。
pub fn Hunk::old_side_text(self : Hunk) -> String {
  self.side_text(Added)
}

///|
/// 重建「改动后」的代码片段:上下文 + 新增行。
/// 喂给大模型看的应该就是这段。
pub fn Hunk::new_side_text(self : Hunk) -> String {
  self.side_text(Removed)
}

///|
fn Hunk::side_text(self : Hunk, exclude : LineKind) -> String {
  let buf : Array[String] = []
  for l in self.lines {
    if l.kind != exclude {
      buf.push(l.content)
    }
  }
  buf.join("\n")
}

///|
/// 展示用的路径:优先给改动后的,被删除的文件退回改动前的。
pub fn FileDiff::display_path(self : FileDiff) -> String {
  if self.new_path == "/dev/null" {
    self.old_path
  } else {
    self.new_path
  }
}

///|
pub fn FileDiff::changed_line_count(self : FileDiff) -> Int {
  let mut n = 0
  for h in self.hunks {
    n = n + h.changed().length()
  }
  n
}

///|
pub fn FileDiff::added_count(self : FileDiff) -> Int {
  let mut n = 0
  for h in self.hunks {
    n = n + h.added_count()
  }
  n
}

///|
pub fn FileDiff::removed_count(self : FileDiff) -> Int {
  let mut n = 0
  for h in self.hunks {
    n = n + h.removed_count()
  }
  n
}

///|
pub fn Diff::empty() -> Diff {
  { files: [], }
}

///|
pub fn Diff::is_empty(self : Diff) -> Bool {
  self.files.is_empty()
}

///|
pub fn Diff::file_count(self : Diff) -> Int {
  self.files.length()
}

///|
pub fn Diff::hunk_count(self : Diff) -> Int {
  let mut n = 0
  for f in self.files {
    n = n + f.hunks.length()
  }
  n
}

///|
pub fn Diff::added_count(self : Diff) -> Int {
  let mut n = 0
  for f in self.files {
    for h in f.hunks {
      n = n + h.added_count()
    }
  }
  n
}

///|
pub fn Diff::removed_count(self : Diff) -> Int {
  let mut n = 0
  for f in self.files {
    for h in f.hunks {
      n = n + h.removed_count()
    }
  }
  n
}

///|
/// 这次改动碰到了哪些文件
pub fn Diff::touched_paths(self : Diff) -> Array[String] {
  let out : Array[String] = []
  for f in self.files {
    out.push(f.display_path())
  }
  out
}