///| Pure parsing functions for git fast-import stream format.

///|
pub(all) struct FastImportDataResult {
  content : Bytes
  next_line : Int
}

///|
/// Parse a "data" command from pre-split lines starting at `start`.
/// Supports both byte-count mode ("data ") and delimited mode ("data < FastImportDataResult {
  if start >= lines.length() {
    return { content: Bytes::new(0), next_line: start }
  }
  let line = lines[start]
  if !line.has_prefix("data ") {
    return { content: Bytes::new(0), next_line: start + 1 }
  }
  let rest = String::unsafe_substring(line, start=5, end=line.length())
  if rest.has_prefix("<<") {
    // Delimited form
    let delim = String::unsafe_substring(rest, start=2, end=rest.length())
    let buf : Array[Byte] = []
    let mut idx = start + 1
    while idx < lines.length() {
      let l = lines[idx]
      if l == delim {
        idx += 1
        break
      }
      if buf.length() > 0 {
        buf.push(b'\n')
      }
      for b in @utf8.encode(l) {
        buf.push(b)
      }
      idx += 1
    }
    { content: fast_import_bytes_from_array(buf), next_line: idx }
  } else {
    // Exact byte count form
    let count = @string.parse_int(rest) catch { _ => 0 }
    if count == 0 {
      return { content: Bytes::new(0), next_line: start + 1 }
    }
    let buf : Array[Byte] = []
    let mut remaining = count
    let mut idx = start + 1
    while remaining > 0 && idx < lines.length() {
      let l = lines[idx]
      let line_bytes = fast_import_parse_string_byte_count(l)
      if buf.length() > 0 {
        if remaining > 0 {
          buf.push(b'\n')
          remaining -= 1
        }
      }
      let encoded = @utf8.encode(l)
      if line_bytes <= remaining {
        for b in encoded {
          buf.push(b)
        }
        remaining -= line_bytes
        idx += 1
      } else {
        let mut taken = 0
        for i in 0..= remaining {
            break
          }
          buf.push(encoded[i])
          taken += 1
        }
        remaining = 0
        idx += 1
      }
    }
    // After data, there may be a trailing LF (blank line) — consume it
    if idx < lines.length() && lines[idx].length() == 0 {
      idx += 1
    }
    { content: fast_import_bytes_from_array(buf), next_line: idx }
  }
}

///|
/// Count UTF-8 encoded byte length of a string.
pub fn fast_import_parse_string_byte_count(s : String) -> Int {
  @utf8.encode(s).length()
}

///|
/// Parsed identity from an author/committer/tagger line.
pub(all) struct FastImportIdent {
  ident : String // "Name "
  timestamp : Int64
  tz : String
}

///|
/// Parse an author/committer line value (after stripping the "author "/"committer " prefix).
/// Input: "Name  timestamp tz"
/// Returns None if the line cannot be parsed.
pub fn fast_import_parse_ident(line : String) -> FastImportIdent? {
  let chars : Array[Char] = []
  for c in line {
    chars.push(c)
  }
  let mut gt_pos = -1
  for i = chars.length() - 1; i >= 0; i = i - 1 {
    if chars[i] == '>' {
      gt_pos = i
      break
    }
  }
  if gt_pos < 0 {
    return None
  }
  let ident_buf = StringBuilder::new()
  for i in 0..<=gt_pos {
    ident_buf.write_char(chars[i])
  }
  let ident = ident_buf.to_string()
  let rest_buf = StringBuilder::new()
  for i in (gt_pos + 1).. {
      let ts_str = String::unsafe_substring(rest, start=0, end=sp)
      let tz = String::unsafe_substring(rest, start=sp + 1, end=rest.length())
      let ts = @string.parse_int64(ts_str) catch { _ => 0L }
      Some({ ident, timestamp: ts, tz })
    }
    None => {
      let ts = @string.parse_int64(rest) catch { _ => 0L }
      Some({ ident, timestamp: ts, tz: "+0000" })
    }
  }
}

///|
/// File operation types parsed from fast-import M/D/deleteall/R/C lines.
pub(all) enum FastImportFileOp {
  Modify(mode~ : String, dataref~ : String, path~ : String)
  Delete(path~ : String)
  DeleteAll
  Rename(old_path~ : String, new_path~ : String)
  Copy(old_path~ : String, new_path~ : String)
} derive(Eq, Debug)

///|
fn fast_import_show_string(value : String) -> String {
  let buf = StringBuilder::new()
  buf.write_char('"')
  for c in value {
    if c == '"' {
      buf.write_string("\\\"")
    } else if c == '\\' {
      buf.write_string("\\\\")
    } else if c == '\n' {
      buf.write_string("\\n")
    } else if c == '\r' {
      buf.write_string("\\r")
    } else if c == '\t' {
      buf.write_string("\\t")
    } else {
      buf.write_char(c)
    }
  }
  buf.write_char('"')
  buf.to_string()
}

///|
pub impl Show for FastImportFileOp with fn output(self, logger) {
  match self {
    Modify(mode~, dataref~, path~) =>
      logger.write_string(
        "Modify(mode=" +
        fast_import_show_string(mode) +
        ", dataref=" +
        fast_import_show_string(dataref) +
        ", path=" +
        fast_import_show_string(path) +
        ")",
      )
    Delete(path~) =>
      logger.write_string("Delete(path=" + fast_import_show_string(path) + ")")
    DeleteAll => logger.write_string("DeleteAll")
    Rename(old_path~, new_path~) =>
      logger.write_string(
        "Rename(old_path=" +
        fast_import_show_string(old_path) +
        ", new_path=" +
        fast_import_show_string(new_path) +
        ")",
      )
    Copy(old_path~, new_path~) =>
      logger.write_string(
        "Copy(old_path=" +
        fast_import_show_string(old_path) +
        ", new_path=" +
        fast_import_show_string(new_path) +
        ")",
      )
  }
}

///|
/// Parse a single file operation line from a fast-import stream.
/// Returns None if the line is not a recognized file operation.
pub fn fast_import_parse_file_op(line : String) -> FastImportFileOp? {
  if line == "deleteall" {
    return Some(FastImportFileOp::DeleteAll)
  }
  if line.has_prefix("D ") {
    let path = String::unsafe_substring(line, start=2, end=line.length())
    return Some(
      FastImportFileOp::Delete(path=fast_import_parse_unquote_path(path)),
    )
  }
  if line.has_prefix("M ") {
    let rest = String::unsafe_substring(line, start=2, end=line.length())
    match rest.find(" ") {
      Some(sp1) => {
        let mode = String::unsafe_substring(rest, start=0, end=sp1)
        let after_mode = String::unsafe_substring(
          rest,
          start=sp1 + 1,
          end=rest.length(),
        )
        match after_mode.find(" ") {
          Some(sp2) => {
            let dataref = String::unsafe_substring(after_mode, start=0, end=sp2)
            let path = fast_import_parse_unquote_path(
              String::unsafe_substring(
                after_mode,
                start=sp2 + 1,
                end=after_mode.length(),
              ),
            )
            Some(FastImportFileOp::Modify(mode~, dataref~, path~))
          }
          None => None
        }
      }
      None => None
    }
  } else if line.has_prefix("R ") {
    let rest = String::unsafe_substring(line, start=2, end=line.length())
    match fast_import_parse_split_two_paths(rest) {
      Some((old_path, new_path)) =>
        Some(FastImportFileOp::Rename(old_path~, new_path~))
      None => None
    }
  } else if line.has_prefix("C ") {
    let rest = String::unsafe_substring(line, start=2, end=line.length())
    match fast_import_parse_split_two_paths(rest) {
      Some((old_path, new_path)) =>
        Some(FastImportFileOp::Copy(old_path~, new_path~))
      None => None
    }
  } else {
    None
  }
}

///|
/// Resolve a dataref string: ":N" returns the mark number, hex returns hex.
/// Returns (mark_number, hex_string) — one will be meaningful depending on format.
pub fn fast_import_parse_dataref(ref_str : String) -> (Int, String) {
  if ref_str.has_prefix(":") {
    let mark_str = String::unsafe_substring(
      ref_str,
      start=1,
      end=ref_str.length(),
    )
    let mark_num = @string.parse_int(mark_str) catch { _ => 0 }
    (mark_num, "")
  } else {
    (0, ref_str)
  }
}

///|
/// Parse a marks file content into an array of (mark_number, hex_oid) pairs.
pub fn fast_import_parse_marks(content : String) -> Array[(Int, String)] {
  let result : Array[(Int, String)] = []
  for line_view in content.split("\n") {
    let line = line_view.to_owned()
    if line.length() == 0 {
      continue
    }
    if line.has_prefix(":") {
      match line.find(" ") {
        Some(sp) => {
          let mark_str = String::unsafe_substring(line, start=1, end=sp)
          let hex = String::unsafe_substring(
            line,
            start=sp + 1,
            end=line.length(),
          )
          let mark_num = @string.parse_int(mark_str) catch { _ => 0 }
          if mark_num > 0 && hex.length() == 40 {
            result.push((mark_num, hex))
          }
        }
        None => ()
      }
    }
  }
  result
}

///|
/// Trim whitespace from a string.
pub fn fast_import_parse_trim(s : String) -> String {
  let mut start = 0
  let mut end = s.length()
  while start < end {
    let c = s.unsafe_get(start)
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      start += 1
    } else {
      break
    }
  }
  while end > start {
    let c = s.unsafe_get(end - 1)
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      end -= 1
    } else {
      break
    }
  }
  if start == 0 && end == s.length() {
    s
  } else {
    String::unsafe_substring(s, start~, end~)
  }
}

///|
/// Unquote a C-style quoted path (e.g., "\"foo/bar\"" -> "foo/bar").
pub fn fast_import_parse_unquote_path(path : String) -> String {
  if path.has_prefix("\"") && path.length() > 1 {
    let inner = String::unsafe_substring(path, start=1, end=path.length())
    let inner = if inner.length() > 0 {
      let chars : Array[Char] = []
      for c in inner {
        chars.push(c)
      }
      if chars.length() > 0 && chars[chars.length() - 1] == '"' {
        let buf = StringBuilder::new()
        for i in 0..<(chars.length() - 1) {
          buf.write_char(chars[i])
        }
        buf.to_string()
      } else {
        inner
      }
    } else {
      inner
    }
    let chars : Array[Char] = []
    for c in inner {
      chars.push(c)
    }
    let buf = StringBuilder::new()
    let mut i = 0
    while i < chars.length() {
      if chars[i] == '\\' && i + 1 < chars.length() {
        match chars[i + 1] {
          'n' => buf.write_char('\n')
          't' => buf.write_char('\t')
          '\\' => buf.write_char('\\')
          '"' => buf.write_char('"')
          _ => {
            buf.write_char('\\')
            buf.write_char(chars[i + 1])
          }
        }
        i += 2
      } else {
        buf.write_char(chars[i])
        i += 1
      }
    }
    buf.to_string()
  } else {
    path
  }
}

///|
/// Split two space-separated paths, handling quoted paths.
pub fn fast_import_parse_split_two_paths(s : String) -> (String, String)? {
  if s.has_prefix("\"") {
    let chars : Array[Char] = []
    for c in s {
      chars.push(c)
    }
    let mut i = 1
    while i < chars.length() {
      if chars[i] == '"' && (i == 0 || chars[i - 1] != '\\') {
        let first = String::unsafe_substring(s, start=0, end=i + 1)
        let rest = if i + 2 < chars.length() {
          String::unsafe_substring(s, start=i + 2, end=chars.length())
        } else {
          ""
        }
        return Some(
          (
            fast_import_parse_unquote_path(first),
            fast_import_parse_unquote_path(rest),
          ),
        )
      }
      i += 1
    }
    None
  } else {
    match s.find(" ") {
      Some(sp) => {
        let first = String::unsafe_substring(s, start=0, end=sp)
        let second = String::unsafe_substring(s, start=sp + 1, end=s.length())
        Some((first, fast_import_parse_unquote_path(second)))
      }
      None => None
    }
  }
}

///|
fn fast_import_bytes_from_array(arr : Array[Byte]) -> Bytes {
  Bytes::from_array(FixedArray::makei(arr.length(), fn(i) { arr[i] }))
}