///|
pub(all) struct PatchInfo {
  display_name : String
  additions : Int
  deletions : Int
  is_rename : Bool
  is_copy : Bool
  is_new : Bool
  is_delete : Bool
  old_name : String
  new_name : String
  old_mode : String
  new_mode : String
  similarity_index : Int
  dissimilarity_index : Int
}

///|
pub fn reverse_patch(patch : String) -> String {
  let lines : Array[String] = []
  for line_view in patch.split("\n") {
    let line = line_view.to_owned()
    if line.has_prefix("+") && !line.has_prefix("+++") {
      lines.push(
        "-" + String::unsafe_substring(line, start=1, end=line.length()),
      )
    } else if line.has_prefix("-") && !line.has_prefix("---") {
      lines.push(
        "+" + String::unsafe_substring(line, start=1, end=line.length()),
      )
    } else if line.has_prefix("--- a/") {
      lines.push(
        "+++ b/" + String::unsafe_substring(line, start=6, end=line.length()),
      )
    } else if line.has_prefix("+++ b/") {
      lines.push(
        "--- a/" + String::unsafe_substring(line, start=6, end=line.length()),
      )
    } else {
      lines.push(line)
    }
  }
  lines.join("\n")
}

///|
pub fn parse_patches(patch : String) -> Array[PatchInfo] {
  let patches : Array[PatchInfo] = []
  let lines_iter = patch.split("\n")
  let all_lines : Array[String] = []
  for lv in lines_iter {
    all_lines.push(lv.to_owned())
  }
  let mut i = 0
  while i < all_lines.length() {
    let line = all_lines[i]
    // Detect start of a diff (git format or non-git format)
    let is_git_diff = line.has_prefix("diff --git ")
    let is_nongit_diff = !is_git_diff && line.has_prefix("diff ")
    if is_git_diff || is_nongit_diff {
      let mut old_name = ""
      let mut new_name = ""
      let mut is_rename = false
      let mut is_copy = false
      let mut is_new = false
      let mut is_delete = false
      let mut old_mode = ""
      let mut new_mode = ""
      let mut similarity = 0
      let mut dissimilarity = 0
      let mut additions = 0
      let mut deletions = 0
      if is_git_diff {
        // Parse "diff --git a/OLD b/NEW"
        let rest = String::unsafe_substring(line, start=11, end=line.length())
        let (oname, nname) = parse_diff_git_header(rest)
        old_name = oname
        new_name = nname
      } else {
        // Non-git diff: "diff a/OLD b/NEW" or "diff -u a/file b/file" etc.
        // We'll get names from --- / +++ lines below
      }
      i += 1
      // Parse extended headers
      while i < all_lines.length() {
        let hline = all_lines[i]
        if hline.has_prefix("diff ") {
          break
        }
        if hline.has_prefix("similarity index ") {
          let pct_str = String::unsafe_substring(
            hline,
            start=17,
            end=hline.length(),
          )
          similarity = parse_pct(pct_str)
        } else if hline.has_prefix("dissimilarity index ") {
          let pct_str = String::unsafe_substring(
            hline,
            start=20,
            end=hline.length(),
          )
          dissimilarity = parse_pct(pct_str)
        } else if hline.has_prefix("rename from ") {
          is_rename = true
          old_name = String::unsafe_substring(
            hline,
            start=12,
            end=hline.length(),
          )
        } else if hline.has_prefix("rename to ") {
          is_rename = true
          new_name = String::unsafe_substring(
            hline,
            start=10,
            end=hline.length(),
          )
        } else if hline.has_prefix("copy from ") {
          is_copy = true
          old_name = String::unsafe_substring(
            hline,
            start=10,
            end=hline.length(),
          )
        } else if hline.has_prefix("copy to ") {
          is_copy = true
          new_name = String::unsafe_substring(
            hline,
            start=8,
            end=hline.length(),
          )
        } else if hline.has_prefix("new file mode ") {
          is_new = true
          new_mode = String::unsafe_substring(
            hline,
            start=14,
            end=hline.length(),
          )
        } else if hline.has_prefix("deleted file mode ") {
          is_delete = true
          old_mode = String::unsafe_substring(
            hline,
            start=18,
            end=hline.length(),
          )
        } else if hline.has_prefix("old mode ") {
          old_mode = String::unsafe_substring(
            hline,
            start=9,
            end=hline.length(),
          )
        } else if hline.has_prefix("new mode ") {
          new_mode = String::unsafe_substring(
            hline,
            start=9,
            end=hline.length(),
          )
        } else if hline.has_prefix("--- ") {
          // Extract old file name
          let fname = String::unsafe_substring(
            hline,
            start=4,
            end=hline.length(),
          )
          if fname == "/dev/null" {
            is_new = true
          } else if fname.has_prefix("a/") {
            if old_name == "" {
              old_name = String::unsafe_substring(
                fname,
                start=2,
                end=fname.length(),
              )
            }
          } else if old_name == "" {
            old_name = fname
          }
          i += 1
          // Expect +++ line next
          if i < all_lines.length() {
            let pline = all_lines[i]
            if pline.has_prefix("+++ ") {
              let pfname = String::unsafe_substring(
                pline,
                start=4,
                end=pline.length(),
              )
              if pfname == "/dev/null" {
                is_delete = true
              } else if pfname.has_prefix("b/") {
                if new_name == "" {
                  new_name = String::unsafe_substring(
                    pfname,
                    start=2,
                    end=pfname.length(),
                  )
                }
              } else if new_name == "" {
                new_name = pfname
              }
            }
          }
          i += 1
          // Count hunk lines
          while i < all_lines.length() {
            let hunkline = all_lines[i]
            if hunkline.has_prefix("diff ") {
              break
            }
            if hunkline.has_prefix("+") &&
              !hunkline.has_prefix("+++ ") &&
              hunkline != "+++" {
              additions += 1
            } else if hunkline.has_prefix("-") &&
              !hunkline.has_prefix("--- ") &&
              hunkline != "---" {
              deletions += 1
            }
            i += 1
          }
          continue
        } else if hline.has_prefix("@@") || hline.has_prefix("index ") {
          // skip index line, start counting on @@ lines via --- path
          i += 1
          continue
        } else {
          i += 1
          continue
        }
        i += 1
      }
      // For non-git diffs, if old_name is empty, we might not have parsed it
      if new_name == "" && old_name != "" {
        new_name = old_name
      }
      if old_name == "" && new_name != "" {
        old_name = new_name
      }
      // git apply.c show_stats: uses new_name if available, else old_name
      let display = if new_name != "" { new_name } else { old_name }
      patches.push({
        display_name: display,
        additions,
        deletions,
        is_rename,
        is_copy,
        is_new,
        is_delete,
        old_name,
        new_name,
        old_mode,
        new_mode,
        similarity_index: similarity,
        dissimilarity_index: dissimilarity,
      })
    } else {
      i += 1
    }
  }
  patches
}

///|
pub fn parse_diff_git_header(rest : String) -> (String, String) {
  // rest is like "a/foo b/bar" — but filenames can contain spaces
  // Strategy: try "a/" prefix, then find " b/" separator
  if rest.has_prefix("a/") {
    let search = rest
    // Find " b/" separator - try from various positions
    let mut pos = 2
    while pos < search.length() - 2 {
      if search[pos] == ' ' &&
        pos + 1 < search.length() &&
        search[pos + 1] == 'b' &&
        pos + 2 < search.length() &&
        search[pos + 2] == '/' {
        let old_name = String::unsafe_substring(search, start=2, end=pos)
        let new_name = String::unsafe_substring(
          search,
          start=pos + 3,
          end=search.length(),
        )
        return (old_name, new_name)
      }
      pos += 1
    }
  }
  ("", "")
}

///|
pub fn parse_pct(s : String) -> Int {
  // Parse "NN%" -> NN
  let mut n = 0
  for c in s {
    if c >= '0' && c <= '9' {
      n = n * 10 + (c.to_int() - '0'.to_int())
    }
  }
  n
}

///|
pub fn make_rename_display(old_name : String, new_name : String) -> String {
  // Find common prefix and suffix, then format as prefix{old => new}suffix
  let mut prefix_len = 0
  let mut last_slash = -1
  let min_len = if old_name.length() < new_name.length() {
    old_name.length()
  } else {
    new_name.length()
  }
  while prefix_len < min_len && old_name[prefix_len] == new_name[prefix_len] {
    if old_name[prefix_len] == '/' {
      last_slash = prefix_len
    }
    prefix_len += 1
  }
  // If not fully matching, back up to last slash boundary
  if prefix_len < old_name.length() || prefix_len < new_name.length() {
    prefix_len = last_slash + 1
  }
  // Find common suffix
  let old_rest_len = old_name.length() - prefix_len
  let new_rest_len = new_name.length() - prefix_len
  let min_rest = if old_rest_len < new_rest_len {
    old_rest_len
  } else {
    new_rest_len
  }
  let mut suffix_len = 0
  while suffix_len < min_rest {
    let oc = old_name[old_name.length() - 1 - suffix_len]
    let nc = new_name[new_name.length() - 1 - suffix_len]
    if oc != nc {
      break
    }
    suffix_len += 1
  }
  // Adjust suffix to start at a slash boundary (or start of rest)
  if suffix_len > 0 {
    // Find the first '/' from the start of the suffix region
    let old_suffix_start = old_name.length() - suffix_len
    let mut adjusted = suffix_len
    let mut found_slash = false
    let mut si = 0
    while si < suffix_len {
      if old_name[old_suffix_start + si] == '/' {
        adjusted = suffix_len - si
        found_slash = true
        break
      }
      si += 1
    }
    if found_slash {
      suffix_len = adjusted
    } else if suffix_len != old_rest_len && suffix_len != new_rest_len {
      suffix_len = 0
    }
  }
  let prefix = String::unsafe_substring(old_name, start=0, end=prefix_len)
  let old_mid = String::unsafe_substring(
    old_name,
    start=prefix_len,
    end=old_name.length() - suffix_len,
  )
  let new_mid = String::unsafe_substring(
    new_name,
    start=prefix_len,
    end=new_name.length() - suffix_len,
  )
  let suffix = String::unsafe_substring(
    old_name,
    start=old_name.length() - suffix_len,
    end=old_name.length(),
  )
  if prefix == "" && suffix == "" {
    "\{old_mid} => \{new_mid}"
  } else {
    "\{prefix}{\{old_mid} => \{new_mid}}\{suffix}"
  }
}