#!/bin/zsh -feu

source /usr/share/mmc-shell/git-functions.sh
source /usr/share/mmc-shell/mmc-functions.sh
colors

trap 'print ${PROGRAM-$0} ERROR: $LINENO:  $ZSH_EVAL_CONTEXT $0 >&2' ZERR

# So 2 steps:
# Not implemented:
# switch (maybe in worktree) to local branch
# fetch --- either pull or ...
#

usage() {
    setopt POSIX_ARGZERO
    cat <<EOF
usage: ${0##*/} [-h] [-f] [-l] [-u] [--] [remote | {remote} {local branch}]

Fast-forward to remote head, stashing around.
* if no remote --> just from the __followed__ branch
* if remote --> same branch (name) on remote
* if even local-branch given -> switch to it first.

-f not only fast-forward !
-b rebase.
-l list
-u DO NOT fetch/update first
-v verbose

-h help
By default {branch} is the current one on the favorite remote.

EOF
    unsetopt POSIX_ARGZERO
}

force=n # act even if not descendant
rebase=n # if not descendant -> rebase
list_difference=no  # show the git log between.

fetch_too=yes # start with git fetch ... from which remote?
verbose=no

while getopts :bfhluv OPT; do
    case $OPT in
        h|+h)
            usage
            exit 0
            ;;
        b)
            rebase=y
            ;;
        f)
            force=y
            ;;
        l)
            list_difference=yes
            ;;
        u)
            fetch_too=no
            ;;
        v)
            verbose=yes
            ;;
        *)
            usage >&2
            exit 2
    esac
done
shift OPTIND-1
OPTIND=1

if [[ $# -gt 2 ]]; then
    usage >&2
    exit 2
fi

# find where to
# I need:
# - remote host,
# - local branch, and
# - the remote branch!

# use-cases:
# I'm on a branch X and want Y to move to H/Y
#                   and want to move to its  ---
#                   and want to move to H/X
#
# $1 is local branch -> either move to its or ...
# $1 is remote or remote-branch

# So I need to set 3 variables:
# local branch, remote host, and remote branch

# local_branch
# GIT_REMOTE_NAME
# target
#    remotes/GIT_REMOTE_NAME/target


# for $1
# sets GIT_REMOTE_NAME and `target'
find_defaults_for_local_branch()
{
    local local_branch=$1
    local remote
    local remote_branch

    if git config branch.${local_branch}.remote >&/dev/null
    then
        remote=$(git config branch.$local_branch.remote)
        GIT_REMOTE_NAME=$remote

        remote_branch=$(git config branch.$local_branch.merge)
        #drop refs/heads
        target=remotes/$remote/${remote_branch#refs/heads/}
        if [[ $verbose = yes ]]; then
            cat <<EOF
For the $local_branch we have config branch.$local_branch.remote = $remote
and the branch is: $remote_branch
so the target is $target
EOF
        fi
    else
        echo "the branch $fg_bold[yellow]$local_branch$reset_color does not follow anything on remotes">&2
        return 1
    fi
}

# compute:
#  local_branch, `target', GIT_REMOTE_NAME
if [[ $# = 0 ]]
then
    local_branch=$(git_current_branch_name)
    find_defaults_for_local_branch $local_branch

elif [[ $# = 1 ]]; then
    # fixme:  branch, remote,
    if git-branch-exists $1
    then
        local_branch=$1
        find_defaults_for_local_branch $local_branch

    elif is_git_remote $1
    then
        # host/branch
        GIT_REMOTE_NAME=$1
        # implicitly.... so we must be on a branch!
        local_branch=$(git_current_branch_name)
        target=remotes/$GIT_REMOTE_NAME/$local_branch
    ## fixme: useful?  elif is_git_remote_branch
        # target=$1
        # local_branch=$(git_current_branch_name)
    else
        die "\`$1' is neither remote, nor local branch"
    fi
    shift

elif [[ $# = 2 ]]  # [remote | remote/branch] [branch]
then
    if is_git_remote $1 &>/dev/null; then
        GIT_REMOTE_NAME=$1
        # todo: ask git wh
        target=remotes/$1/$2
        local_branch=$2
    elif is_git_remote_branch $1; then
        # so is_git_remote_branch is useless?
        # but remote/branch is valid!
        # maybe `is_git_remote_branch' should return the expanded ref.
        target=remotes/${1#remotes/}
        # why not simply:
        # GIT_REMOTE_NAME=${{1#remotes/}%/*}
        GIT_REMOTE_NAME=${1#remotes/}
        GIT_REMOTE_NAME=${GIT_REMOTE_NAME%/*}
        local_branch=$2
    else
        die "confused: '$1' is not a remote, nor branch"
    fi
fi




[[ $verbose = yes ]] && info "$local_branch <- $GIT_REMOTE_NAME $target"

GIT_QUIET=()
if [[ $verbose != yes ]]
then
    GIT_QUIET+=(--quiet)
fi

if [[ $fetch_too = yes ]]
then
    (
        if [[ $verbose = yes ]]; then
            set -x
        fi
        # -p, --prune  Before fetching, remove any remote-tracking references that no longer exist on the remote.
        git remote prune $GIT_REMOTE_NAME
        git fetch --prune --progress  --verbose $GIT_REMOTE_NAME ${target#remotes/$GIT_REMOTE_NAME/}
    )
fi

if [[ $list_difference = yes ]]
then
    git log --oneline $local_branch..remotes/$GIT_REMOTE_NAME/$target
    # exit 0
fi

# if the fast-forward would move ahead!
git_ff_to_cautiously()
{
    # given $ref and $force
    local local_branch=$1
    local ref=$2


    [[ $verbose = yes ]] && info "will fast-forward $local_branch to $ref"
    # unless we are on it already:

    if [[ $verbose = yes ]]; then
        set -x
    fi

    # todo: check that the ref exists!
    if ! git rev-parse $ref -- > /dev/null; then
        git branch -a
        die "the remote branch does not exist: $ref"
    fi

    if [[ $verbose = yes ]]; then
        echo "compare $local_branch  $(git rev-parse $local_branch) != $(git rev-parse $ref) $ref">&2
    fi

    if [[ $(git rev-parse $local_branch) != $(git rev-parse $ref) ]]; then

        if [[ $rebase = y ]];
        then
            current_branch=$(git_current_branch_name)
            # todo: switch back!
            # with-stash ...
            mmc_stash_if_non_clean 'stash for git-ff/rebase'
            if [[ $current_branch != $local_branch ]]; then
                git switch $local_branch
            fi

            # automatically squash  !fixup commits:
            info "rebase & unstash:"
            git rebase --autosquash $ref

            # return:
            git switch -- # git switch $previous_branch
            mmc_unstash_if_stashed
            info "done: rebased & unstashed"
        elif [[ $force = y ]]  || git merge-base --is-ancestor $local_branch $ref
        then
            # todo: can you do that without touching the working tree?
            current_branch=$(current_branch_name_maybe)

            if [[ $current_branch != $local_branch ]]
            then
                git branch --force $local_branch $ref
            else
                # the current, so stash!
                if git merge $GIT_QUIET --autostash --ff-only $ref;
                then
                    if [[ $verbose = yes ]]; then
                        info "ff (without conflicts w/ dirty files)"
                    fi
                else
                    # touching timestamps:
                    # mmc_stash_if_non_clean "merge fast-forward"
                    # stash_if_non_clean

                    # Could not Fast-Forward, so only force can help:
                    if [[ $force = y ]]; then
                        info "Forcing reset to $ref"
                        git reset --hard $ref

                    else
                        # this case is impossible?
                        info "Fast-Forward failed"
                        : do-nothing
                        # git checkout $branch
                        # git merge --ff-only $ref
                    fi
                    # mmc_unstash_if_stashed
                fi
            fi
        else
            # todo: if ancestor, print a different message.
            # [[ $verbose = yes ]] &&
            echo >&2 "cannot fast-forward to $ref $fg[red]not our descendant$reset_color"
            exit 1
        fi
    else
        [[ $verbose = yes ]] &&
            echo >&2 "$local_branch and $ref resolve both to " $(git rev-parse $local_branch)
        : ok
    fi
}

git_ff_to_cautiously $local_branch $target

# if not set
if ! git config branch.${local_branch}.remote >&/dev/null
then
    echo "setting upstream: $local_branch -> $target" >&2
    git branch $local_branch --set-upstream-to=$target
fi
