# bash completion for GNU make

# Extract the valid target names starting with PREFIX from the output of
# `make -npq'
# @param mode    If this is `-d', the directory names already specified in
#                PREFIX are omitted in the output
# @param prefix  Prefix of the target names
_comp_cmd_make__extract_targets()
{
    local mode=$1
    local -x prefix=$2

    # display mode, only output current path component to the next slash
    local -x prefix_replace=$prefix
    [[ $mode == -d && $prefix == */* ]] &&
        prefix_replace=${prefix##*/}

    _comp_awk -f "${BASH_SOURCE[0]%/*}/../helpers-core/make-extract-targets.awk"
}

# Truncate the non-unique filepaths in COMPREPLY to only generate unique
# directories or files.  This function discards the files under subdirectories
# unless the path is unique under each subdirectory and instead generate the
# subdirectory path.  For example, when there are two candidates, "abc/def" and
# "abc/xyz", we generate "abc/" instead of generating both candidates directly.
# When there is only one candidate "abc/def", we generate the full path
# "abc/def".
#
# @var[in] cur
# @var[in] mode
# @var[in,out] COMPREPLY
_comp_cmd_make__truncate_non_unique_paths()
{
    local prefix=$cur
    [[ $mode == -d ]] && prefix=
    if ((${#COMPREPLY[@]} > 0)); then
        # collect the possible completions including the directory names in
        # `paths' and count the number of children of each subdirectory in
        # `nchild'.
        local -A paths nchild
        local target
        for target in "${COMPREPLY[@]}"; do
            local path=${target%/}
            while [[ ! ${paths[$path]+set} ]] &&
                paths[$path]=set &&
                [[ $path == "$prefix"*/* ]]; do
                path=${path%/*}
                nchild[$path]=$((${nchild[$path]-0} + 1))
            done
        done

        COMPREPLY=()
        local nreply=0
        for target in "${!paths[@]}"; do
            # generate only the paths that do not have a unique child and whose
            # all parent and ancestor directories have a unique child.
            ((${nchild[$target]-0} == 1)) && continue
            local path=$target
            while [[ $path == "$prefix"*/* ]]; do
                path=${path%/*}
                ((${nchild[$path]-0} == 1)) || continue 2
            done

            # suffix `/' when the target path is a subdiretory, which has
            # at least one child.
            COMPREPLY[nreply++]=$target${nchild[$target]+/}
        done
    fi
}

_comp_cmd_make()
{
    local cur prev words cword was_split comp_args
    _comp_initialize -s -- "$@" || return

    local makef makef_dir=("-C" ".") i

    local noargopts='!(-*|*[foWICmEDVxj]*)'
    # shellcheck disable=SC2254
    case $prev in
        --file | --makefile | --old-file | --assume-old | --what-if | --new-file | \
            --assume-new | -${noargopts}[foW])
            _comp_compgen_filedir
            return
            ;;
        --include-dir | --directory | -${noargopts}[ICm])
            _comp_compgen_filedir -d
            return
            ;;
        -${noargopts}E)
            _comp_compgen -- -v
            return
            ;;
        --eval | -${noargopts}[DVx])
            return
            ;;
        --jobs | -${noargopts}j)
            local REPLY
            _comp_get_ncpus
            _comp_compgen -- -W "{1..$((REPLY * 2))}"
            return
            ;;
    esac

    [[ $was_split ]] && return

    if [[ $cur == -* ]]; then
        _comp_compgen_help || _comp_compgen_usage
        [[ ${COMPREPLY-} == *= ]] && compopt -o nospace
    elif [[ $cur == *=* ]]; then
        prev=${cur%%=*}
        cur=${cur#*=}
        local diropt=""
        [[ ${prev,,} == *dir?(ectory) ]] && diropt=-d
        _comp_compgen_filedir ${diropt:+"$diropt"}
    else
        # before we check for makefiles, see if a path was specified
        # with -C/--directory
        for ((i = 1; i < ${#words[@]}; i++)); do
            if [[ ${words[i]} == @(-${noargopts}C|--directory) ]]; then
                # Expand tilde expansion
                local REPLY
                _comp_dequote "${words[i + 1]-}" &&
                    [[ -d $REPLY ]] &&
                    makef_dir=(-C "$REPLY")
                break
            fi
        done

        # before we scan for targets, see if a Makefile name was
        # specified with -f/--file/--makefile
        for ((i = 1; i < ${#words[@]}; i++)); do
            if [[ ${words[i]} == @(-${noargopts}f|--?(make)file) ]]; then
                # Expand tilde expansion
                local REPLY
                _comp_dequote "${words[i + 1]-}" &&
                    [[ -f $REPLY ]] &&
                    makef=(-f "$REPLY")
                break
            fi
        done

        # recognise that possible completions are only going to be displayed so
        # only the base name is shown.
        #
        # Note: This is currently turned off because the test suite of
        # bash-completion conflicts with it; it uses "set show-all-if-ambiguous
        # on" (causing COMP_TYPE == 37) to retrieve the action completion
        # results, and also the compact form with only the basenames is not
        # essentially needed.  To re-enable it, please uncomment the following
        # if-statement.
        local mode=--
        # if ((COMP_TYPE != 9 && COMP_TYPE != 37 && COMP_TYPE != 42)); then
        #     mode=-d # display-only mode
        # fi

        _comp_split COMPREPLY "$(LC_ALL=C \
            $1 -npq __BASH_MAKE_COMPLETION__=1 \
            ${makef+"${makef[@]}"} "${makef_dir[@]}" .DEFAULT 2>/dev/null |
            _comp_cmd_make__extract_targets "$mode" "$cur")"

        _comp_cmd_make__truncate_non_unique_paths

        if [[ $mode != -d ]]; then
            # Completion will occur if there is only one suggestion
            # so set options for completion based on the first one
            [[ ${COMPREPLY-} == */ ]] && compopt -o nospace
        fi

    fi
} &&
    complete -F _comp_cmd_make make gmake gnumake pmake colormake bmake
