// libgit2 バインディング

// 不透明な外部型(C FFI用)

///|
#external
type GitRepository

///|
#external
priv type GitReference

///|
#external
priv type GitRevwalk

///|
#external
priv type GitCommit

///|
#external
priv type GitOidHolder

// ===== UTF-8エンコーディングヘルパー =====
// Using @encoding from moonbitlang/x/encoding

///|
fn to_c_string(s : String) -> Bytes {
  @encoding.encode(UTF8, s)
}

///|
fn from_c_bytes(bytes : Bytes) -> String {
  @encoding.decoder(UTF8).decode_lossy(bytes[:])
}

// ===== Low-level extern declarations =====

///|
extern "C" fn git2_init() -> Int = "git2_init"

///|
extern "C" fn git2_shutdown() -> Int = "git2_shutdown"

///|
#borrow(path)
extern "C" fn git2_repository_open(path : Bytes) -> GitRepository = "git2_repository_open"

///|
#borrow(repo)
extern "C" fn git2_repository_free(repo : GitRepository) -> Unit = "git2_repository_free"

///|
#borrow(repo)
extern "C" fn git2_repository_is_null(repo : GitRepository) -> Int = "git2_repository_is_null"

///|
#borrow(repo)
extern "C" fn git2_repository_path(repo : GitRepository) -> Bytes = "git2_repository_path"

///|
#borrow(repo)
extern "C" fn git2_repository_head(repo : GitRepository) -> GitReference = "git2_repository_head"

///|
#borrow(reference)
extern "C" fn git2_reference_free(reference : GitReference) -> Unit = "git2_reference_free"

///|
#borrow(reference)
extern "C" fn git2_reference_is_null(reference : GitReference) -> Int = "git2_reference_is_null"

///|
#borrow(reference)
extern "C" fn git2_reference_name(reference : GitReference) -> Bytes = "git2_reference_name"

///|
#borrow(reference)
extern "C" fn git2_reference_target_str(reference : GitReference) -> Bytes = "git2_reference_target_str"

///|
#borrow(repo)
extern "C" fn git2_revwalk_new(repo : GitRepository) -> GitRevwalk = "git2_revwalk_new"

///|
#borrow(walk)
extern "C" fn git2_revwalk_free(walk : GitRevwalk) -> Unit = "git2_revwalk_free"

///|
#borrow(walk)
extern "C" fn git2_revwalk_is_null(walk : GitRevwalk) -> Int = "git2_revwalk_is_null"

///|
#borrow(walk)
extern "C" fn git2_revwalk_push_head(walk : GitRevwalk) -> Int = "git2_revwalk_push_head"

///|
#borrow(walk)
extern "C" fn git2_revwalk_sorting(walk : GitRevwalk, sort_mode : Int) -> Int = "git2_revwalk_sorting"

///|
extern "C" fn git2_oid_holder_new() -> GitOidHolder = "git2_oid_holder_new"

///|
#borrow(holder)
extern "C" fn git2_oid_holder_free(holder : GitOidHolder) -> Unit = "git2_oid_holder_free"

///|
#borrow(walk, holder)
extern "C" fn git2_revwalk_next(
  walk : GitRevwalk,
  holder : GitOidHolder,
) -> Int = "git2_revwalk_next"

///|
#borrow(repo, holder)
extern "C" fn git2_commit_lookup(
  repo : GitRepository,
  holder : GitOidHolder,
) -> GitCommit = "git2_commit_lookup"

///|
#borrow(commit)
extern "C" fn git2_commit_free(commit : GitCommit) -> Unit = "git2_commit_free"

///|
#borrow(commit)
extern "C" fn git2_commit_is_null(commit : GitCommit) -> Int = "git2_commit_is_null"

///|
#borrow(commit)
extern "C" fn git2_commit_summary(commit : GitCommit) -> Bytes = "git2_commit_summary"

///|
#borrow(commit)
extern "C" fn git2_commit_id_str(commit : GitCommit) -> Bytes = "git2_commit_id_str"

///|
#borrow(commit)
extern "C" fn git2_commit_time(commit : GitCommit) -> Int64 = "git2_commit_time"

///|
#borrow(commit)
extern "C" fn git2_commit_author_name(commit : GitCommit) -> Bytes = "git2_commit_author_name"

///|
#borrow(commit)
extern "C" fn git2_commit_author_email(commit : GitCommit) -> Bytes = "git2_commit_author_email"

///|
#borrow(commit)
extern "C" fn git2_commit_committer_name(commit : GitCommit) -> Bytes = "git2_commit_committer_name"

///|
#borrow(commit)
extern "C" fn git2_commit_committer_email(commit : GitCommit) -> Bytes = "git2_commit_committer_email"

///|
#borrow(commit)
extern "C" fn git2_commit_message(commit : GitCommit) -> Bytes = "git2_commit_message"

///|
extern "C" fn git2_error_last_message() -> Bytes = "git2_error_last_message"

///|
extern "C" fn git2_is_loaded() -> Int = "git2_is_loaded"

// ===== Branch iterator =====

///|
#external
priv type GitBranchIterator

///|
#borrow(repo)
extern "C" fn git2_branch_iterator_new(
  repo : GitRepository,
  flags : Int,
) -> GitBranchIterator = "git2_branch_iterator_new"

///|
#borrow(iter)
extern "C" fn git2_branch_iterator_free(iter : GitBranchIterator) -> Unit = "git2_branch_iterator_free"

///|
#borrow(iter)
extern "C" fn git2_branch_iterator_is_null(iter : GitBranchIterator) -> Int = "git2_branch_iterator_is_null"

///|
#borrow(iter)
extern "C" fn git2_branch_next(iter : GitBranchIterator) -> Int = "git2_branch_next"

///|
#borrow(iter)
extern "C" fn git2_branch_name(iter : GitBranchIterator) -> Bytes = "git2_branch_name"

///|
#borrow(iter)
extern "C" fn git2_branch_type(iter : GitBranchIterator) -> Int = "git2_branch_type"

// ===== Diff =====

///|
#external
priv type GitDiff

///|
#external
priv type GitDiffStats

///|
#borrow(repo)
extern "C" fn git2_diff_index_to_workdir(repo : GitRepository) -> GitDiff = "git2_diff_index_to_workdir"

///|
#borrow(diff)
extern "C" fn git2_diff_free(diff : GitDiff) -> Unit = "git2_diff_free"

///|
#borrow(diff)
extern "C" fn git2_diff_is_null(diff : GitDiff) -> Int = "git2_diff_is_null"

///|
#borrow(diff)
extern "C" fn git2_diff_num_deltas(diff : GitDiff) -> Int = "git2_diff_num_deltas"

///|
#borrow(diff)
extern "C" fn git2_diff_stats(diff : GitDiff) -> GitDiffStats = "git2_diff_stats"

///|
#borrow(stats)
extern "C" fn git2_diff_stats_free(stats : GitDiffStats) -> Unit = "git2_diff_stats_free"

///|
#borrow(stats)
extern "C" fn git2_diff_stats_is_null(stats : GitDiffStats) -> Int = "git2_diff_stats_is_null"

///|
#borrow(stats)
extern "C" fn git2_diff_stats_files_changed(stats : GitDiffStats) -> Int = "git2_diff_stats_files_changed"

///|
#borrow(stats)
extern "C" fn git2_diff_stats_insertions(stats : GitDiffStats) -> Int = "git2_diff_stats_insertions"

///|
#borrow(stats)
extern "C" fn git2_diff_stats_deletions(stats : GitDiffStats) -> Int = "git2_diff_stats_deletions"

// ===== Status =====

///|
#external
priv type GitStatusIterator

///|
#borrow(repo)
extern "C" fn git2_status_new(repo : GitRepository) -> GitStatusIterator = "git2_status_new"

///|
#borrow(iter)
extern "C" fn git2_status_free(iter : GitStatusIterator) -> Unit = "git2_status_free"

///|
#borrow(iter)
extern "C" fn git2_status_is_null(iter : GitStatusIterator) -> Int = "git2_status_is_null"

///|
#borrow(iter)
extern "C" fn git2_status_count(iter : GitStatusIterator) -> Int = "git2_status_count"

///|
#borrow(iter)
extern "C" fn git2_status_next(iter : GitStatusIterator) -> Int = "git2_status_next"

///|
#borrow(iter)
extern "C" fn git2_status_advance(iter : GitStatusIterator) -> Unit = "git2_status_advance"

///|
#borrow(iter)
extern "C" fn git2_status_entry_flags(iter : GitStatusIterator) -> Int = "git2_status_entry_flags"

// ===== High-level API =====

// Sort modes for revwalk

///|
pub const GIT_SORT_NONE : Int = 0

///|
pub const GIT_SORT_TOPOLOGICAL : Int = 1

///|
pub const GIT_SORT_TIME : Int = 2

///|
pub const GIT_SORT_REVERSE : Int = 4

///|
/// ライブラリがロードされているかチェック
pub fn is_loaded() -> Bool {
  git2_is_loaded() == 1
}

///|
/// ライブラリの初期化(ライブラリのロードも行う)
/// 戻り値: 成功時はtrue、失敗時はfalse
pub fn git_init() -> Bool {
  let rc = git2_init()
  rc >= 0
}

///|
/// ライブラリのシャットダウン
pub fn git_shutdown() -> Unit {
  let _ = git2_shutdown()

}

///|
/// 最後のエラーメッセージを取得
pub fn last_error() -> String {
  from_c_bytes(git2_error_last_message())
}

///|
/// リポジトリ
pub struct Repository {
  priv handle : GitRepository
}

///|
#borrow(path)
extern "C" fn git2_repository_init(
  path : Bytes,
  is_bare : Int,
) -> GitRepository = "git2_repository_init"

///|
/// リポジトリを開く
pub fn Repository::open(path : String) -> Repository? {
  let repo = git2_repository_open(to_c_string(path))
  if git2_repository_is_null(repo) == 1 {
    None
  } else {
    Some({ handle: repo })
  }
}

///|
/// 新しいリポジトリを初期化
pub fn Repository::init(path : String) -> Repository? {
  let repo = git2_repository_init(to_c_string(path), 0)
  if git2_repository_is_null(repo) == 1 {
    None
  } else {
    Some({ handle: repo })
  }
}

///|
/// リポジトリを閉じる
pub fn Repository::close(self : Repository) -> Unit {
  git2_repository_free(self.handle)
}

///|
/// リポジトリのパスを取得
pub fn Repository::path(self : Repository) -> String {
  from_c_bytes(git2_repository_path(self.handle))
}

///|
/// HEADの参照名を取得
pub fn Repository::head_name(self : Repository) -> String? {
  let head_ref = git2_repository_head(self.handle)
  if git2_reference_is_null(head_ref) == 1 {
    None
  } else {
    let name = from_c_bytes(git2_reference_name(head_ref))
    git2_reference_free(head_ref)
    Some(name)
  }
}

///|
/// HEADのコミットIDを取得
pub fn Repository::head_id(self : Repository) -> String? {
  let head_ref = git2_repository_head(self.handle)
  if git2_reference_is_null(head_ref) == 1 {
    None
  } else {
    let id = from_c_bytes(git2_reference_target_str(head_ref))
    git2_reference_free(head_ref)
    Some(id)
  }
}

///|
/// コミット情報
pub struct CommitInfo {
  id : String
  message : String
  summary : String
  author_name : String
  author_email : String
  committer_name : String
  committer_email : String
  time : Int64
}

///|
pub fn CommitInfo::short_id(self : CommitInfo) -> String {
  let buf = StringBuilder::new()
  let len = if self.id.length() < 7 { self.id.length() } else { 7 }
  for i = 0; i < len; i = i + 1 {
    buf.write_char(self.id.code_unit_at(i).to_int().unsafe_to_char())
  }
  buf.to_string()
}

///|
pub fn CommitInfo::to_string(self : CommitInfo) -> String {
  let short_id = self.short_id()
  "\{short_id} \{self.summary} <\{self.author_name}>"
}

// ===== Branch API =====

///|
/// ブランチタイプ
pub enum BranchType {
  Local
  Remote
}

///|
/// ブランチ情報
pub struct BranchInfo {
  name : String
  branch_type : BranchType
}

///|
pub fn BranchInfo::to_string(self : BranchInfo) -> String {
  let type_str = match self.branch_type {
    Local => "local"
    Remote => "remote"
  }
  "\{self.name} (\{type_str})"
}

///|
/// ブランチ一覧を取得
pub fn Repository::branches(
  self : Repository,
  include_remote : Bool,
) -> Array[BranchInfo] {
  let branches : Array[BranchInfo] = []
  let flags = if include_remote { 3 } else { 1 } // GIT_BRANCH_ALL or GIT_BRANCH_LOCAL
  let iter = git2_branch_iterator_new(self.handle, flags)
  if git2_branch_iterator_is_null(iter) == 1 {
    return branches
  }
  while git2_branch_next(iter) == 0 {
    let name = from_c_bytes(git2_branch_name(iter))
    let branch_type : BranchType = if git2_branch_type(iter) == 1 {
      Local
    } else {
      Remote
    }
    branches.push({ name, branch_type })
  }
  git2_branch_iterator_free(iter)
  branches
}

// ===== Diff API =====

///|
/// 差分統計
pub struct DiffStats {
  files_changed : Int
  insertions : Int
  deletions : Int
}

///|
pub fn DiffStats::to_string(self : DiffStats) -> String {
  "\{self.files_changed} files changed, \{self.insertions} insertions(+), \{self.deletions} deletions(-)"
}

///|
/// ワーキングツリーの差分統計を取得
pub fn Repository::diff_stats(self : Repository) -> DiffStats? {
  let diff = git2_diff_index_to_workdir(self.handle)
  if git2_diff_is_null(diff) == 1 {
    return None
  }
  let stats = git2_diff_stats(diff)
  if git2_diff_stats_is_null(stats) == 1 {
    git2_diff_free(diff)
    return None
  }
  let result : DiffStats = {
    files_changed: git2_diff_stats_files_changed(stats),
    insertions: git2_diff_stats_insertions(stats),
    deletions: git2_diff_stats_deletions(stats),
  }
  git2_diff_stats_free(stats)
  git2_diff_free(diff)
  Some(result)
}

///|
/// ワーキングツリーの変更ファイル数を取得
pub fn Repository::diff_count(self : Repository) -> Int {
  let diff = git2_diff_index_to_workdir(self.handle)
  if git2_diff_is_null(diff) == 1 {
    return 0
  }
  let count = git2_diff_num_deltas(diff)
  git2_diff_free(diff)
  count
}

// ===== Status API =====

// Status flags (from libgit2)

///|
pub const GIT_STATUS_CURRENT : Int = 0

///|
pub const GIT_STATUS_INDEX_NEW : Int = 1

///|
pub const GIT_STATUS_INDEX_MODIFIED : Int = 2

///|
pub const GIT_STATUS_INDEX_DELETED : Int = 4

///|
pub const GIT_STATUS_WT_NEW : Int = 128

///|
pub const GIT_STATUS_WT_MODIFIED : Int = 256

///|
pub const GIT_STATUS_WT_DELETED : Int = 512

///|
/// ステータスフラグ
pub struct StatusFlags {
  flags : Int
}

///|
pub fn StatusFlags::new(flags : Int) -> StatusFlags {
  { flags, }
}

///|
pub fn StatusFlags::is_new(self : StatusFlags) -> Bool {
  (self.flags & GIT_STATUS_WT_NEW) != 0
}

///|
pub fn StatusFlags::is_modified(self : StatusFlags) -> Bool {
  (self.flags & GIT_STATUS_WT_MODIFIED) != 0
}

///|
pub fn StatusFlags::is_deleted(self : StatusFlags) -> Bool {
  (self.flags & GIT_STATUS_WT_DELETED) != 0
}

///|
pub fn StatusFlags::is_staged(self : StatusFlags) -> Bool {
  (self.flags & 0x1F) != 0 // Any INDEX_* flag
}

///|
pub fn StatusFlags::to_string(self : StatusFlags) -> String {
  let parts : Array[String] = []
  if self.is_new() {
    parts.push("new")
  }
  if self.is_modified() {
    parts.push("modified")
  }
  if self.is_deleted() {
    parts.push("deleted")
  }
  if self.is_staged() {
    parts.push("staged")
  }
  if parts.length() == 0 {
    "clean"
  } else {
    parts
    .iter()
    .fold(init="", fn(acc, s) { if acc == "" { s } else { acc + ", " + s } })
  }
}

///|
/// ワーキングツリーのステータス(変更ファイル数)
pub fn Repository::status_count(self : Repository) -> Int {
  let iter = git2_status_new(self.handle)
  if git2_status_is_null(iter) == 1 {
    return 0
  }
  let count = git2_status_count(iter)
  git2_status_free(iter)
  count
}

///|
/// ワーキングツリーのステータスフラグ一覧を取得
pub fn Repository::status_flags(self : Repository) -> Array[StatusFlags] {
  let result : Array[StatusFlags] = []
  let iter = git2_status_new(self.handle)
  if git2_status_is_null(iter) == 1 {
    return result
  }
  while git2_status_next(iter) == 0 {
    let flags = git2_status_entry_flags(iter)
    result.push(StatusFlags::new(flags))
    git2_status_advance(iter)
  }
  git2_status_free(iter)
  result
}

// ===== Tree API =====

///|
#external
type GitTree

///|
#borrow(commit)
extern "C" fn git2_commit_tree(commit : GitCommit) -> GitTree = "git2_commit_tree"

///|
#borrow(tree)
extern "C" fn git2_tree_free(tree : GitTree) -> Unit = "git2_tree_free"

///|
#borrow(tree)
extern "C" fn git2_tree_is_null(tree : GitTree) -> Int = "git2_tree_is_null"

///|
#borrow(tree)
extern "C" fn git2_tree_entrycount(tree : GitTree) -> Int = "git2_tree_entrycount"

///|
#borrow(tree)
extern "C" fn git2_tree_entry_name_at(tree : GitTree, idx : Int) -> Bytes = "git2_tree_entry_name_at"

///|
#borrow(tree)
extern "C" fn git2_tree_entry_type_at(tree : GitTree, idx : Int) -> Int = "git2_tree_entry_type_at"

///|
#borrow(tree)
extern "C" fn git2_tree_entry_id_at(tree : GitTree, idx : Int) -> Bytes = "git2_tree_entry_id_at"

///|
/// オブジェクトタイプ
pub enum ObjectType {
  Blob
  Tree
  Commit
  Tag
  Unknown
}

///|
/// ツリーエントリ情報
pub struct TreeEntry {
  name : String
  id : String
  object_type : ObjectType
}

///|
pub fn TreeEntry::to_string(self : TreeEntry) -> String {
  let type_str = match self.object_type {
    Blob => "blob"
    Tree => "tree"
    Commit => "commit"
    Tag => "tag"
    Unknown => "unknown"
  }
  "\{type_str} \{self.id} \{self.name}"
}

///|
/// ツリー
pub struct Tree {
  priv handle : GitTree
}

///|
pub fn Tree::entry_count(self : Tree) -> Int {
  git2_tree_entrycount(self.handle)
}

///|
pub fn Tree::entries(self : Tree) -> Array[TreeEntry] {
  let result : Array[TreeEntry] = []
  let count = git2_tree_entrycount(self.handle)
  for i = 0; i < count; i = i + 1 {
    let name = from_c_bytes(git2_tree_entry_name_at(self.handle, i))
    let id = from_c_bytes(git2_tree_entry_id_at(self.handle, i))
    let type_int = git2_tree_entry_type_at(self.handle, i)
    // libgit2 git_object_t values: COMMIT=1, TREE=2, BLOB=3, TAG=4
    let object_type : ObjectType = match type_int {
      1 => Commit
      2 => Tree
      3 => Blob
      4 => Tag
      _ => Unknown
    }
    result.push({ name, id, object_type })
  }
  result
}

///|
pub fn Tree::close(self : Tree) -> Unit {
  git2_tree_free(self.handle)
}

///|
/// コミットからツリーを取得する内部関数
fn get_tree_from_commit(commit : GitCommit) -> Tree? {
  let tree = git2_commit_tree(commit)
  if git2_tree_is_null(tree) == 1 {
    None
  } else {
    Some({ handle: tree })
  }
}

// ===== Tag API =====

///|
#external
priv type GitTagList

///|
#borrow(repo)
extern "C" fn git2_tag_list(repo : GitRepository) -> GitTagList = "git2_tag_list"

///|
#borrow(list)
extern "C" fn git2_tag_list_free(list : GitTagList) -> Unit = "git2_tag_list_free"

///|
#borrow(list)
extern "C" fn git2_tag_list_is_null(list : GitTagList) -> Int = "git2_tag_list_is_null"

///|
#borrow(list)
extern "C" fn git2_tag_list_count(list : GitTagList) -> Int = "git2_tag_list_count"

///|
#borrow(list)
extern "C" fn git2_tag_list_name_at(list : GitTagList, idx : Int) -> Bytes = "git2_tag_list_name_at"

///|
#borrow(repo)
extern "C" fn git2_tag_count(repo : GitRepository) -> Int = "git2_tag_count"

///|
/// タグ一覧を取得
pub fn Repository::tags(self : Repository) -> Array[String] {
  let result : Array[String] = []
  let list = git2_tag_list(self.handle)
  if git2_tag_list_is_null(list) == 1 {
    return result
  }
  let count = git2_tag_list_count(list)
  for i = 0; i < count; i = i + 1 {
    let name = from_c_bytes(git2_tag_list_name_at(list, i))
    result.push(name)
  }
  git2_tag_list_free(list)
  result
}

///|
/// タグ数を取得
pub fn Repository::tag_count(self : Repository) -> Int {
  git2_tag_count(self.handle)
}

// ===== Remote API =====

///|
#external
priv type GitRemoteList

///|
#external
priv type GitRemote

///|
#borrow(repo)
extern "C" fn git2_remote_list(repo : GitRepository) -> GitRemoteList = "git2_remote_list"

///|
#borrow(list)
extern "C" fn git2_remote_list_free(list : GitRemoteList) -> Unit = "git2_remote_list_free"

///|
#borrow(list)
extern "C" fn git2_remote_list_is_null(list : GitRemoteList) -> Int = "git2_remote_list_is_null"

///|
#borrow(list)
extern "C" fn git2_remote_list_count(list : GitRemoteList) -> Int = "git2_remote_list_count"

///|
#borrow(list)
extern "C" fn git2_remote_list_name_at(
  list : GitRemoteList,
  idx : Int,
) -> Bytes = "git2_remote_list_name_at"

///|
#borrow(repo, name)
extern "C" fn git2_remote_lookup(
  repo : GitRepository,
  name : Bytes,
) -> GitRemote = "git2_remote_lookup"

///|
#borrow(remote)
extern "C" fn git2_remote_free(remote : GitRemote) -> Unit = "git2_remote_free"

///|
#borrow(remote)
extern "C" fn git2_remote_is_null(remote : GitRemote) -> Int = "git2_remote_is_null"

///|
#borrow(remote)
extern "C" fn git2_remote_name(remote : GitRemote) -> Bytes = "git2_remote_name"

///|
#borrow(remote)
extern "C" fn git2_remote_url(remote : GitRemote) -> Bytes = "git2_remote_url"

///|
#borrow(remote)
extern "C" fn git2_remote_pushurl(remote : GitRemote) -> Bytes = "git2_remote_pushurl"

///|
/// リモート情報
pub struct RemoteInfo {
  name : String
  url : String
  push_url : String
}

///|
pub fn RemoteInfo::to_string(self : RemoteInfo) -> String {
  "\{self.name} \{self.url}"
}

///|
/// リモート名一覧を取得
pub fn Repository::remote_names(self : Repository) -> Array[String] {
  let result : Array[String] = []
  let list = git2_remote_list(self.handle)
  if git2_remote_list_is_null(list) == 1 {
    return result
  }
  let count = git2_remote_list_count(list)
  for i = 0; i < count; i = i + 1 {
    let name = from_c_bytes(git2_remote_list_name_at(list, i))
    result.push(name)
  }
  git2_remote_list_free(list)
  result
}

///|
/// リモート情報を取得
pub fn Repository::remote(self : Repository, name : String) -> RemoteInfo? {
  let remote = git2_remote_lookup(self.handle, to_c_string(name))
  if git2_remote_is_null(remote) == 1 {
    return None
  }
  let result : RemoteInfo = {
    name: from_c_bytes(git2_remote_name(remote)),
    url: from_c_bytes(git2_remote_url(remote)),
    push_url: from_c_bytes(git2_remote_pushurl(remote)),
  }
  git2_remote_free(remote)
  Some(result)
}

///|
/// すべてのリモート情報を取得
pub fn Repository::remotes(self : Repository) -> Array[RemoteInfo] {
  let result : Array[RemoteInfo] = []
  let names = self.remote_names()
  for name in names {
    match self.remote(name) {
      Some(info) => result.push(info)
      None => ()
    }
  }
  result
}

// ===== Config API =====

///|
#borrow(repo, name)
extern "C" fn git2_config_get_string(
  repo : GitRepository,
  name : Bytes,
) -> Bytes = "git2_config_get_string"

///|
/// git config の値を取得
pub fn Repository::config_get(self : Repository, name : String) -> String? {
  let value = git2_config_get_string(self.handle, to_c_string(name))
  let str = from_c_bytes(value)
  if str.length() == 0 {
    None
  } else {
    Some(str)
  }
}

///|
/// HEAD のツリーを取得
pub fn Repository::head_tree(self : Repository) -> Tree? {
  // Get HEAD commit
  let walk = git2_revwalk_new(self.handle)
  if git2_revwalk_is_null(walk) == 1 {
    return None
  }
  let _ = git2_revwalk_push_head(walk)
  let holder = git2_oid_holder_new()
  let err = git2_revwalk_next(walk, holder)
  if err != 0 {
    git2_oid_holder_free(holder)
    git2_revwalk_free(walk)
    return None
  }
  let commit = git2_commit_lookup(self.handle, holder)
  git2_oid_holder_free(holder)
  git2_revwalk_free(walk)
  if git2_commit_is_null(commit) == 1 {
    return None
  }
  let result = get_tree_from_commit(commit)
  git2_commit_free(commit)
  result
}

///|
/// コミット履歴を取得(最新からn件)
pub fn Repository::log(self : Repository, max_count : Int) -> Array[CommitInfo] {
  let commits : Array[CommitInfo] = []
  let walk = git2_revwalk_new(self.handle)
  if git2_revwalk_is_null(walk) == 1 {
    return commits
  }
  let _ = git2_revwalk_sorting(walk, GIT_SORT_TIME)
  let _ = git2_revwalk_push_head(walk)
  let holder = git2_oid_holder_new()
  let mut count = 0
  while count < max_count {
    let err = git2_revwalk_next(walk, holder)
    if err != 0 {
      break
    }
    let commit = git2_commit_lookup(self.handle, holder)
    if git2_commit_is_null(commit) == 1 {
      continue
    }
    let info : CommitInfo = {
      id: from_c_bytes(git2_commit_id_str(commit)),
      message: from_c_bytes(git2_commit_message(commit)),
      summary: from_c_bytes(git2_commit_summary(commit)),
      author_name: from_c_bytes(git2_commit_author_name(commit)),
      author_email: from_c_bytes(git2_commit_author_email(commit)),
      committer_name: from_c_bytes(git2_commit_committer_name(commit)),
      committer_email: from_c_bytes(git2_commit_committer_email(commit)),
      time: git2_commit_time(commit),
    }
    commits.push(info)
    git2_commit_free(commit)
    count += 1
  }
  git2_oid_holder_free(holder)
  git2_revwalk_free(walk)
  commits
}

// ===== Blob API =====

///|
#external
type GitBlob

///|
#borrow(repo, oid_str)
extern "C" fn git2_blob_lookup(
  repo : GitRepository,
  oid_str : Bytes,
) -> GitBlob = "git2_blob_lookup"

///|
#borrow(blob)
extern "C" fn git2_blob_free(blob : GitBlob) -> Unit = "git2_blob_free"

///|
#borrow(blob)
extern "C" fn git2_blob_is_null(blob : GitBlob) -> Int = "git2_blob_is_null"

///|
#borrow(blob)
extern "C" fn git2_blob_rawsize(blob : GitBlob) -> Int64 = "git2_blob_rawsize"

///|
#borrow(blob)
extern "C" fn git2_blob_is_binary(blob : GitBlob) -> Int = "git2_blob_is_binary"

///|
#borrow(blob)
extern "C" fn git2_blob_content(blob : GitBlob) -> Bytes = "git2_blob_content"

///|
/// ブロブ(ファイル内容)
pub struct Blob {
  priv handle : GitBlob
}

///|
/// OID からブロブを取得
pub fn Repository::blob(self : Repository, oid : String) -> Blob? {
  let blob = git2_blob_lookup(self.handle, to_c_string(oid))
  if git2_blob_is_null(blob) == 1 {
    None
  } else {
    Some({ handle: blob })
  }
}

///|
/// ブロブのサイズを取得
pub fn Blob::size(self : Blob) -> Int64 {
  git2_blob_rawsize(self.handle)
}

///|
/// バイナリファイルかどうか
pub fn Blob::is_binary(self : Blob) -> Bool {
  git2_blob_is_binary(self.handle) == 1
}

///|
/// ブロブの内容を取得(バイト列)
pub fn Blob::content(self : Blob) -> Bytes {
  git2_blob_content(self.handle)
}

///|
/// ブロブの内容を文字列として取得
pub fn Blob::content_string(self : Blob) -> String {
  from_c_bytes(git2_blob_content(self.handle))
}

///|
/// ブロブを閉じる
pub fn Blob::close(self : Blob) -> Unit {
  git2_blob_free(self.handle)
}

// ===== Index API =====

///|
#external
type GitIndex

///|
#borrow(repo)
extern "C" fn git2_index_open(repo : GitRepository) -> GitIndex = "git2_index_open"

///|
#borrow(index)
extern "C" fn git2_index_free(index : GitIndex) -> Unit = "git2_index_free"

///|
#borrow(index)
extern "C" fn git2_index_is_null(index : GitIndex) -> Int = "git2_index_is_null"

///|
#borrow(index, path)
extern "C" fn git2_index_add_bypath(index : GitIndex, path : Bytes) -> Int = "git2_index_add_bypath"

///|
#borrow(index, path)
extern "C" fn git2_index_remove_bypath(index : GitIndex, path : Bytes) -> Int = "git2_index_remove_bypath"

///|
#borrow(index)
extern "C" fn git2_index_write(index : GitIndex) -> Int = "git2_index_write"

///|
#borrow(index)
extern "C" fn git2_index_write_tree(index : GitIndex) -> Bytes = "git2_index_write_tree"

///|
#borrow(index)
extern "C" fn git2_index_entrycount(index : GitIndex) -> Int = "git2_index_entrycount"

///|
#borrow(index)
extern "C" fn git2_index_read(index : GitIndex) -> Int = "git2_index_read"

///|
/// インデックス(ステージングエリア)
pub struct Index {
  priv handle : GitIndex
}

///|
/// インデックスを開く
pub fn Repository::index(self : Repository) -> Index? {
  let idx = git2_index_open(self.handle)
  if git2_index_is_null(idx) == 1 {
    None
  } else {
    Some({ handle: idx })
  }
}

///|
/// ファイルをステージに追加
pub fn Index::add(self : Index, path : String) -> Bool {
  git2_index_add_bypath(self.handle, to_c_string(path)) == 0
}

///|
/// ファイルをステージから削除
pub fn Index::remove(self : Index, path : String) -> Bool {
  git2_index_remove_bypath(self.handle, to_c_string(path)) == 0
}

///|
/// インデックスを書き込み
pub fn Index::write(self : Index) -> Bool {
  git2_index_write(self.handle) == 0
}

///|
/// インデックスからツリーを作成し、ツリー OID を返す
pub fn Index::write_tree(self : Index) -> String? {
  let oid = from_c_bytes(git2_index_write_tree(self.handle))
  if oid.length() == 40 {
    Some(oid)
  } else {
    None
  }
}

///|
/// ステージされたエントリ数
pub fn Index::entry_count(self : Index) -> Int {
  git2_index_entrycount(self.handle)
}

///|
/// インデックスを再読み込み
pub fn Index::reload(self : Index) -> Bool {
  git2_index_read(self.handle) == 0
}

///|
/// インデックスを閉じる
pub fn Index::close(self : Index) -> Unit {
  git2_index_free(self.handle)
}

// ===== Signature API =====

///|
#external
type GitSignature

///|
#borrow(name, email)
extern "C" fn git2_signature_now(name : Bytes, email : Bytes) -> GitSignature = "git2_signature_now"

///|
#borrow(repo)
extern "C" fn git2_signature_default(repo : GitRepository) -> GitSignature = "git2_signature_default"

///|
#borrow(sig)
extern "C" fn git2_signature_free(sig : GitSignature) -> Unit = "git2_signature_free"

///|
#borrow(sig)
extern "C" fn git2_signature_is_null(sig : GitSignature) -> Int = "git2_signature_is_null"

///|
#borrow(sig)
extern "C" fn git2_signature_name(sig : GitSignature) -> Bytes = "git2_signature_name"

///|
#borrow(sig)
extern "C" fn git2_signature_email(sig : GitSignature) -> Bytes = "git2_signature_email"

///|
/// 署名(author/committer)
pub struct Signature {
  priv handle : GitSignature
}

///|
/// 現在時刻で署名を作成
pub fn Signature::now(name : String, email : String) -> Signature? {
  let sig = git2_signature_now(to_c_string(name), to_c_string(email))
  if git2_signature_is_null(sig) == 1 {
    None
  } else {
    Some({ handle: sig })
  }
}

///|
/// リポジトリのデフォルト署名(user.name, user.email から)
pub fn Repository::default_signature(self : Repository) -> Signature? {
  let sig = git2_signature_default(self.handle)
  if git2_signature_is_null(sig) == 1 {
    None
  } else {
    Some({ handle: sig })
  }
}

///|
/// 署名の名前
pub fn Signature::name(self : Signature) -> String {
  from_c_bytes(git2_signature_name(self.handle))
}

///|
/// 署名のメールアドレス
pub fn Signature::email(self : Signature) -> String {
  from_c_bytes(git2_signature_email(self.handle))
}

///|
/// 署名を解放
pub fn Signature::close(self : Signature) -> Unit {
  git2_signature_free(self.handle)
}

// ===== Commit 作成 API =====

///|
#borrow(repo, message, author, committer, tree_oid)
extern "C" fn git2_commit_create(
  repo : GitRepository,
  message : Bytes,
  author : GitSignature,
  committer : GitSignature,
  tree_oid : Bytes,
) -> Bytes = "git2_commit_create"

///|
/// コミットを作成(HEAD を親として)
/// 戻り値: 新しいコミットの OID
pub fn Repository::commit(
  self : Repository,
  message : String,
  author : Signature,
  committer : Signature,
  tree_oid : String,
) -> String? {
  let oid = from_c_bytes(
    git2_commit_create(
      self.handle,
      to_c_string(message),
      author.handle,
      committer.handle,
      to_c_string(tree_oid),
    ),
  )
  if oid.length() == 40 {
    Some(oid)
  } else {
    None
  }
}

///|
/// 簡易コミット(デフォルト署名を使用)
pub fn Repository::commit_simple(
  self : Repository,
  message : String,
) -> String? {
  // インデックスを開く
  let idx = match self.index() {
    Some(i) => i
    None => return None
  }
  // ツリーを書き込み
  let tree_oid = match idx.write_tree() {
    Some(oid) => oid
    None => {
      idx.close()
      return None
    }
  }
  idx.close()
  // デフォルト署名を取得
  let sig = match self.default_signature() {
    Some(s) => s
    None => return None
  }
  // コミット作成
  let result = self.commit(message, sig, sig, tree_oid)
  sig.close()
  result
}

// ===== Checkout API =====

///|
#borrow(repo)
extern "C" fn git2_checkout_head(repo : GitRepository, force : Int) -> Int = "git2_checkout_head"

///|
/// HEAD にチェックアウト
pub fn Repository::checkout_head(self : Repository) -> Bool {
  git2_checkout_head(self.handle, 0) == 0
}

// ===== Reset API =====

///|
/// リセットタイプ
pub(all) enum ResetType {
  Soft // HEAD のみ移動
  Mixed // HEAD とインデックスを更新(デフォルト)
  Hard // HEAD、インデックス、ワーキングツリーを更新
}

///|
#borrow(repo, commit_oid)
extern "C" fn git2_reset(
  repo : GitRepository,
  commit_oid : Bytes,
  reset_type : Int,
) -> Int = "git2_reset"

///|
/// 指定したコミットにリセット
pub fn Repository::reset(
  self : Repository,
  commit_oid : String,
  reset_type : ResetType,
) -> Bool {
  let type_int = match reset_type {
    Soft => 1
    Mixed => 2
    Hard => 3
  }
  git2_reset(self.handle, to_c_string(commit_oid), type_int) == 0
}