# -*- mode: sh; sh-basic-offset: 4; indent-tabs-mode: nil -*-
# ex: ft=sh ts=4 sw=4 et
# shellcheck disable=2034,2154

# Completion script for hledger.
# Created using a Makefile and real hledger.

# This script is sourced by an interactive shell, so do NOT do things like
# 'set -o pipefail' or mangle the global environment in any other way!
# That said, we *do* remove colon (:) from COMP_WORDBREAKS which impacts
# the rest of the session and completion for other programs.

# INSTALLATION:
# To install you can simply source this file from your shell's startup files.
#
# Alternatively, copy/symlink it into `${BASH_COMPLETION_USER_DIR}/completions`
# or `${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions`, rename
# it to either `hledger`, `_hledger` or `hledger.bash`, and it will be loaded
# dynamically the first time you use the `hledger` command. Optionally, create
# symlinks to this file for any extensions used e.g.:
#
# mkdir -p "${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion}/completions" &&
# cd "${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion}/completions" &&
# cp /path/to/hledger-completion.bash hledger &&
# ln -s hledger hledger-ui &&
# ln -s hledger hledger-web &&
# : done.


_hledger_completion() {
    local cur prev words cword
    _init_completion -n : || return 0

    # Current treatment for special characters:
    # - exclude colon (:) from COMP_WORDBREAKS
    # - option processing assumes that `=` is in COMP_WORDBREAKS
    # - use compopt -o filenames selectively to escape the rest
    COMP_WORDBREAKS=${COMP_WORDBREAKS//:}
    case $COMP_WORDBREAKS in
        *=*) : ;;
        *)   COMP_WORDBREAKS=$COMP_WORDBREAKS= ;;
    esac

    local subcommand
    local subcommandOptions
    local i
    for ((i=1; i<${#words[@]}; i++)); do
        subcommand=${words[i]}
        if ! grep -Fxqe "$subcommand" <<< "$_hledger_complist_commands"; then
            subcommand=
            continue
        fi
        # There could be other commands begining with $subcommand, e.g.:
        # $subcommand == reg --> register, register-match,
        # $subcommand == bal --> balance, balancesheet, balancesheetequity, etc.
        # Do not ignore them!
        if ((i == cword)); then
            _hledger_compreply "$(
                _hledger_compgen "$_hledger_complist_commands"
            )"
            return 0
        fi

        # Replace dashes with underscores and use indirect expansion
        subcommandOptions=_hledger_complist_options_${subcommand//-/_}

        if [[ $cur == -* ]]; then
            _hledger_compreply "$(_hledger_compgen "${!subcommandOptions}")"
            # Suspend space on completion of long options requiring an argument
            [[ ${COMPREPLY[0]} == --*= ]] && compopt -o nospace

            return 0
        fi
        break
    done

    # Option argument completion
    _hledger_compreply_optarg && return

    if [[ -z $subcommand ]]; then
        if [[ $cur == -* ]]; then
            _hledger_compreply "$(
                _hledger_compgen "$_hledger_complist_generic_options"
            )"
            # Suspend space on completion of long options requiring an argument
            [[ ${COMPREPLY[0]} == --*= ]] && compopt -o nospace
        else
            _hledger_compreply "$(
                _hledger_compgen "$_hledger_complist_commands"
            )"
        fi

        return 0
    fi

    # Set this from here on because queries tend to have lots of special chars
    # TODO: better handling of special characters
    compopt -o filenames

    # Query completion
    _hledger_compreply_query && return

    # Subcommand specific
    case $subcommand in
        # These do not expect or support any query arguments
        commodities|check|files|help|import|print-unique|test)
            return 0
            ;;
    esac

    # Offer query filters and accounts for the rest
    _hledger_compreply "$(_hledger_compgen "$_hledger_complist_query_filters")"
    if [[ -z $cur ]]; then
        _hledger_compreply_append "$(
            _hledger_compgen "$(_hledger accounts --flat --depth 1)"
        )"
    else
        _hledger_compreply_append "$(
            _hledger_compgen "$(_hledger accounts --flat)"
        )"
    fi

    # Suspend space on completion of query prefix
    # Do not sort, keep accounts and query filters grouped separately
    [[ ${COMPREPLY[0]} == *: ]] && compopt -o nospace
    compopt -o nosort

    return 0
}

_hledger_extension_completion() {
    local cmd=${1##*/}
    local ext=${cmd#hledger-}
    # Pretend that hledger is called with the given extension
    # as the first argument and call main completion function
    COMP_WORDS=("hledger" "$ext" "${COMP_WORDS[@]:1}")
    COMP_CWORD=$((COMP_CWORD + 1))
    _hledger_completion "hledger" "${@:1}"
}

# Register completion function for hledger:
complete -F _hledger_completion hledger

# Register completion functions for hledger extensions:
complete -F _hledger_extension_completion hledger-ui hledger-web

# Helpers

# Comment out when done
_hledger_debug() {
    ((HLEDGER_DEBUG)) || return 0
    local var vars=(words)
    (($#)) && vars=("$@")
    for var in "${vars[@]}"; do
        printf '\ndebug: %s\n' "$(declare -p "$var")" >&2
    done
}

# Stolen from bash-completion
# This function quotes the argument in a way so that readline dequoting
# results in the original argument.  This is necessary for at least
# `compgen' which requires its arguments quoted/escaped:
_hledger_quote_by_ref()
{
    printf -v "$2" %q "$1"

    # If result becomes quoted like this: $'string', re-evaluate in order to
    # drop the additional quoting.  See also: http://www.mail-archive.com/
    # bash-completion-devel@lists.alioth.debian.org/msg01942.html
    [[ ${!2} == \$* ]] && eval "$2=${!2}"
}

# Set the value of COMPREPLY from newline delimited completion candidates
_hledger_compreply() {
    local IFS=$'\n'
    # shellcheck disable=2206
    COMPREPLY=($1)
}

# Append the value of COMPREPLY from newline delimited completion candidates
_hledger_compreply_append() {
    local IFS=$'\n'
    # shellcheck disable=2206
    COMPREPLY+=($1)
}

# Generate input suitable for _hledger_compreply() from newline delimited
# completion candidates. It doesn't seem there is a way to feed a literal
# word list to compgen -- it will eat your quotes, drink your booze and...
# Completion candidates are quoted accordingly first and then we leave it to
# compgen to deal with readline.
#
# Arguments:
# $1: a newline separated list with completion cadidates
# $2: (optional) a prefix string to add to generated completions
# $3: (optional) a word to match instead of $cur, the default.
# If $match is null and $prefix is defined the match is done against $cur
# stripped of $prefix. If both $prefix and $match are null we match against
# $cur and no prefix is added to completions.
_hledger_compgen() {
    local complist=$1
    local prefix=$2
    local match=$3
    local quoted=()
    local word
    local i=0

    while IFS= read -r word; do
        _hledger_quote_by_ref "$word" word
        quoted[i++]=$word
    done <<< "$complist"

    if (($# < 3)); then
        match=${cur:${#prefix}}
    fi

    local IFS=$'\n'
    compgen -P "$prefix" -W "${quoted[*]}" -- "$match"
}

# Try required option argument completion. Set COMPREPLY and return 0 on
# success, 1 if option doesn't require an argument or out of context
_hledger_compreply_optarg() {
    local option=${words[cword - 1]}
    local match=$cur
    local wordlist

    # Match the empty string on --file=<TAB>, not the equal sign itself
    if [[ $cur == = ]]; then
        match=""
    # Once input is present, cword is incremented so we compensate
    elif [[ $prev == = ]]; then
        option=${words[cword - 2]}
    fi

    [[ $option == -* ]] || return

    case $option in
        --alias)
            compopt -o nospace -o filenames
            _hledger_compreply "$(
                _hledger_compgen "$(_hledger accounts --flat)" "" "$match"
            )"
            ;;
        -f|--file|--rules-file|-o|--output-file)
            compopt -o filenames
            _hledger_compreply "$(compgen -f -- "$match")"
            ;;
        --pivot)
            compopt -o nosort
            wordlist="code description note payee"
            _hledger_compreply "$(compgen -W "$wordlist" -- "$match")"
            _hledger_compreply_append "$(
                _hledger_compgen "$(_hledger tags)" "" "$match"
            )"
            ;;
        --value)
            wordlist="cost then end now"
            _hledger_compreply "$(compgen -W "$wordlist" -- "$match")"
            ;;
        -X|--exchange)
            _hledger_compreply "$(
                _hledger_compgen "$(_hledger commodities)" "" "$match"
            )"
            ;;
        --color|--colour)
            compopt -o nosort
            wordlist="auto always yes never no"
            _hledger_compreply "$(compgen -W "$wordlist" -- "$match")"
            ;;
        -O|--output-format)
            wordlist="txt csv json sql"
            _hledger_compreply "$(compgen -W "$wordlist" -- "$match")"
            ;;
        --close-acct|--open-acct)
            compopt -o filenames
            _hledger_compreply "$(
                _hledger_compgen "$(_hledger accounts --flat)" "" "$match"
            )"
            ;;
        --debug)
            wordlist="{1..9}"
            _hledger_compreply "$(compgen -W "$wordlist" -- "$match")"
            ;;
        # Argument required, but no handler (yet)
        -b|-e|-p)
            _hledger_compreply ""
            ;;
        # Check if an unhandled long option requires an argument
        *)
            local optionList argRequired

            if [[ -n $subcommandOptions ]]; then
                optionList=${!subcommandOptions}
            else
                optionList=$_hledger_complist_generic_options
            fi

            while IFS= read -r argRequired; do
                if [[ $argRequired == "$option=" ]]; then
                    _hledger_compreply ""
                    return 0
                fi
            done <<< "$optionList"

            return 1
            ;;
    esac

    return 0
}

# Query filter completion through introspection
_hledger_compreply_query() {
    [[ $cur =~ .: ]] || return
    local query=${cur%%:*}:
    local match=${cur#*:}
    grep -Fxqe "$query" <<< "$_hledger_complist_query_filters" || return

    local hledgerArgs=()
    case $query in
        acct:)
            if (( ${#match} )); then
                hledgerArgs=(accounts --flat)
            else
                hledgerArgs=(accounts --flat --depth 1)
            fi
            ;;
        code:)  hledgerArgs=(codes) ;;
        cur:)   hledgerArgs=(commodities) ;;
        desc:)  hledgerArgs=(descriptions) ;;
        note:)  hledgerArgs=(notes) ;;
        payee:) hledgerArgs=(payees) ;;
        tag:)   hledgerArgs=(tags) ;;
        *)
            local wordlist
            case $query in
                amt:)    wordlist="< <= > >=" ;;
                real:)   wordlist="\  0" ;;
                status:) wordlist="\  * !" ;;
                *)       return 1 ;;
            esac
            _hledger_compreply "$(
                compgen -P "$query" -W "$wordlist" -- "$match"
            )"
            return 0
            ;;
    esac

    _hledger_compreply "$(
        _hledger_compgen "$(_hledger "${hledgerArgs[@]}")" "$query"
    )"

    return 0
}

# Parse the command line so far and fill the array $optarg with the arguments to
# given options. $optarg should be declared by the caller
_hledger_optarg() {
    local options=("$@")
    local i j offset
    optarg=()

    # hledger balance --file ~/ledger _
    # 0       1       2      3        4
    for ((i=1; i < ${#words[@]} - 2; i++)); do
        offset=0
        for j in "${!options[@]}"; do
            if [[ ${words[i]} == "${options[j]}" ]]; then
                if [[ ${words[i+1]} == '=' ]]; then
                    offset=2
                else
                    offset=1
                fi
                # Pass it through compgen to unescape it
                optarg+=("$(compgen -W "${words[i + offset]}")")
            fi
        done
        ((i += offset))
    done
}

# Get ledger file from -f --file arguments from COMP_WORDS and pass it to the
# 'hledger' call. Note that --rules-file - if present - must also be passed!
# Multiple files are allowed so pass them all in the order of appearance.
_hledger() {
    local hledgerArgs=("$@")
    local file
    local -a optarg

    _hledger_optarg -f --file
    for file in "${optarg[@]}"; do
        [[ -f $file ]] && hledgerArgs+=(--file "$file")
    done

    _hledger_optarg --rules-file
    for file in "${optarg[@]}"; do
        [[ -f $file ]] && hledgerArgs+=(--rules-file "$file")
    done

    # Discard errors. Is there a way to validate files before using them?
    hledger "${hledgerArgs[@]}" 2>/dev/null
}
