#!/usr/bin/env bash
# MIT (c) Wenxuan Zhang

# This file is meant to be executed directly. If it's available on the PATH,
# it can also be used as a subcommand of git, which then forwards all arguments
# on to forgit. So, all of these commands will work as expected:
#
# `git forgit log`
# `git forgit checkout_file`
# `git forgit checkout_file README.md`
#
# This gives users the choice to set aliases inside of their git config instead
# of their shell config if they prefer.

# Set shell for fzf preview commands
# Disable shellcheck for "which", because it suggests "command -v xxx" instead,
# which is not a working replacement.
# See https://github.com/koalaman/shellcheck/issues/1162
# shellcheck disable=2230
SHELL="$(which bash)"
export SHELL

# Get absolute forgit path
FORGIT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)/$(basename -- "${BASH_SOURCE[0]}")

FORGIT_FZF_DEFAULT_OPTS="
$FZF_DEFAULT_OPTS
--ansi
--height='80%'
--bind='alt-k:preview-up,alt-p:preview-up'
--bind='alt-j:preview-down,alt-n:preview-down'
--bind='ctrl-r:toggle-all'
--bind='ctrl-s:toggle-sort'
--bind='?:toggle-preview'
--bind='alt-w:toggle-preview-wrap'
--preview-window='right:60%'
+1
$FORGIT_FZF_DEFAULT_OPTS
"

_forgit_warn() { printf "%b[Warn]%b %s\n" '\e[0;33m' '\e[0m' "$@" >&2; }
_forgit_info() { printf "%b[Info]%b %s\n" '\e[0;32m' '\e[0m' "$@" >&2; }
_forgit_inside_work_tree() { git rev-parse --is-inside-work-tree >/dev/null; }
# tac is not available on OSX, tail -r is not available on Linux, so we use either of them
_forgit_reverse_lines() { tac 2> /dev/null || tail -r; }

_forgit_previous_commit() {
    # "SHA~" is invalid when the commit is the first commit, but we can use "--root" instead
    if [[ "$(git rev-parse "$1")" == "$(git rev-list --max-parents=0 HEAD)" ]]; then
        echo "--root"
    else
        echo "$1~"
    fi
}

_forgit_contains_non_flags() {
    while (("$#")); do
        case "$1" in
        -*) shift ;;
        *)
            return 0
            ;;
        esac
    done
    return 1
}

# optional render emoji characters (https://github.com/wfxr/emoji-cli)
hash emojify &>/dev/null && _forgit_emojify='|emojify'

# extract the first git sha occurring in the input and strip trailing newline
_forgit_extract_sha="grep -Eo '[a-f0-9]+' | head -1 | tr -d '[:space:]'"

_forgit_pager=${FORGIT_PAGER:-$(git config core.pager || echo 'cat')}
_forgit_show_pager=${FORGIT_SHOW_PAGER:-$(git config pager.show || echo "$_forgit_pager")}
_forgit_diff_pager=${FORGIT_DIFF_PAGER:-$(git config pager.diff || echo "$_forgit_pager")}
_forgit_ignore_pager=${FORGIT_IGNORE_PAGER:-$(hash bat &>/dev/null && echo 'bat -l gitignore --color=always' || echo 'cat')}
_forgit_blame_pager=${FORGIT_BLAME_PAGER:-$(git config pager.blame || echo "$_forgit_pager")}
_forgit_enter_pager=${FORGIT_ENTER_PAGER:-"LESS='-r' less"}

_forgit_log_format=${FORGIT_LOG_FORMAT:-%C(auto)%h%d %s %C(black)%C(bold)%cr%Creset}
_forgit_log_preview_options="--graph --pretty=format:'$_forgit_log_format' --color=always --abbrev-commit --date=relative"
_forgit_fullscreen_context=${FORGIT_FULLSCREEN_CONTEXT:-10}
_forgit_preview_context=${FORGIT_PREVIEW_CONTEXT:-3}
_forgit_is_file_tracked="(git ls-files {} --error-unmatch) &> /dev/null"

# git commit viewer
_forgit_log() {
    _forgit_inside_work_tree || return 1
    local opts graph files log_format preview_cmd enter_cmd
    files=$(sed -nE 's/.*-- (.*)/\1/p' <<< "$*") # extract files parameters for `git show` command
    preview_cmd="echo {} | $_forgit_extract_sha | xargs -I% git show --color=always -U$_forgit_preview_context % -- $files | $_forgit_show_pager"
    enter_cmd="echo {} | $_forgit_extract_sha | xargs -I% ${FORGIT} diff %^! $files"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        +s +m --tiebreak=index
        --bind=\"enter:execute($enter_cmd)\"
        --bind=\"ctrl-y:execute-silent(echo {} | $_forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
        --preview=\"$preview_cmd\"
        $FORGIT_LOG_FZF_OPTS
    "
    graph=--graph
    [[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
    log_format=${FORGIT_GLO_FORMAT:-$_forgit_log_format}
    eval "git log $graph --color=always --format='$log_format' $FORGIT_LOG_GIT_OPTS $* $_forgit_emojify" |
        FZF_DEFAULT_OPTS="$opts" fzf
    fzf_exit_code=$?
    # exit successfully on 130 (ctrl-c/esc)
    [[ $fzf_exit_code == 130 ]] && return 0
    return $fzf_exit_code
}

# git diff viewer
_forgit_diff() {
    _forgit_inside_work_tree || return 1
    local files opts commits repo get_files preview_cmd enter_cmd
    [[ $# -ne 0 ]] && {
        if git rev-parse "$1" -- &>/dev/null ; then
            if [[ $# -gt 1 ]] && git rev-parse "$2" -- &>/dev/null; then
                commits="$1 $2" && files=("${@:3}")
            else
                commits="$1" && files=("${@:2}")
            fi
        else
            files=("$@")
        fi
    }
    repo="$(git rev-parse --show-toplevel)"
    # Construct a null-terminated list of the filenames
    # The input looks like one of these lines:
    #   [R100]  file  ->  another file
    #   [A]     file with spaces
    #   [D]     oldfile
    # And we transform it to this representation for further usage with "xargs -0":
    #   file\0another file\0
    #   file with spaces\0
    #   oldfile\0
    # We have to do a two-step sed -> tr pipe because OSX's sed implementation does
    # not support the null-character directly.
    get_files="echo {} | sed 's/.*] *//' | sed 's/  ->  /\\\n/' | tr '\\\n' '\\\0'"
    # Similar to the line above, but only gets a single file from a single line
    # Gets the new name of renamed files
    get_file="echo {} | sed 's/.*] *//' | sed 's/.*->  //'"
    # Git stashes are named "stash@{x}", which contains the fzf placeholder "{x}".
    # In order to support passing stashes as arguments to _forgit_diff, we have to
    # prevent fzf from interpreting this substring by escaping the opening bracket.
    # The string is evaluated a few subsequent times, so we need multiple escapes.
    escaped_commits=${commits//\{/\\\\\{}
    git_diff="git diff --color=always $FORGIT_DIFF_GIT_OPTS $escaped_commits"
    preview_cmd="cd '$repo' && $get_files | xargs -0 $git_diff -U$_forgit_preview_context -- | $_forgit_diff_pager"
    enter_cmd="cd '$repo' && $get_files | xargs -0 $git_diff -U$_forgit_fullscreen_context -- | $_forgit_diff_pager"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        +m -0 --bind=\"enter:execute($enter_cmd | $_forgit_enter_pager)\"
        --preview=\"$preview_cmd\"
        --bind=\"alt-e:execute-silent($EDITOR \\\"\$\($get_file)\\\" >/dev/tty </dev/tty)+refresh-preview\"
        $FORGIT_DIFF_FZF_OPTS
        --prompt=\"$commits > \"
    "
    eval "git diff --name-status $FORGIT_DIFF_GIT_OPTS $commits -- ${files[*]} | sed -E 's/^([[:alnum:]]+)[[:space:]]+(.*)$/[\1]	\2/'" |
        sed 's/	/  ->  /2' | expand -t 8 |
        FZF_DEFAULT_OPTS="$opts" fzf
    fzf_exit_code=$?
    # exit successfully on 130 (ctrl-c/esc)
    [[ $fzf_exit_code == 130 ]] && return 0
    return $fzf_exit_code
}

# git add selector
_forgit_add() {
    _forgit_inside_work_tree || return 1
    local git_add changed unmerged untracked files opts preview extract
    git_add="git add $FORGIT_ADD_GIT_OPTS"
    # Add files if passed as arguments
    [[ $# -ne 0 ]] && { $git_add "$@" && git status -su; return $?; }

    changed=$(git config --get-color color.status.changed red)
    unmerged=$(git config --get-color color.status.unmerged red)
    untracked=$(git config --get-color color.status.untracked red)
    # NOTE: paths listed by 'git status -su' mixed with quoted and unquoted style
    # remove indicators | remove original path for rename case | remove surrounding quotes
    extract="
        sed 's/^.*]  //' |
        sed 's/.* -> //' |
        sed -e 's/^\\\"//' -e 's/\\\"\$//'"
    preview="
        file=\$(echo {} | $extract)
        if (git status -s -- \\\"\$file\\\" | grep '^??') &>/dev/null; then  # diff with /dev/null for untracked files
            git diff --color=always --no-index -- /dev/null \\\"\$file\\\" | $_forgit_diff_pager | sed '2 s/added:/untracked:/'
        else
            git diff --color=always -- \\\"\$file\\\" | $_forgit_diff_pager
        fi"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        -0 -m --nth 2..,..
        --preview=\"$preview\"
        --bind=\"alt-e:execute-silent($EDITOR \\\"\$\(echo {} | $extract\)\\\" >/dev/tty </dev/tty)+refresh-preview\"
        $FORGIT_ADD_FZF_OPTS
    "
    files=$(git -c color.status=always -c status.relativePaths=true status -su |
        grep -F -e "$changed" -e "$unmerged" -e "$untracked" |
        sed -E 's/^(..[^[:space:]]*)[[:space:]]+(.*)$/[\1]  \2/' |
        FZF_DEFAULT_OPTS="$opts" fzf |
        sh -c "$extract")
    [[ -n "$files" ]] && echo "$files"| tr '\n' '\0' | $git_add --pathspec-file-nul --pathspec-from-file - && git status -su && return
    echo 'Nothing to add.'
}

# git reset HEAD (unstage) selector
_forgit_reset_head() {
    _forgit_inside_work_tree || return 1
    local git_reset_head cmd files opts rootdir
    git_reset_head="git reset -q $FORGIT_RESET_HEAD_GIT_OPTS HEAD"
    [[ $# -ne 0 ]] && { $git_reset_head "$@" && git status --short; return $?; }
    rootdir=$(git rev-parse --show-toplevel)
    cmd="git diff --staged --color=always -- $rootdir/{} | $_forgit_diff_pager "
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        -m -0
        --preview=\"$cmd\"
        $FORGIT_RESET_HEAD_FZF_OPTS
    "
    files="$(git diff --staged --name-only | FZF_DEFAULT_OPTS="$opts" fzf)"
    # shellcheck disable=2086
    [[ -n "$files" ]] && echo "$files" | tr '\n' '\0' | xargs -0 -I% $git_reset_head "$rootdir"/% && git status --short && return
    echo 'Nothing to unstage.'
}

# git stash viewer
_forgit_stash_show() {
    _forgit_inside_work_tree || return 1
    local git_stash_show git_stash_list cmd opts
    git_stash_show="git stash show --color=always --ext-diff"
    [[ $# -ne 0 ]] && { $git_stash_show "$@"; return $?; }
    git_stash_list="git stash list $FORGIT_STASH_SHOW_GIT_OPTS"
    cmd="echo {} |cut -d: -f1 |xargs -I% $git_stash_show % |$_forgit_diff_pager"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        +s +m -0 --tiebreak=index --bind=\"enter:execute($cmd | $_forgit_enter_pager)\"
        --bind=\"ctrl-y:execute-silent(echo {} | cut -d: -f1 | tr -d '[:space:]' | ${FORGIT_COPY_CMD:-pbcopy})\"
        --preview=\"$cmd\"
        $FORGIT_STASH_FZF_OPTS
    "
    $git_stash_list | FZF_DEFAULT_OPTS="$opts" fzf
    fzf_exit_code=$?
    # exit successfully on 130 (ctrl-c/esc)
    [[ $fzf_exit_code == 130 ]] && return 0
    return $fzf_exit_code
}

# git stash push selector
_forgit_stash_push() {
    _forgit_inside_work_tree || return 1
    local git_stash_push msg args
    git_stash_push="git stash push $FORGIT_STASH_PUSH_GIT_OPTS"
    args=( "$@" )
    while (( "$#" )); do
        case "$1" in
            # allow message as argument
            -m|--message)
            msg="$2"
            shift 2
            ;;
            # ignore -u as it's used implicitly
            -u|--include-untracked) shift ;;
            # pass to git directly when encountering anything else
            *) $git_stash_push "${args[@]}"; return $?
        esac
    done
    local opts preview files
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        -m
        $FORGIT_STASH_PUSH_FZF_OPTS
    "
    preview="
        if $_forgit_is_file_tracked; then
            git diff --color=always {} | $_forgit_diff_pager
        else
            git diff --color=always /dev/null {} | $_forgit_diff_pager
        fi
    "
    # Show both modified and untracked files
    files=$(git ls-files --exclude-standard --modified --others | FZF_DEFAULT_OPTS="$opts" fzf --preview="$preview")
    [[ -z "$files" ]] && return 1
    echo "${files[@]}" | tr '\n' '\0' | $git_stash_push ${msg:+-m "$msg"} -u --pathspec-file-nul --pathspec-from-file -
}

# git clean selector
_forgit_clean() {
    _forgit_inside_work_tree || return 1
    _forgit_contains_non_flags "$@" && { git clean -q "$@"; return $?; }
    local git_clean files opts
    git_clean="git clean $FORGIT_CLEAN_GIT_OPTS"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        -m -0
        $FORGIT_CLEAN_FZF_OPTS
    "
    # Note: Postfix '/' in directory path should be removed. Otherwise the directory itself will not be removed.
    files=$(git clean -xdffn "$@"| sed 's/^Would remove //' | FZF_DEFAULT_OPTS="$opts" fzf |sed 's#/$##')
    # shellcheck disable=2086
    [[ -n "$files" ]] && echo "$files" | tr '\n' '\0' | xargs -0 -I% $git_clean -xdff '%' && git status --short && return
    echo 'Nothing to clean.'
}

_forgit_cherry_pick() {
    local git_cherry_pick base target preview opts fzf_selection fzf_exitval

    git_cherry_pick="git cherry-pick $FORGIT_CHERRY_PICK_GIT_OPTS"

    base=$(git branch --show-current)
    [[ -z "$base" ]] && echo "Current commit is not on a branch." && return 1

    [[ -z $1 ]] && echo "Please specify target branch" && return 1
    target="$1"

    # in this function, we do something interesting to maintain proper ordering as it's assumed 
    # you generally want to cherry pick oldest->newest when you multiselect
    # The instances of "cut", "nl" and "sort" all serve this purpose
    # Please see https://github.com/wfxr/forgit/issues/253 for more details

    preview="echo {} | cut -f2- | $_forgit_extract_sha | xargs -I% git show --color=always % | $_forgit_show_pager"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        --preview=\"$preview\"
        --multi --ansi --with-nth 2.. -0 --tiebreak=index
        $FORGIT_CHERRY_PICK_FZF_OPTS
    "
    # Note: do not add any pipe after the fzf call here, otherwise the fzf_exitval is not propagated properly.
    # Any eventual post processing can be done afterwards when the "commits" variable is assigned below.
    fzf_selection=$(git log --right-only --color=always --cherry-pick --oneline "$base"..."$target" | nl |
        FZF_DEFAULT_OPTS="$opts" fzf)
    fzf_exitval=$?
    [[ $fzf_exitval != 0 ]] && return $fzf_exitval
    [[ -z "$fzf_selection" ]] && return $fzf_exitval

    ${IFS+"false"} && unset old_IFS || old_IFS="$IFS"
    IFS=$'\n'
    # shellcheck disable=2207
    commits=($(echo "$fzf_selection" | sort --numeric-sort --key=1 | cut -f2 | cut -d' ' -f1 | _forgit_reverse_lines))
    ${old_IFS+"false"} && unset IFS || IFS="$old_IFS"
    [ ${#commits[@]} -eq 0 ] && return 1

    $git_cherry_pick "${commits[@]}"
}

_forgit_cherry_pick_from_branch() {
    _forgit_inside_work_tree || return 1
    local cmd preview opts branch exitval input_branch args base

    base=$(git branch --show-current)
    [[ -z "$base" ]] && echo "Current commit is not on a branch." && return 1

    args=("$@")
    if [[ $# -ne 0 ]]; then
        input_branch=${args[0]}
    fi
    cmd="git branch --color=always --all | LC_ALL=C sort -k1.1,1.1 -rs"
    preview="git log --right-only --color=always --cherry-pick --oneline $base...{1}"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        +s +m --tiebreak=index --header-lines=1
        --preview=\"$preview\"
        $FORGIT_CHERRY_PICK_FROM_BRANCH_FZF_OPTS
        "
    # loop until either the branch selector is closed or a commit to be cherry
    # picked has been selected from within a branch
    while true
    do
        if [[ -z $input_branch ]]; then
            branch="$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')"
        else
            branch=$input_branch
        fi

        unset input_branch
        [[ -z "$branch" ]] && return 1

        _forgit_cherry_pick "$branch"

        exitval=$?
        [[ $exitval != 130 ]] || [[ $# -ne 0 ]] && return $exitval
    done
}

_forgit_rebase() {
    _forgit_inside_work_tree || return 1
    local git_rebase cmd preview opts graph files target_commit prev_commit
    git_rebase="git rebase -i $FORGIT_REBASE_GIT_OPTS"
    graph=--graph
    [[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
    cmd="git log $graph --color=always --format='$_forgit_log_format' $* $_forgit_emojify"
    files=$(sed -nE 's/.* -- (.*)/\1/p' <<< "$*") # extract files parameters for `git show` command
    preview="echo {} | $_forgit_extract_sha | xargs -I% git show --color=always % -- $files | $_forgit_show_pager"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        +s +m --tiebreak=index
        --bind=\"ctrl-y:execute-silent(echo {} | $_forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
        --preview=\"$preview\"
        $FORGIT_REBASE_FZF_OPTS
    "
    target_commit=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | eval "$_forgit_extract_sha")
    if [[ -n "$target_commit" ]]; then
        prev_commit=$(_forgit_previous_commit "$target_commit")

        $git_rebase "$prev_commit"
    fi
}

_forgit_fixup() {
    _forgit_inside_work_tree || return 1
    git diff --cached --quiet && echo 'Nothing to fixup: there are no staged changes.' && return 1
    local git_fixup cmd preview opts graph files target_commit prev_commit
    git_fixup="git commit --fixup $FORGIT_FIXUP_GIT_OPTS"
    graph=--graph
    [[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
    cmd="git log $graph --color=always --format='$_forgit_log_format' $* $_forgit_emojify"
    files=$(sed -nE 's/.* -- (.*)/\1/p' <<< "$*")
    preview="echo {} | $_forgit_extract_sha | xargs -I% git show --color=always % -- $files | $_forgit_show_pager"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        +s +m --tiebreak=index
        --bind=\"ctrl-y:execute-silent(echo {} | $_forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
        --preview=\"$preview\"
        $FORGIT_FIXUP_FZF_OPTS
    "
    target_commit=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | eval "$_forgit_extract_sha")
    if [[ -n "$target_commit" ]] && $git_fixup "$target_commit"; then
        prev_commit=$(_forgit_previous_commit "$target_commit")
        # rebase will fail if there are unstaged changes so --autostash is needed to temporarily stash them
        # GIT_SEQUENCE_EDITOR=: is needed to skip the editor
        GIT_SEQUENCE_EDITOR=: git rebase --autostash -i --autosquash "$prev_commit"
    fi

}

# git checkout-file selector
_forgit_checkout_file() {
    _forgit_inside_work_tree || return 1
    local git_checkout cmd files opts
    git_checkout="git checkout $FORGIT_CHECKOUT_FILE_GIT_OPTS"
    [[ $# -ne 0 ]] && { $git_checkout -- "$@"; return $?; }
    cmd="git diff --color=always -- {} | $_forgit_diff_pager"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        -m -0
        --preview=\"$cmd\"
        $FORGIT_CHECKOUT_FILE_FZF_OPTS
    "
    files="$(git ls-files --modified "$(git rev-parse --show-toplevel)"| FZF_DEFAULT_OPTS="$opts" fzf)"
    [[ -n "$files" ]] && echo "$files" | tr '\n' '\0' | $git_checkout --pathspec-file-nul --pathspec-from-file -
}

# git checkout-branch selector
_forgit_checkout_branch() {
    _forgit_inside_work_tree || return 1
    # if called with arguments, check if branch exists, else create a new one
    if [[ $# -ne 0 ]]; then
        if [[ "$*" == "-" ]] || git show-branch "$@" &>/dev/null; then
            git switch "$@"
        else
            git switch -c "$@"
        fi
        checkout_status=$?
        git status --short
        return $checkout_status
    fi

    local git_checkout cmd preview opts branch
    cmd="git branch --color=always ${FORGIT_CHECKOUT_BRANCH_BRANCH_GIT_OPTS:---all} | LC_ALL=C sort -k1.1,1.1 -rs"
    preview="git log {1} $_forgit_log_preview_options"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        +s +m --tiebreak=index --header-lines=1
        --preview=\"$preview\"
        $FORGIT_CHECKOUT_BRANCH_FZF_OPTS
        "
    branch="$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')"
    [[ -z "$branch" ]] && return 1

    git_checkout="git checkout $FORGIT_CHECKOUT_BRANCH_GIT_OPTS"
    # track the remote branch if possible
    if [[ "$branch" == "remotes/origin/"* ]]; then
        if git branch | grep -qw "${branch#remotes/origin/}"; then
            # hack to force creating a new branch which tracks the remote if a local branch already exists
            $git_checkout -b "track/${branch#remotes/origin/}" --track "$branch"
        elif ! $git_checkout --track "$branch" 2>/dev/null; then
            $git_checkout "$branch"
        fi
    else
        $git_checkout "$branch"
    fi
}

# git checkout-tag selector
_forgit_checkout_tag() {
    _forgit_inside_work_tree || return 1
    local git_checkout cmd opts preview
    git_checkout="git checkout $FORGIT_CHECKOUT_TAG_GIT_OPTS"
    [[ $# -ne 0 ]] && { $git_checkout "$@"; return $?; }
    cmd="git tag -l --sort=-v:refname"
    preview="git log {1} $_forgit_log_preview_options"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        +s +m --tiebreak=index
        --preview=\"$preview\"
        $FORGIT_CHECKOUT_TAG_FZF_OPTS
    "
    tag="$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf)"
    [[ -z "$tag" ]] && return 1
    $git_checkout "$tag"
}

# git checkout-commit selector
_forgit_checkout_commit() {
    _forgit_inside_work_tree || return 1
    local git_checkout cmd opts graph
    git_checkout="git checkout $FORGIT_CHECKOUT_COMMIT_GIT_OPTS"
    [[ $# -ne 0 ]] && { $git_checkout "$@"; return $?; }
    cmd="echo {} | $_forgit_extract_sha |xargs -I% git show --color=always % | $_forgit_show_pager"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        +s +m --tiebreak=index
        --bind=\"ctrl-y:execute-silent(echo {} | $_forgit_extract_sha | ${FORGIT_COPY_CMD:-pbcopy})\"
        --preview=\"$cmd\"
        $FORGIT_CHECKOUT_COMMIT_FZF_OPTS
    "
    graph=--graph
    [[ $FORGIT_LOG_GRAPH_ENABLE == false ]] && graph=
    # shellcheck disable=2086
    eval "git log $graph --color=always --format='$_forgit_log_format' $_forgit_emojify" |
        FZF_DEFAULT_OPTS="$opts" fzf | eval "$_forgit_extract_sha" | xargs -I% $git_checkout % --
}

_forgit_branch_delete() {
    _forgit_inside_work_tree || return 1
    local git_branch preview opts cmd branches
    git_branch="git branch $FORGIT_BRANCH_DELETE_GIT_OPTS"
    [[ $# -ne 0 ]] && { $git_branch -D "$@"; return $?; }
    preview="git log {1} $_forgit_log_preview_options"

    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        +s --multi --tiebreak=index --header-lines=1
        --preview=\"$preview\"
        $FORGIT_BRANCH_DELETE_FZF_OPTS
    "

    cmd="git branch --color=always | LC_ALL=C sort -k1.1,1.1 -rs"
    branches=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $1}')
    # shellcheck disable=2086
    echo -n "$branches" | tr '\n' '\0' | xargs -I{} -0 $git_branch -D {}
}

# git revert-commit selector
_forgit_revert_commit() {
    _forgit_inside_work_tree || return 1
    local git_revert cmd opts files preview commits IFS
    git_revert="git revert $FORGIT_REVERT_COMMIT_GIT_OPTS"
    [[ $# -ne 0 ]] && { $git_revert "$@"; return $?; }

    cmd="git log --graph --color=always --format='$_forgit_log_format' $* $_forgit_emojify"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        +s --tiebreak=index
        --ansi --with-nth 2..
        $FORGIT_REVERT_COMMIT_FZF_OPTS
    "

    # in this function, we do something interesting to maintain proper ordering as it's assumed 
    # you generally want to revert newest->oldest when you multiselect
    # The instances of "cut", "nl" and "sort" all serve this purpose
    # Please see https://github.com/wfxr/forgit/issues/253 for more details

    files=$(sed -nE 's/.* -- (.*)/\1/p' <<< "$*") # extract files parameters for `git show` command
    preview="echo {} | cut -f2- | $_forgit_extract_sha | xargs -I% git show --color=always % -- $files | $_forgit_show_pager"

    ${IFS+"false"} && unset old_IFS || old_IFS="$IFS"
    IFS=$'\n'
    # shellcheck disable=2207
    commits=($(eval "$cmd" | 
        nl |
        FZF_DEFAULT_OPTS="$opts" fzf --preview="$preview" -m | 
        sort --numeric-sort --key=1 | 
        cut -f2- |
        sed 's/^[^a-f^0-9]*\([a-f0-9]*\).*/\1/'))
    ${old_IFS+"false"} && unset IFS || IFS="$old_IFS"

    [ ${#commits[@]} -eq 0 ] && return 1

    $git_revert "${commits[@]}"
}

# git blame viewer
_forgit_blame() {
    _forgit_inside_work_tree || return 1
    local git_blame opts flags preview file
    git_blame="git blame $FORGIT_BLAME_GIT_OPTS"
    _forgit_contains_non_flags "$@" && { $git_blame "$@"; return $?; }
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        $FORGIT_BLAME_FZF_OPTS
    "
    flags=$(git rev-parse --flags "$@")
    preview="
        if $_forgit_is_file_tracked; then
            git blame {} --date=short $FORGIT_BLAME_GIT_OPTS $flags | $_forgit_blame_pager
        else
            echo File not tracked
        fi
    "
    file=$(FZF_DEFAULT_OPTS="$opts" fzf --preview="$preview")
    [[ -z "$file" ]] && return 1
    # shellcheck disable=2086
    eval $git_blame "$file" "$flags"
}

# git ignore generator
export FORGIT_GI_REPO_REMOTE=${FORGIT_GI_REPO_REMOTE:-https://github.com/dvcs/gitignore}
export FORGIT_GI_REPO_LOCAL="${FORGIT_GI_REPO_LOCAL:-${XDG_CACHE_HOME:-$HOME/.cache}/forgit/gi/repos/dvcs/gitignore}"
export FORGIT_GI_TEMPLATES=${FORGIT_GI_TEMPLATES:-$FORGIT_GI_REPO_LOCAL/templates}

_forgit_ignore() {
    [ -d "$FORGIT_GI_REPO_LOCAL" ] || _forgit_ignore_update
    local IFS cmd args opts
    cmd="$_forgit_ignore_pager $FORGIT_GI_TEMPLATES/{2}{,.gitignore} 2>/dev/null"
    opts="
        $FORGIT_FZF_DEFAULT_OPTS
        -m --preview-window='right:70%'
        --preview=\"eval $cmd\"
        $FORGIT_IGNORE_FZF_OPTS
    "
    ${IFS+"false"} && unset old_IFS || old_IFS="$IFS"
    IFS=$'\n'
    # shellcheck disable=SC2206,2207
    args=($@) && [[ $# -eq 0 ]] && args=($(_forgit_ignore_list | nl -nrn -w4 -s'  ' |
        FZF_DEFAULT_OPTS="$opts" fzf | awk '{print $2}'))
    ${old_IFS+"false"} && unset IFS || IFS="$old_IFS"
    [ ${#args[@]} -eq 0 ] && return 1
    # shellcheck disable=SC2068
    _forgit_ignore_get ${args[@]}
}
_forgit_ignore_update() {
    if [[ -d "$FORGIT_GI_REPO_LOCAL" ]]; then
        _forgit_info 'Updating gitignore repo...'
        (cd "$FORGIT_GI_REPO_LOCAL" && git pull --no-rebase --ff) || return 1
    else
        _forgit_info 'Initializing gitignore repo...'
        git clone --depth=1 "$FORGIT_GI_REPO_REMOTE" "$FORGIT_GI_REPO_LOCAL"
    fi
}
_forgit_ignore_get() {
    local item filename header
    for item in "$@"; do
        if filename=$(find -L "$FORGIT_GI_TEMPLATES" -type f \( -iname "${item}.gitignore" -o -iname "${item}" \) -print -quit); then
            [[ -z "$filename" ]] && _forgit_warn "No gitignore template found for '$item'." && continue
            header="${filename##*/}" && header="${header%.gitignore}"
            echo "### $header" && cat "$filename" && echo
        fi
    done
}
_forgit_ignore_list() {
    find "$FORGIT_GI_TEMPLATES" -print |sed -e 's#.gitignore$##' -e 's#.*/##' | sort -fu
}
_forgit_ignore_clean() {
    setopt localoptions rmstarsilent
    [[ -d "$FORGIT_GI_REPO_LOCAL" ]] && rm -rf "$FORGIT_GI_REPO_LOCAL"
}

valid_commands=(
    "add"
    "blame"
    "branch_delete"
    "checkout_branch"
    "checkout_commit"
    "checkout_file"
    "checkout_tag"
    "cherry_pick"
    "cherry_pick_from_branch"
    "clean"
    "diff"
    "fixup"
    "ignore"
    "log"
    "rebase"
    "reset_head"
    "revert_commit"
    "stash_show"
    "stash_push"
)

cmd="$1"
shift

# shellcheck disable=SC2076
if [[ ! " ${valid_commands[*]} " =~ " ${cmd} " ]]; then
    if [[ -z "$cmd" ]]; then
        printf "forgit: missing command\n\n"
    else
        printf "forgit: '%s' is not a valid forgit command.\n\n" "$cmd"
    fi
    printf "The following commands are supported:\n"
    printf "\t%s\n" "${valid_commands[@]}"
    exit 1
fi

_forgit_"${cmd}" "$@"
