#!/usr/bin/env sh

# SPDX-License-Identifier:  MIT
# Copyright 2023-2024 Jorengarenar

# globals {{{

gc_prog_name="fmt-diff"
gc_version="1.0.2"

g_cached_opt=

gf_debug=0
gf_no_term=
gf_use_color=
gf_warn_risky_stderr=

g_fmtprg_devnull="2> /dev/null"

g_rm_list=""

# }}}

# helpers {{{1

abspath () {
    if [ -d "$1" ]; then
        cd "$1" && pwd
    elif [ -f "$1" ]; then
        case "$1" in
            /*)  echo "$1" ;;
            */*) echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" ;;
            *)   echo "$PWD/$1" ;;
        esac
    else
        warn "no such file or directory"
    fi
}

cleanup () {
    sed '1!G;h;$!d' "$g_rm_list" | while read -r path; do
        if [ -d "$path" ]; then
            for f in "$path"/*; do
                [ -e "$f" ] && [ ! -s "$f" ] && rm "$f"
            done
            rmdir "$path"
        else
            rm "$path"
        fi
    done
}

debug () (
    [ "$1" != "--" ] && [ "$gf_debug" -eq 0 ] && return
    if [ -z "$BASH_VERSION" ]; then
        >&2 printf '[debug] %s | %s\n' "$(basename "$0")" "$*"
        return
    fi

    # shellcheck disable=SC3028,SC3054
    >&2 printf '[debug] %s:%-3d  %30s | %s\n' \
        "$(basename "$0")" "${BASH_LINENO[0]}" "${FUNCNAME[1]}"'()'  "$*"
)

mk_tmp () (
    if [ -z "$TMPDIR" ]; then
        debug "TMPDIR not set"
    fi

    if [ ! -d "$TMPDIR" ]; then
        debug "TMPDIR=$TMPDIR doesn't exist"
        mkdir -p "$TMPDIR"
        if [ ! -d "$TMPDIR" ]; then
            debug "unable to create $TMPDIR"
            return 1
        fi
    fi

    tmp="$TMPDIR"
    while [ -e "$tmp" ]; do
        tmp="${TMPDIR:-/tmp}"/"$(sh -c 'echo "$$"')"
    done

    cmd="touch"
    while [ "$#" -gt 0 ]; do
        case "$1" in
            -u) cmd= ;;
            -d) cmd="mkdir" ;;
            --suffix) tmp="$tmp$2" && shift ;;
            -p) debug "option '-p' not supported" && shift ;;
            -?*) debug "option '$1' not supported" ;;
            ?*) debug "custom TEMPLATE not supported" ;;
        esac
        shift
    done

    if [ -n "$cmd" ]; then
        $cmd "$tmp"
        if [ -e "$tmp" ]; then
            debug "tmp = $tmp"
        else
            debug "unable to create tmp = $tmp"
            return 1
        fi
    fi

    echo "$tmp"
)

pat_list_to_case () {
    sed 's/\s*,\s*/|/g'
}

rm_list__push () {
    [ "$gf_debug" -ge 3 ] && return
    for f in "$@"; do
        echo "$f" >> "$g_rm_list"
    done
}

sed_esc () {
    if [ "$1" = "-p" ]; then
        printf '%s\n' "$2" | sed -e 's/[]\/$*.^[]/\\&/g'
    elif [ "$1" = "-s" ]; then
        printf '%s\n' "$2" | sed -e 's/[\/&]/\\&/g'
    fi
}

sgr () {
    if [ "$gf_use_color" -eq 1 ]; then
        printf "\033[%sm%s\033[0m" "$1" "$2"
    else
        printf '%s' "$2"
    fi
}

warn () {
    >&2 printf "%s: " "$(basename "$0")"
    # printf "\033[30m\033[103m"
    >&2 echo "$@"
    # printf "\033[0m"
}

# git config --get {{{1

_git_conf_get__ft_map () (
    file="$1"

    debug "args =" "$@"

    git config --get-regexp "$gc_prog_name"'\.map-' | while read -r line; do
        debug "line = $line"
        line="$(echo "$line" | awk -F'map-' '{print $2}')"
        ft="$(echo "$line" | cut -d' ' -f1)"
        ext="$(echo "$line" | cut -d' ' -f2- | pat_list_to_case)"

        if [ -n "$ext" ]; then
            # shellcheck disable=SC2254
            eval "case '$file' in
                $ext)
                    echo '$ft'
                    return
                    ;;
            esac"
        fi
    done
)

_git_conf_get__glob__var () (
    debug "args =" "$@"

    file="$1"
    var="$2"

    val=

    while read -r line; do
        debug "line = $line"
        opt="$(echo "$line" | cut -d' ' -f1)"
        # shellcheck disable=SC2254
        case "$gc_prog_name.$file.$var" in
            $opt) val="$(echo "$line" | cut -d' ' -f2-)" ;;
        esac
    done << EOF
$(git config --get-regexp "$gc_prog_name"'\.[^=].*\.'"$var"'$')
EOF

    echo "$val"
)

_git_conf_get__ft_formatter () (
    file="$1"
    ft="$2"

    debug "args =" "$@"

    fmt="$(_git_conf_get__glob__var "$file" "formatter-$ft")"

    if [ -z "$fmt" ]; then
        fmt="$(_git_conf_get__var "$gc_prog_name.=$ft.formatter")"
    fi

    if [ -z "$fmt" ]; then
        fmt="$(_git_conf_get__var "$gc_prog_name.formatter-$ft")"
    fi

    echo "$fmt"
)

_git_conf_get__pager () (
    if [ "$gf_no_term" = 1 ]; then
        echo "cat"
        return
    fi

    pgr=
    [ -z "$pgr" ] && pgr="$(git_conf_get pager.$gc_prog_name)"
    [ -z "$pgr" ] && pgr="$(git_conf_get pager.diff)"
    [ -z "$pgr" ] && pgr="$(git_conf_get core.pager)"
    [ -z "$pgr" ] && pgr="$GIT_PAGER"
    [ -z "$pgr" ] && pgr="$PAGER"
    [ -z "$pgr" ] && pgr="less"

    echo "$pgr"
)

_git_conf_get__var () (
    debug "args =" "$@"
    # shellcheck disable=SC2086
    git config --get "$@"
)

git_conf_get () (
    if [ "$1" = "pager" ]; then
        _git_conf_get__pager
        return
    elif [ -z "$2" ]; then
        _git_conf_get__var "$1"
        return
    fi

    file="$1"
    var="$2"

    val=$(_git_conf_get__glob__var "$file" "$var")

    if [ -z "$val" ] && [ "$var" = "filetype" ]; then
        val="$(_git_conf_get__ft_map "$file")"
        [ -z "$val" ] && return 1
    fi

    if [ -z "$val" ]; then
        ft="$(detect_filetype "$file")"
        if [ -n "$ft" ]; then
            debug "var = $var"
            case "$var" in
                formatter)
                    val="$(_git_conf_get__ft_formatter "$file" "$ft")"
                    ;;
                *)
                    val="$(_git_conf_get__var "$gc_prog_name.=$ft.$var")"
                    ;;
            esac
        fi
    fi

    if [ -z "$val" ] && [ "$var" = "formatter" ]; then
        val="$(_git_conf_get__var "$gc_prog_name"."$var")"
    fi

    if [ -z "$val" ] && [ "$var" = "ignore" ]; then
        list="$(_git_conf_get__var "$gc_prog_name"."$var")"
        if [ -n "$list" ]; then
            list="$(echo "$list" | pat_list_to_case)"
            # shellcheck disable=SC2254
            eval "case '$file' in
                $list) val=1 ;;
            esac"
        fi
    fi

    echo "$val"
)

# processing files {{{1

detect_filetype () (
    file="$1"

    ft="$(git_conf_get "$1" filetype)"

    if [ -z "$ft" ] && [ -x "$(command -v vim)" ]; then
        vim --clean -es \
            +'let c_syntax_for_h = 1' \
            +"e $1" \
            +'exec "!echo ".&ft' \
            +"quit!"
    fi

    echo "$ft"
)

git_changes_formatted () (
    tempdir="$1"
    formatter="$2"

    b_raw="$3"
    a_raw="$4"
    b_fmt="$5"
    a_fmt="$6"

    merge () { git merge-file -p "$@"; }
    chng_fmt="$tempdir"/chng_fmt
    temp="$tempdir"/temp

    rm_list__push "$temp"
    rm_list__push "$chng_fmt"

    if git diff -s "$b_fmt" "$a_fmt"; then
        merge --our "$b_raw" "$a_raw" "$b_fmt" > "$temp"
        merge "$b_raw" "$temp" "$b_fmt" > "$chng_fmt"
        echo "$chng_fmt"
        return
    fi

    merge --their "$a_raw" "$a_fmt" "$b_fmt" > "$chng_fmt"

    merge --our "$chng_fmt" "$a_raw" "$b_raw" > "$temp"
    if ! git diff -s "$chng_fmt" "$temp"; then
        merge --their "$b_raw" "$chng_fmt" "$b_fmt" > "$temp"
        merge --their "$b_raw" "$temp" "$b_fmt" > "$chng_fmt"
    fi

    merge --our "$a_raw" "$a_fmt" "$b_raw" > "$temp"
    if ! git diff -s "$chng_fmt" "$temp"; then
        echo "$chng_fmt"
    fi

    sh -c "$formatter" < "$chng_fmt" > "$temp" 2> /dev/null
    if ! git diff -s "$b_fmt" "$temp"; then
        return 1
    fi
)

git_diff () (
    b_raw="$1"
    chng_fmt="$2"
    filename="$3"

    case "$filename" in
        /*) ;;
        *) a_filename="a/$a_filename" ;;
    esac

    [ "$gf_use_color" -eq 1 ] && color="--color" || color=

    # shellcheck disable=SC2086
    git diff $color "$b_raw" "$chng_fmt" | \
        sed \
            -e "s/a\/*$(sed_esc -p "$b_raw")/$(sed_esc -s a/"$filename")/g" \
            -e "s/b\/*$(sed_esc -p "$chng_fmt")/$(sed_esc -s b/"$filename")/g"
)

git_retrieve_file_from_sha () (
    sha="${1%%.*}"
    file="$2"

    debug "sha = '$sha'"
    debug "file = '$file'"

    if [ "$sha" -eq 0 ] 2> /dev/null; then
        if [ -n "$file" ]; then
            cat "$file"
        else
            printf ''
        fi
    else
        git show "$sha" 2> /dev/null
    fi
)

list_files () (
    if ! git rev-parse --is-inside-work-tree 2> /dev/null 1>&2; then
        [ -s "$1" ] && printf '0 0 %s\n' "$(abspath "$1")"
        return
    fi

    if [ -e "$1" ] && ! git ls-files --error-unmatch "$1" 2> /dev/null 1>&2; then
        printf '0 0 %s\n' "$(abspath "$1")"
        return
    fi

    # shellcheck disable=SC2086
    git diff $g_cached_opt --diff-filter=ARMC --raw "$@" | \
        awk '{ print $3 " " $4 " " $6 " " $7 }'
)

process_file () (
    risky="$1"

    a_hash="$2"
    b_hash="$3"
    a_name="$4"
    b_name="${5:-$a_name}"

    debug "# PROCESSING: $b_name #"
    debug "args = " "$@"

    case "$(git_conf_get "$b_name" ignore)" in
        true|1) return ;;
    esac

    formatter="$(git_conf_get "$b_name" formatter)"
    debug "formatter = $formatter"

    if [ -z "$formatter" ]; then
        return
    fi

    if [ "${formatter%% *}" = "!" ]; then
        formatter="${formatter#! *}"
    elif [ ! -x "$(command -v "${formatter%% *}")" ]; then
        warn "format program '${formatter%% *}' not found"
        return
    fi

    tempdir="$(mk_tmp -d)"
    rm_list__push "$tempdir"

    debug "$b_name => $tempdir"

    b_raw="$tempdir"/b_raw
    a_raw="$tempdir"/a_raw
    b_fmt="$tempdir"/b_fmt
    a_fmt="$tempdir"/a_fmt

    git_retrieve_file_from_sha "$a_hash" "" > "$a_raw"
    git_retrieve_file_from_sha "$b_hash" "$b_name" > "$b_raw"
    rm_list__push "$a_raw" "$b_raw"

    if [ ! -s "$b_raw" ]; then
        warn "couldn't retrieve '$b_name'"
        return
    fi

    fmtcmd="$tempdir"/fmtcmd
    rm_list__push "$fmtcmd"
    echo "$formatter" > "$fmtcmd"

    eval "sh '$fmtcmd' < '$b_raw' > '$b_fmt' $g_fmtprg_devnull"
    rm_list__push "$b_fmt"

    git diff -s "$b_raw" "$b_fmt" && return

    eval "sh '$fmtcmd' < '$a_raw' > '$a_fmt' $g_fmtprg_devnull"
    rm_list__push "$a_fmt"

    chng_fmt="$(git_changes_formatted \
        "$tempdir" "$formatter"\
        "$b_raw" "$a_raw" \
        "$b_fmt" "$a_fmt" \
    )"
    ret="$?"

    [ -z "$chng_fmt" ] && return

    [ "$ret" -ne 0 ] && echo "$b_name" >> "$risky"
    git_diff "$b_raw" "$chng_fmt" "$b_name"
)

processing () (
    outdir="$(mk_tmp -d --suffix '.out')"
    rm_list__push "$outdir"
    debug "OUTDIR: $outdir"

    files="$(list_files "$@")"  # must be before 'cd $repo_root'

    repo_root="$(git rev-parse --show-toplevel 2> /dev/null)"
    [ -n "$repo_root" ] && { cd "$repo_root" || exit 1; }

    risky="$outdir"/0.risky.list
    touch "$risky"
    rm_list__push "$risky"

    i=1
    i_len="$(echo "$files" | wc -l | tr -d '\n' | wc -m)"
    while read -r line; do
        [ -z "$line" ] && continue

        outfile="$(printf '%s/%.*d' "$outdir" "$i_len" "$i")"
        rm_list__push "$outfile"

        # shellcheck disable=SC2086
        process_file "$risky" $line > "$outfile" &
        [ "$gf_debug" -ge 2 ] && wait

        i=$((i+1))
    done << EOL
$files
EOL

    wait

    if [ -s "$risky" ]; then
        cat > "$risky".temp << EOF
$(sgr 33 '@ WARNING!')
$(sgr 33 '@ Suggestions for the following files might smuggle unsolicited changes:')
$(sort "$risky" | sed 's/^/-  /')
$(sgr 33 "@ Review them thoroughly before applying.")


EOF
        mv "$risky".temp "$risky"

        if [ "$gf_warn_risky_stderr" -eq 1 ]; then
            cat "$risky" 1>&2
            true > "$risky"
        fi
    fi

    if [ -n "$(ls -A "$outdir")" ]; then
        cat "$outdir"/* | sh -c "$(git_conf_get pager)"
    fi
)

# }}}1

usage () {
    cat << EOF
usage: git fmt-diff [<options>] [<commit>] [--] [<path>...]
   or: git fmt-diff [<options>] --cached [<commit>] [--] [<path>...]
   or: git fmt-diff [<options>] <commit>...<commit> [--] [<path>...]
   or: git fmt-diff [<options>] <blob> <blob>

options:
  -h                   display this help and exit
  --cached             view the changes you staged for the next commit relative
                       to the named <commit> (which defaults to HEAD)
  --staged             a synonym of --cached
  --color              always show colors
  --no-color           turn off colored diff
  --fmtprg-stderr      show stderr from formatter programs
  --warn-risky-stderr  redirects warning about potential unsolicited changes in suggestions to stderr;
                       option not recommended as the warning blocks dangerous 'git apply'
  --version            print version of git-$gc_prog_name script
EOF
}

# parse options {{{2

case "$@" in
    --debug)
        if [ -z "$BASH_VERSION" ] && [ -x "$(command -v bash)" ]; then
            exec bash "$0" "$@"
        fi
        ;;
esac

while :; do
    case "$1" in
        -h)
            usage
            exit
            ;;
        --cached|--staged)
            g_cached_opt="--cached"
            ;;
        --debug)
            gf_debug=1
            ;;
        --debug=*)
            gf_debug="${1#--debug=}"
            ;;
        --color)
            gf_use_color=1
            ;;
        --no-color)
            gf_use_color=0
            ;;
        --fmtprg-stderr)
            g_fmtprg_devnull=""
            ;;
        --warn-risky-stderr)
            gf_warn_risky_stderr=1
            ;;
        --version)
            echo "$gc_version"
            exit
            ;;
        --)
            shift
            break
            ;;
        -?*)
            warn "unknown option: $1"
            usage
            exit 1
            ;;
        *)
            break
            ;;
    esac

    shift
done

for o in "$@"; do
    case "$o" in
        -?*)
            warn "option '$o' must come before non-option arguments"
            return 1
            ;;
    esac
done

[ -t 1 ] && gf_no_term=0 || gf_no_term=1
[ -z "$gf_warn_risky_stderr" ] && gf_warn_risky_stderr=0
[ -z "$gf_use_color" ] && [ "$gf_no_term" = 0 ] && gf_use_color=1
[ -z "$gf_use_color" ] && gf_use_color=0

repo_root="$(git rev-parse --show-toplevel 2> /dev/null)"

# setup env {{{2
TMPDIR="${TMPDIR:-/tmp}"/git-"$gc_prog_name"
mkdir -p "$TMPDIR"
g_rm_list="$(mk_tmp --suffix '.rm.list')"
rm_list__push "$TMPDIR"
rm_list__push "$g_rm_list"

# something for Windows... forgot what exactly...
pwd -W > /dev/null 2>&1 && TMPDIR="$(cd "$TMPDIR" && pwd -W)"

export LESS="R$LESS"

trap 'cleanup' EXIT
# }}}2

processing "$@"