#autoload

if [[ $_NIX_SHELL_COMPLETION_LOADED ]]; then
    # No point in re-defining these functions each time we do a completion
    return 0
fi

# Simple completion function to select a system
# List gathered from: https://github.com/NixOS/nixpkgs/blob/master/lib/platforms.nix
_nix_systems () {
    _values 'Systems' \
        i686-linux x86_64-linux \
        armv5tel-linux armv6l-linux armv7l-linux mips64el-linux \
        x86_64-darwin \
        i686-freebsd x86_64-freebsd \
        i686-openbsd x86_64-openbsd \
        i686-netbsd x86_64-netbsd \
        i686-cygwin x86_64-cygwin
}

# Completion function to select an angle-bracket expression from the nix path
# Assumptions: No '=' in the actual path components in NIX_PATH
# TODO: Complete files in /some/path for expressions like <nixpkgs/pkgs/...>
# IMPROVEMENT: Remove '<nixos-config>' since that seems rather useless(?)
_nix_shortcuts () {
    local nix_path=(${(s.:.)NIX_PATH})
    local named=(${(M)nix_path:#*=*})
    local dirs=(${nix_path:#*=*})
    local valid_dir_globs=($^dirs"/*/default.nix(N:h:t)")
    local valid_dirs=(${~valid_dir_globs})
    local names=(${named%%=*})
    if [[ "$valid_dirs" || "$names" ]]; then
        _values shortcuts "<"${^valid_dirs}">" "<"${^names}">"
    fi
}

_nix_path() {
  _alternative \
    'nixpkgs:Nixpkgs:_nix_shortcuts' \
    'path:File Path:_nix_complete_dotnix_files'
}

_nix_complete_dotnix_files () {
    _alternative \
        "file:Local file:_path_files -g '*.nix(N) *(N-/)'" \
        'shortcuts:Shortcuts:_nix_shortcuts' \
        'channel:Channel:(channel:
            channel:nixos-13.10
            channel:nixos-14.04           channel:nixos-14.04-small
            channel:nixos-14.12           channel:nixos-14.12-small
            channel:nixos-15.09           channel:nixos-15.09-small
            channel:nixos-16.03           channel:nixos-16.03-small
            channel:nixos-16.09           channel:nixos-16.09-small
            channel:nixos-17.03           channel:nixos-17.03-small
            channel:nixos-17.09           channel:nixos-17.09-small
            channel:nixos-18.03           channel:nixos-18.03-small
            channel:nixos-18.09           channel:nixos-18.09-small
            channel:nixos-19.03           channel:nixos-19.03-small
            channel:nixos-19.09           channel:nixos-19.09-small
            channel:nixos-20.03           channel:nixos-20.03-small
            channel:nixos-20.09           channel:nixos-20.09-small
            channel:nixos-21.05           channel:nixos-21.05-small
            channel:nixos-21.11           channel:nixos-21.11-small
            channel:nixos-22.05           channel:nixos-22.05-small
            channel:nixos-22.11           channel:nixos-22.11-small
            channel:nixos-unstable        channel:nixos-unstable-small
            channel:nixpkgs-unstable)' \
        'url:URL:(https:// http://)'
}  # see: https://channels.nixos.org/

_nix_complete_includes () {
    local cur=${words[$CURRENT]}
    local -a nixpath=(${(s.:.)NIX_PATH})
    local -a path_names
    local p
    for p in ${nixpath[*]}; do
        [[ "$p" == *=* ]] && \
            path_names+=(${p%=*}:Path:_nix_complete_dotnix_files)
    done
    if [[ $cur == *=* ]]; then
        path_names+=(${cur%=*}:Path:_nix_complete_dotnix_files)
    fi
    if [[ "$path_names" ]]; then
        _alternative \
            "nixpath:nixpath:_values -s = 'Nix path name' $path_names" \
            'file:Path:_path_files -/'
    else
        _path_files -/
    fi
    return
}

_nix_generations () {
    # List of package names with version numbers stripped
    setopt extendedglob
    local -a generations=(${${${(f)"$(nix-env --list-generations)"}##  #}/  /:})
    _describe -V "Nix Generations" generations
}

_nix_installed_packages () {
    # List of package names with version numbers stripped
    # with the alternative to specify store paths
    local extra_help=$1
    local prefix='-P ./'
    local current_word=$words[$CURRENT]
    # When referencing an absolute path we can't prefix with ./
    if [[ -z ${current_word:##(/*|\~/*)} && -n $current_word ]]; then
        prefix=""
    fi
    local -a packages=(${${(f)"$(nix-env -q)"}%%-[0-9]*})

    _alternative \
        "package:packages:_values 'Installed package $extra_help' $packages" \
        "file:Store path to package:_files ${prefix}"
}


# Generate nix code creating the default expression used by 'nix-env -iA'
_nix_gen_defexpr () {
    setopt local_options null_glob
    local -A result # name -> path map

    # Search depth first for directories containing 'default.nix'
    # nix-env -iA prioritize the user's channels in case of name collision
    # Not sure how collisions in user-created directories are resolved.
    local -aU initialQueue=($1/channels $1/channels_root $1/*)
    local -a queue=($initialQueue)

    while [[ ${#queue} > 0 ]]; do
        local current=$queue[1]
        shift queue
        if [[ -e $current/default.nix ]]; then
            local name=$(basename $current)
            if [[ -z $result[$name] ]]; then
                result[$name]=$current
            fi
        else
            queue=($current/* $queue)
        fi
    done

    local nix_expr="{\n"
    for name expr_path in ${(kv)result}; do
        nix_expr+="${name} = import ${expr_path};"
        nix_expr+="\n"
    done
    nix_expr+="}"

    echo $nix_expr
}


# Complete attribute names using $1 as the toplevel expression NB: If calling
# this function from an action spec you need to prefix it with a space so that
# eg. _arguments won't pass it options which it doesn't expect, ie:
#     `: _nix_attr_paths`
_nix_attr_paths () {

    local cur=${${words[$CURRENT]#(\'|\")}%(\'|\")}

    # Starting with '.' causes _sep_parts to complain, so exit early.
    # This also guards against error output when completion './files' with nix.
    if [[ $cur == .* ]]; then
        return
    fi

    local defexpr=$1
    local attr_path=""
    if [[ $cur == *.* ]]; then
        attr_path=${cur%.*}
    fi

    # attr1.attr3 -> ("attr1" "attr2")
    local -a paths=(${(s,.,)attr_path})
    # Add quotes in a second step to avoid ("") when empty
    paths=(${${paths/%/\"}/#/\"})

    # Auto call any functions in the attribute path. This isn't a language
    # feature, but done by nix when passing attributes on the command line.
    local -a result
    result=($(_nix_eval_stdin <<NIX_FILE
        let
          autocall = setOrLambda:
              if builtins.isFunction setOrLambda then
                 setOrLambda {}
              else
                 setOrLambda;

          top = autocall ($defexpr);
          names = [ ${paths[*]} ];

          # Returns attr.name calling it if it's a function
          reducer = set: name:
              autocall (builtins.getAttr name set);
          result = builtins.foldl' reducer top names; #'
        in
          if builtins.isAttrs result then
            builtins.attrNames result
          else
            ""
NIX_FILE
    ))

    # If the eval failed return the error message
    if [[ $? > 0 ]]; then
        _message "Eval failed, can't complete (an URL might not be cached):
$result"
        return 1
    fi

    local -a prefix=()
    if [[ -n $attr_path ]]; then
        for i in ${=attr_path//./ }; do
            prefix+=("($i)" .)
        done
    fi

    local package=""
    _wanted package package "Attribute path" \
            _sep_parts $prefix result \.
    return $?
}

function _nix_eval_stdin () {
    setopt local_options pipefail
    # Build up a modified NIX_PATH using -I and --include
    local i override=""
    for ((i=1; i < ${#words[*]}; i++)); do
        case "${words[i]}" in
            -I|--include)
                override+=${override:+:}${words[$((i+1))]}
                ;;
        esac
    done
    override+=${override:+:}${NIX_PATH}

    # Resolve channel: syntax
    while [[ "$override" == *(=|:)channel:* ]]; do
        local channel=${override#*channel:}
        channel="channel:"${channel%%:*}
        local url="https://nixos.org/channels/"${channel:8}"/nixexprs.tar.xz"
        # Replace the channel with its url
        override=${override/"$channel"/"$url"}
    done

    # Resolve any url to a cache, else we might trigger a blocking download
    while [[ "$override" == *https://* ]]; do
        # Find the first url
        local url=${override#*https://}
        # Strip everything starting with the first colon
        url="https://"${url%%:*}
        local cache=$(_nix_resolve_url "$url")
        # Replace the url with the cache
        override=${override/"$url"/"$cache"}
    done

    NIX_PATH=$override nix-instantiate --eval - 2>/dev/null | tr '[]"' ' '
    return $?
}


# Generate the top level expression in all the various ways the different
# commands expects it to be built. Then generate completions by calling
# _nix_attr_paths $defexpr
_nix_complete_attr_paths () {

    local defexpr=""
    local file=$(_nix_get_file_arg)
    if [[ "$file" ]]; then
        # Extract --arg and --argstr into $args
        local i=1 args="" name="" value=""
        for ((i=1; i < ${#words[*]}; i++)); do
            case "${words[$i]}" in
                --arg)
                    name=${(Q)words[$((i+1))]}
                    value=${(Q)words[$((i+2))]}
                    args+="$name = $value;"
                    i=$((i+2))
                    ;;
                --argstr)
                    name=${(Q)words[$((i+1))]}
                    value=${(Q)words[$((i+2))]}
                    args+="$name = \"$value\";"
                    i=$((i+2))
                    ;;
            esac
        done
        args=${args:+{$args}}

        local opt
        defexpr="import $file $args"
        for opt in $words; do
            case $opt in
                --expr|-[^-]#E[^-]#)
                    defexpr="($file) $args"
                    break
                    ;;
            esac
        done
    else
        if [[ $service == nix-env ]]; then
            defexpr=$(_nix_gen_defexpr ~/.nix-defexpr)

        elif [[ $service == nix ]]; then
            # Extract the channels from NIX_PATH and -I/--include
            local -a channels=(${(s.:.)NIX_PATH})
            # Add -I/--include afterwards, so they will shadow the NIX_PATH
            channels+=(${(s.:.)opt_args[-I]})
            channels+=(${(s.:.)opt_args[--include]})

            # Add the names in an associative array to avoid duplicates
            local -A names
            local channel name
            for channel in $channels; do
                name=${channel%%=*}
                nix_path=${channel#*=}
                if [[ $name != $channel ]]; then
                    # Only add paths with a name, not sure how they work
                    names[$name]=1
                fi
            done

            defexpr=$'{ '
            for name in ${(@k)names}; do
                # nixos-config isn't useful or possible to complete
                [[ $name == nixos-config ]] && continue
                defexpr+="$name = import <${name}>; "
            done
            defexpr+=' }'
        fi
    fi

    if [[ $defexpr ]]; then
        _nix_attr_paths $defexpr
    fi
}

function _nix_resolve_url () {
    local url=$1
    local version="$($service --version)"
    local input
    if [[ "${version##* }" == 1.11.* ]]; then
        # works for nix 1.11
        input="$url"
    else
        # works for nix 1.12
        input="${url##*/}\0$url"
    fi
    local sha
    sha=$(nix-hash --flat --base32 --type sha256 <(printf "$input"))
    local cache=${XDG_CACHE_HOME:-~/.cache}/nix/tarballs
    local link="$cache"/"$sha"-file
    if [[ -e "$link" ]]; then
        echo "$cache/$(basename $(readlink $link))-unpacked"
    fi
}

function _nix_get_file_arg () {

    local file=""
    if [[ "$service" == (nix-env|nix) ]]; then
        local i
        # Extract the last seen -f/--file argument
        for ((i=1; i < ${#words[*]}; i++)); do
            case "${words[i]}" in
                --file|-f)
                    file=${words[$((i+1))]}
                    ;;
                -f\.)
                    # -f. is accepted shorthand for -f .
                    file=.
                    ;;
            esac
        done
    elif [[ $line ]]; then
        file=$line[1]
    elif [[ -e shell.nix && $service == nix-shell ]]; then
        file=shell.nix
    elif [[ -e default.nix ]]; then
        file=default.nix
    fi

    # Remove one level of shell quoting to make sure we see the same value as
    # the nix-* program will see.
    # ($opt_args and $line contain the verbatim string:
    #  eg. given `nix-shell '<nixpkgs>' -A ` $line[1] will be `'<nixpkgs>'` while
    #  nix-shell will see `<nixpkgs>`)
    file=${(Q)file}

    if [[ "file" ]]; then
        # Expand channel: syntax
        if [[ "$file" == channel:* ]]; then
            file="https://nixos.org/channels/"${file:8}"/nixexprs.tar.xz"
        fi

        if [[ -e $file ]]; then
            # If the path exist use the absolute path to make sure import will
            # accept it.
            # (Otherwise the path is likely a <nixpkgs> notation)
            file=${file:a}
        elif [[ "$file" == https://* ]]; then
            file=$(_nix_resolve_url $file)
        fi
    fi
    print -n -- $file
}

function _nix_complete_function_arg () {
    local file=$(_nix_get_file_arg)
    local func=${file:+import $file} opt
    local i exclude=""
    for ((i=1; i < ${#words}; i++)); do
        case "${words[$i]}" in
            --expr|-[^-]#E[^-]#)
                func="$file"
                ;;
            --arg|--argstr)
                # Don't add the name we're currently typing
                [[ $i == $((CURRENT - 1)) ]] && continue
                exclude+=${exclude:+|}${words[$((i+1))]}
                ;;
        esac
    done
    if [[ ! $func ]]; then
        return
    fi
    local -a names
    names=($(_nix_eval_stdin 2>&1 <<NIX_FILE
             if builtins.typeOf ($func) == "lambda" then
                builtins.attrNames (builtins.functionArgs ($func))
             else
                 ""
NIX_FILE
                    ))
    if [[ $? > 0 ]]; then
        _message "Eval failed, can't complete (an URL might not be cached):
$names"
        return 1
    fi
    names=(${names:#(${~exclude})})
    [[ "$names" ]] && _values "Argument name" $names
}

_nix_profiles () {
    local prefix='-P /nix/var/nix/profiles/ -W /nix/var/nix/profiles/'
    local cur=$words[$CURRENT]
    if [[ $cur ]]; then
        prefix=""
    fi
    _path_files -/ ${=prefix}
}

# Either true or false: useful for completing many Nix options
_nix_options_bool () {
    _values true false
}
# List gathered from: https://nixos.org/nix/manual/#sec-conf-file
# TODO: Complete the value as well, not just the key
_nix_options () {
    # Complete nix options with descriptions
    local -a nix_options
    # Strip the header line, remove leading spaces and replace separating
    # whitespace with ':'
    nix_options=(${${${${(f)"$(nix --help-config)"}:1:-1}/#  /}/  ##/:})
                   _describe -t nix_options "Option" nix_options
}

_nix_options_value () {
    # Print the description of the option we're setting
    local OPTION=$words[$(($CURRENT - 1))]
    # Remove lines not starting with " $OPTION " and strip eveything up to the
    # last two consecutive spaces
    local description=${${${(f)"$(nix --help-config)"}:#^(  $OPTION *)}/*  /}
    local -a values=()
    case "$description" in
        Whether*)
            _values $description true false
        ;;
        *)
            _path_files
        ;;
    esac
}

_nix_run_command_names () {
    local cmd chan

    if (( ${+commands[sqlite3]} )); then
        # Extract the channels from NIX_PATH and -I/--include
        # Add -I/--include afterwards, so they will shadow the NIX_PATH
        local -a nix_path=(
            ${(s.:.)NIX_PATH}
            ${(s.:.)opt_args[-I]}
            ${(s.:.)opt_args[--include]}
        )

        # channels: key - channel name, value - path to channel
        local -A channels
        for chan in $nix_path; do
            if [[ $chan = *=* ]]; then
                # name=path
                channels[${chan%%=*}]=${chan#*=}
            else
                # path to directory with channels
                for chan in $chan/*(-/); do
                    channels[$chan:t]=$chan
                done
            fi
        done

        # pkg_cmds is list of commands inside packages
        # This is an associative array to avoid duplicates.
        local -A pkg_cmds
        for chan in ${(k)channels}; do
            # Extract args with prefix "$chan."
            local -a pkgs=( "${${(M)words[@]:#"$chan".*}[@]##"$chan".}" )
            (( ${#pkgs} )) || continue

            local db=${channels[$chan]}/programs.sqlite
            [ -f "$db" ] || continue

            pkgs=( "'${^pkgs[@]//\'/''}'" ) # SQL-quote
            local query="SELECT name FROM programs WHERE package IN (${(j:,:)pkgs})"

            for cmd in $(sqlite3 "$db" "$query"); do
                pkg_cmds[$cmd]=
            done
        done

        compadd -X 'Package commands' -- ${(k)pkg_cmds}
    fi

    _command_names -e -X 'All commands'
}

## Common options

# Used in: nix-build, nix-env, nix-instantiate, nix-shell, nixops
__nix_boilerplate_opts=(
    '(- *)--help[Print help message and exit]'
    '(- *)--version[Print version number and exit]'
)

# Used in: nix-collect-garbage, nix-env, nix-store, nixops
__nix_dry_run='--dry-run[Show what would be done without doing it]'

# Used in: nix-collect-garbage, nix-store
__nix_gc_common=(
    '(- --print* --delete)--print-roots[Print roots used by garbage collector]'
    '(- --print* --delete)--print-live[Print store paths reachable from roots]'
    '(- --print* --delete)--print-dead[Print store paths not reachable from roots]'
    '(- --print* --delete)--delete[Garbage collect all dead paths from the store]'
)

# Used in: nixos-install, nix_common_opts
__nix_search_path_args=(
    '*-I[add a path to the list of locations used to look up <...> file names]:include path:_nix_complete_includes'
)

__nix_repair='--repair[Fix corrupted or missing store paths by redownloading or rebuilding]';

__nix_expr_opts=(
    '(--expr -E)'{--expr,-E}'[interpret command line args as Nix expressions]'
)

# Misc Nix options accepted by nixos-rebuild
__nix_common_nixos_rebuild=(
    $__nix_search_path_args
    $__nix_repair
    '(--verbose -v)*'{--verbose,-v}'[Increase verbosity of diagnostic messages]'
    '(--no-build-output -Q)'{--no-build-output,-Q}'[silence output to stdout and stderr]'
    '(--max-jobs -j)'{--max-jobs,-j}'[max number of build jobs in parallel]:jobs:'
    '--cores[threads per job (e.g. -j argument to make)]:cores:'
    '(--keep-going -k)'{--keep-going,-k}"[keep going until all builds are finished]"
    '(--keep-failed -K)'{--keep-failed,-K}'[keep failed builds (usually in /tmp)]'
    '--fallback[If binary download fails, fall back on building from source]'
    '--show-trace[Print stack trace of evaluation errors]'
    '*--option[set Nix configuration option]:options:_nix_options:value:_nix_options_value'
)

__nix_common_nixos_build_vms=(
    '*--option[set Nix configuration option]:options:_nix_options:value:_nix_options_value'
)

__nix_common_store_opts=(
    '--add-root[register result as a root of the garbage collector]:path (Hint /nix/var/nix/gcroots):_path_files -/'
    '--indirect[store gc root outside GC roots directory]'
)

# Used in: nix-build, nix-env, nix-instantiate, nix-shell and nix-store
__nix_extra_build_opts=(
    '--max-silent-time[max seconds without getting stdout/err from builder]:Seconds:'
    '--timeout[max seconds builders should run]:seconds:'
    '--readonly-mode[do not open Nix database]'
    '--log-format[configure how output is formatted]:output format:((pretty\:"Default" escapes\:"Indicate nesting with escape codes" flat\:"Remove all nesting"))'
)

# Used in: nix-build, nix-env, nix-instantiate, nix-shell
__nix_common_opts=(
    $__nix_common_nixos_rebuild
    $__nix_args_opts
    $__nix_extra_build_opts
    '*--include[add a path to the list of locations used to look up <...> file names]:include path:_nix_complete_includes'
    '*--arg[argument to pass to the Nix function]:Name:_nix_complete_function_arg:Value: '
    '*--argstr[pass a string]:Name:_nix_complete_function_arg:String: '
)

# Options for nix-store --realise, used by nix-build
__nix_store_realise_opts=(
    $__nix_dry_run
    '--check[rebuild and see if output is deterministic]'
)

_NIX_SHELL_COMPLETION_LOADED=1