#!/bin/bash

##
## binaryDiv's dotfiles management script
## Version 0.5.3
##

usage() {
    cat << END_OF_HELPTEXT
    Usage: $(basename $0) COMMAND [...]
    Manage dotfiles via git.

    General commands:
      help          print this help message

    Installation:
      install       run this on first start: install dotfiles script in ~/bin, configure git, ...
                    (this will NOT create links for all dotfiles, though)

    Dotfile management:
      ls [DIR]            list contents of .dotfiles directory (or DIR relative to it)
                          (-l: list host-specific files, i.e. ~/.dotfiles/_local/HOSTNAME/DIR)
      edit FILE           open ~/.dotfiles/FILE in vim
                          (-l: open host-specific files, i.e. ~/.dotfiles/_local/HOSTNAME/DIR)
      diff FILE           use vimdiff to compare ~/FILE to ~/.dotfiles/FILE
                          (-l: compare host-specific files, i.e. ~/.dotfiles/_local/HOSTNAME/DIR)
      link FILE...        create a symlink from ~/FILE to ~/.dotfiles/FILE (multiple files possible)
                          (-q: be quiet, only print something if an error occurs)
                          (-l: link host-specific files, i.e. ~/FILE to ~/.dotfiles/_local/HOSTNAME/FILE)
      linkstatus [DIR]    list contents of .dotfiles with information about whether there are links to
                          them in ~/, whether they differ, etc...
                          (-l: show linkstatus only for host-specific files, i.e. ~/.dotfiles/_local/HOSTNAME)
      createfrom FILE...  copy ~/FILE to ~/.dotfiles/FILE and symlink the file
                          (-n: only copy, leave the original, do not create symlink)
                          (-l: create host-specific files, i.e. ~/FILE to ~/.dotfiles/_local/HOSTNAME/FILE)

    Synchronization:
      git ARGS...   wrapper for git (all arguments are passed on to git)
      pull          shortcut for git pull
END_OF_HELPTEXT
}


# Define root dir for dotfiles management (defaults to $HOME, can be overridden
# with environment variables for debugging)
if [[ -z $DOTFILESROOT ]]; then
    DOTFILESROOT="$HOME"
fi

# Set variables
homedir="$DOTFILESROOT"
dotfilesdir="$DOTFILESROOT/.dotfiles"

dotfiles_local_base="_local"
dotfiles_local_prefix="$dotfiles_local_base/$(hostname)"


# Get command
[[ $# -gt 0 ]] || { usage; exit 1; }
cmd=$1
shift

# Parse command
case "$cmd" in
    help|--help|-h)
        usage
        exit 1
    ;;

    install)
        bin_source="$dotfilesdir/bin/dotfiles"
        bin_target="$homedir/bin/dotfiles"
        bin_dir="$homedir/bin"

        if [[ ! -e $bin_target ]]; then
            echo "* Installing dotfiles script in $bin_dir/"
            mkdir -pv "$bin_dir"
            ln -sr "$bin_source" "$bin_target" || exit 1
        elif [[ ! "$(realpath $bin_target)" -ef "$bin_source" ]]; then
            echo "! Warning: File $bin_target already exists but is NOT a link to $bin_source"
        fi

        if [[ ":$PATH:" != *":$bin_dir:"* ]]; then
            echo "! Warning: $bin_dir is not in your PATH."
        fi

        show_bash_restart_info=

        # Enable user-defined bash completion per directory
        bashcompletion_filename=.bash_completion
        if [[ ! -e "$homedir/$bashcompletion_filename" ]] ||
           [[ ! "$(realpath "$homedir/$bashcompletion_filename")" -ef "$dotfilesdir/$bashcompletion_filename" ]];
        then
            echo "* Installing user-defined bash completion (~/.bash_completion.d/)"
            "$0" link -q "$bashcompletion_filename" &&
                show_bash_restart_info=1
        fi
        mkdir -p "$homedir/.bash_completion.d"

        # Activate dotfiles bash completion
        bashcompletion_dotfiles_filename=.bash_completion.d/10-dotfiles.sh
        if [[ ! -e "$homedir/$bashcompletion_dotfiles_filename" ]] ||
           [[ ! "$(realpath "$homedir/$bashcompletion_dotfiles_filename")" -ef "$dotfilesdir/$bashcompletion_dotfiles_filename" ]];
        then
            echo "* Installing bash completion for dotfiles script"
            "$0" link -q "$bashcompletion_dotfiles_filename" &&
                show_bash_restart_info=1
        fi

        if [[ -n $show_bash_restart_info ]]; then
            echo "  Restart bash or run 'source $homedir/.bash_completion' to activate it now"
        fi

        echo "* Configuring git settings"
        git -C "$dotfilesdir" config merge.ff only
    ;;

    ls)
        lsdir="$dotfilesdir"
        [[ $1 = "-l" ]] && { lsdir="$lsdir/$dotfiles_local_prefix"; shift; }
        [[ -n $1 ]] && lsdir="$lsdir/$1"

        echo "$lsdir:"
        ls -lAh --color=auto "$lsdir"
    ;;

    linkstatus)
        cd "$dotfilesdir"

        subdir="$1"
        [[ $subdir = "-l" ]] && { subdir="$dotfiles_local_prefix/$1"; shift; }

        if [[ -n $subdir ]]; then
            subdir="${subdir%/}"
            [[ $subdir = $dotfiles_local_base ]] && subdir=$dotfiles_local_prefix

            [[ -e $subdir ]] || { echo "$dotfilesdir/$subdir does not exist"; exit 1; }
            [[ -d $subdir ]] && subdir="$subdir/"
        fi

        shopt -s nullglob dotglob
        dotdirs=()

        for file in $subdir*; do
            [[ -e $file ]] || continue
            [[ $file = ".git" ]] && continue

            file_home="$homedir/$file"
            file_dotfiles="$dotfilesdir/$file"

            if [[ $subdir == $dotfiles_local_base/* ]]; then
                file_home="$homedir/${file#$dotfiles_local_prefix/}"
            fi

            if [[ -e $file_home ]]; then
                if [[ "$(realpath "$file_home")" -ef "$file_dotfiles" ]]; then
                    file_status="    LINKED  "
                elif [[ -d $file_home ]]; then
                    dotdirs+=($file)
                    continue
                elif [[ -f $file_home ]] && cmp --silent "$file_home" "$file_dotfiles"; then
                    file_status="==  EQUAL   "
                else
                    file_status="<>  DIFFERS "
                fi
            elif [[ $file = $dotfiles_local_base ]]; then
                dotdirs=("$dotfiles_local_prefix" "${dotdirs[@]}")
                continue
            else
                if [[ -d $file_dotfiles ]]; then
                    file_status="++  NEW DIR "
                else
                    file_status="++  NEW FILE"
                fi
            fi

            [[ -d $file_dotfiles ]] && file="$file/"
            echo "$file_status    $file"
        done

        if [[ $dotdirs ]]; then
            echo
            for dir in ${dotdirs[@]}; do
                echo "[$dir]"
                dotfiles linkstatus "$dir" | grep '\(LINKED\|EQUAL\|DIFFERS\|NEW\)'
                echo
            done
        fi
    ;;

    edit)
        basedir=$dotfilesdir
        [[ $1 = "-l" ]] && { basedir="$basedir/$dotfiles_local_prefix"; shift; }
        [[ -n $1 ]] || { echo "$(basename $0) $cmd: Missing argument."; exit 1; }
        vim "$basedir/$1"
    ;;

    diff)
        basedir=$dotfilesdir
        [[ $1 = "-l" ]] && { basedir="$basedir/$dotfiles_local_prefix"; shift; }
        [[ -n $1 ]] || { echo "$(basename $0) $cmd: Missing argument."; exit 1; }
        file_A="$homedir/$1"
        file_B="$basedir/$1"

        if [[ "$(realpath "$file_A")" -ef "$file_B" ]]; then
            echo "$file_A is a link to $file_B"
        else
            vimdiff "$file_A" "$file_B"
        fi
    ;;

    link)
        while getopts ":ql" opt; do
            case $opt in
                q) QUIET=1 ;;
                l) link_local=1 ;;
                \?)
                    echo "Invalid option: -$OPTARG"
                    exit 1
                    ;;
            esac
        done
        shift $((OPTIND-1))

        [[ -n $1 ]] || { echo "$(basename $0) $cmd: Missing argument."; exit 1; }

        for filename in "$@"; do
            file_home="$homedir/$filename"
            file_dotfiles="$dotfilesdir/$filename"

            if [[ $link_local ]]; then
                file_dotfiles="$dotfilesdir/$dotfiles_local_prefix/$filename"
            fi

            if [[ ! -e $file_dotfiles ]]; then
                echo "! $file_dotfiles: target file does not exist"
                exit 1
            fi

            if [[ -e $file_home ]]; then
                if [[ "$(realpath "$file_home")" -ef "$file_dotfiles" ]]; then
                    # Target is already a link to the correct file
                    continue
                fi

                if cmp --silent "$file_home" "$file_dotfiles"; then
                    echo "! $file_home already exists and is equal to $file_dotfiles"
                    read -p "  Delete $file_home and create link? [y/N] " -r
                    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
                        # Don't delete file
                        continue
                    fi
                    rm "$file_home" || exit 1
                else
                    echo "! $file_home already exists but is NOT a link to $file_dotfiles, please fix manually"
                    exit 1
                fi
            fi

            [[ $QUIET ]] || echo "* Creating link $file_home -> $file_dotfiles"
            mkdir -p "$(dirname "$file_home")" || exit 1
            ln -sr "$file_dotfiles" "$file_home" || exit 1
        done
    ;;

    createfrom)
        while getopts ":nl" opt; do
            case $opt in
                n) NOLINK=1 ;;
                l) create_local=1 ;;
                \?)
                    echo "Invalid option: -$OPTARG"
                    exit 1
                    ;;
            esac
        done
        shift $((OPTIND-1))

        [[ -n $1 ]] || { echo "$(basename $0) $cmd: Missing argument."; exit 1; }

        for filename in "$@"; do
            file_home="$homedir/$filename"
            file_dotfiles="$dotfilesdir/$filename"

            if [[ $create_local ]]; then
                file_dotfiles="$dotfilesdir/$dotfiles_local_prefix/$filename"
            fi

            if [[ ! -e $file_home ]]; then
                echo "! $file_home: source file does not exist"
                exit 1
            elif [[ -e $file_dotfiles ]]; then
                echo "! $file_dotfiles already exists"
                exit 1
            else
                mkdir -p "$(dirname "$file_dotfiles")"
                [[ $QUIET ]] || echo "* Copy $file_home -> $file_dotfiles"
                cp -n "$file_home" "$file_dotfiles" || exit 1
                if [[ ! $NOLINK ]]; then
                    [[ $QUIET ]] || echo "* Remove original and link $file_home -> $file_dotfiles"
                    rm "$file_home" || exit 1
                    ln -sr "$file_dotfiles" "$file_home" || exit 1
                fi
            fi
        done
    ;;

    git)
        git -C "$dotfilesdir" "$@"
    ;;

    pull)
        git -C "$dotfilesdir" pull "$@"
    ;;

    bash-completion)
        cat << 'END_OF_BASHCOMPLETION'
        _dotfiles_filenames() {
            compopt -o filenames
            files=($(compgen -f -X '*/@(.|..|.git)' .dotfiles/$cur))
            COMPREPLY=(${files[@]#.dotfiles/})
        }
        _dotfiles_filenames_local() {
            compopt -o filenames
            dotfiles_prefix=.dotfiles/_local/$(hostname)
            files=($(compgen -f -X '*/@(.|..|.git)' $dotfiles_prefix/$cur))
            COMPREPLY=(${files[@]#$dotfiles_prefix/})
        }
        _dotfiles() {
            COMPREPLY=()
            local cur="${COMP_WORDS[COMP_CWORD]}"
            local commands="help install ls edit diff link linkstatus createfrom git pull"
            local gitcommands="add commit diff fetch log pull push rebase status"

            if [[ $COMP_CWORD -eq 1 ]]; then
                COMPREPLY=($(compgen -W "$commands" -- $cur))
            else
                case "${COMP_WORDS[1]}" in
                    ls|edit|diff)
                        [[ $COMP_CWORD -eq 2 ]] && _dotfiles_filenames
                    ;;

                    link|linkstatus)
                        if [[ $COMP_CWORD -gt 2 && ${COMP_WORDS[2]} = "-l" ]]; then
                            _dotfiles_filenames_local
                        else
                            _dotfiles_filenames
                        fi
                    ;;

                    createfrom)
                        compopt -o filenames
                        COMPREPLY=($(compgen -f -- ${cur}))
                    ;;

                    git)
                        if [[ $COMP_CWORD -eq 2 ]]; then
                            COMPREPLY=($(compgen -W "$gitcommands" -- ${cur}))
                        else
                            _dotfiles_filenames
                        fi
                    ;;
                esac
            fi
        }

        complete -F _dotfiles dotfiles
END_OF_BASHCOMPLETION
    ;;

    *)
        echo "Unknown command '$cmd', use 'help' to show all commands."
        exit 1
    ;;
esac

