#!@bash@/bin/bash

# This code explicitly requires GNU Core Utilities and we therefore
# need to ensure they are prioritized over any other similarly named
# tools on the system.
PATH=@coreutils@/bin:@less@/bin${PATH:+:}$PATH

set -euo pipefail

function errorEcho() {
    # shellcheck disable=2048,2086
    echo $* >&2
}

# Attempts to set the HOME_MANAGER_CONFIG global variable.
#
# If no configuration file can be found then this function will print
# an error message and exit with an error code.
function setConfigFile() {
    if [[ -v HOME_MANAGER_CONFIG ]] ; then
        if [[ ! -e "$HOME_MANAGER_CONFIG" ]] ; then
            errorEcho "No configuration file found at $HOME_MANAGER_CONFIG"
            exit 1
        fi

        HOME_MANAGER_CONFIG="$(realpath "$HOME_MANAGER_CONFIG")"
        return
    fi

    local defaultConfFile="${XDG_CONFIG_HOME:-$HOME/.config}/nixpkgs/home.nix"
    local confFile
    for confFile in "$defaultConfFile" \
                    "$HOME/.nixpkgs/home.nix" ; do
        if [[ -e "$confFile" ]] ; then
            HOME_MANAGER_CONFIG="$confFile"
            return
        fi
    done

    errorEcho "No configuration file found." \
         "Please create one at $defaultConfFile"
    exit 1
}

function setHomeManagerNixPath() {
    local path
    for path in "@HOME_MANAGER_PATH@" \
                "${XDG_CONFIG_HOME:-$HOME/.config}/nixpkgs/home-manager" \
                "$HOME/.nixpkgs/home-manager" ; do
        if [[ -e "$path" || "$path" =~ ^https?:// ]] ; then
            export NIX_PATH="home-manager=$path${NIX_PATH:+:}$NIX_PATH"
            return
        fi
    done
}

function doBuildAttr() {
    setConfigFile
    setHomeManagerNixPath

    local extraArgs="$*"

    for p in "${EXTRA_NIX_PATH[@]}"; do
        extraArgs="$extraArgs -I $p"
    done

    if [[ -v VERBOSE ]]; then
        extraArgs="$extraArgs --show-trace"
    fi

    # shellcheck disable=2086
    nix-build \
        "<home-manager/home-manager/home-manager.nix>" \
        $extraArgs \
        --argstr confPath "$HOME_MANAGER_CONFIG" \
        --argstr confAttr "$HOME_MANAGER_CONFIG_ATTRIBUTE"
}

# Presents news to the user. Takes as argument the path to a "news
# info" file as generated by `buildNews`.
function presentNews() {
    local infoFile="$1"

    # shellcheck source=/dev/null
    . "$infoFile"

    # shellcheck disable=2154
    if [[ $newsNumUnread -eq 0 ]]; then
        return
    elif [[ "$newsDisplay" == "silent" ]]; then
        return
    elif [[ "$newsDisplay" == "notify" ]]; then
        local msg
        if [[ $newsNumUnread -eq 1 ]]; then
            msg="There is an unread and relevant news item.\n"
            msg+="Read it by running the command '$(basename "$0") news'."
        else
            msg="There are $newsNumUnread unread and relevant news items.\n"
            msg+="Read them by running the command '$(basename "$0") news'."
        fi

        # Not actually an error but here stdout is reserved for
        # nix-build output.
        errorEcho
        errorEcho -e "$msg"
        errorEcho

        if [[ -v DISPLAY ]] && type -P notify-send > /dev/null; then
            notify-send "Home Manager" "$msg"
        fi
    elif [[ "$newsDisplay" == "show" ]]; then
        doShowNews --unread
    else
        errorEcho "Unknown 'news.display' setting '$newsDisplay'."
    fi
}

function doBuild() {
    if [[ ! -w . ]]; then
        errorEcho "Cannot run build in read-only directory";
        return 1
    fi

    local newsInfo
    newsInfo=$(buildNews)

    local exitCode
    doBuildAttr -A activationPackage \
        && exitCode=0 || exitCode=1

    presentNews "$newsInfo"

    return $exitCode
}

function doSwitch() {
    local newsInfo
    newsInfo=$(buildNews)

    local generation
    local exitCode=0
    local wrkdir

    # Build the generation and run the activate script. Note, we
    # specify an output link so that it is treated as a GC root. This
    # prevents an unfortunately timed GC from removing the generation
    # before activation completes.
    wrkdir="$(mktemp -d)"
    generation=$(doBuildAttr -o "$wrkdir/result" -A activationPackage) \
        && $generation/activate || exitCode=1

    # Because the previous command never fails, the script keeps
    # running and $wrkdir is always removed.
    rm -r "$wrkdir"

    presentNews "$newsInfo"

    return $exitCode
}

function doListGens() {
    # Whether to colorize the generations output.
    local color="never"
    if [[ -t 1 ]]; then
        color="always"
    fi

    pushd "/nix/var/nix/profiles/per-user/$USER" > /dev/null
    # shellcheck disable=2012
    ls --color=$color -gG --time-style=long-iso --sort time home-manager-*-link \
        | cut -d' ' -f 4- \
        | sed -E 's/home-manager-([[:digit:]]*)-link/: id \1/'
    popd > /dev/null
}

# Removes linked generations. Takes as arguments identifiers of
# generations to remove.
function doRmGenerations() {
    if [[ -v VERBOSE ]]; then
        export VERBOSE_ARG="--verbose"
    else
        export VERBOSE_ARG=""
    fi

    if [[ -v DRY_RUN ]] ; then
        export DRY_RUN_CMD=echo
    else
        export DRY_RUN_CMD=""
    fi

    pushd "/nix/var/nix/profiles/per-user/$USER" > /dev/null

    for generationId in "$@"; do
        local linkName="home-manager-$generationId-link"

        if [[ ! -e $linkName ]]; then
            errorEcho "No generation with ID $generationId"
        elif [[ $linkName == $(readlink home-manager) ]]; then
            errorEcho "Cannot remove the current generation $generationId"
        else
            echo Removing generation $generationId
            $DRY_RUN_CMD rm $VERBOSE_ARG $linkName
        fi
    done

    popd > /dev/null
}

function doListPackages() {
    local outPath
    outPath="$(nix-env -q --out-path | grep -o '/.*home-manager-path$')"
    if [[ -n "$outPath" ]] ; then
        nix-store -q --references "$outPath" | sed 's/[^-]*-//'
    else
        errorEcho "No home-manager packages seem to be installed."
    fi
}

function newsReadIdsFile() {
    local dataDir="${XDG_DATA_HOME:-$HOME/.local/share}/home-manager"
    local path="$dataDir/news-read-ids"

    # If the path doesn't exist then we should create it, otherwise
    # Nix will error out when we attempt to use builtins.readFile.
    if [[ ! -f "$path" ]]; then
        mkdir -p "$dataDir"
        touch "$path"
    fi

    echo "$path"
}

# Builds news meta information to be sourced into this script.
#
# Note, we suppress build output to remove unnecessary verbosity. We
# also use "no out link" to avoid the need for a build directory
# (although this exposes the risk of GC removing the result before we
# manage to source it).
function buildNews() {
    doBuildAttr --quiet \
                --attr newsInfo \
                --no-out-link \
                --arg check false \
                --argstr newsReadIdsFile "$(newsReadIdsFile)"
}

function doShowNews() {
    local infoFile
    infoFile=$(buildNews) || return 1

    # shellcheck source=/dev/null
    . "$infoFile"

    # shellcheck disable=2154
    case $1 in
        --all)
            ${PAGER:-less} "$newsFileAll"
            ;;
        --unread)
            ${PAGER:-less} "$newsFileUnread"
            ;;
        *)
            errorEcho "Unknown argument $1"
            return 1
    esac

    # shellcheck disable=2154
    if [[ -s "$newsUnreadIdsFile" ]]; then
        local newsReadIdsFile
        newsReadIdsFile="$(newsReadIdsFile)"
        cat "$newsUnreadIdsFile" >> "$newsReadIdsFile"
    fi
}

function doHelp() {
    echo "Usage: $0 [OPTION] COMMAND"
    echo
    echo "Options"
    echo
    echo "  -f FILE      The home configuration file."
    echo "               Default is '~/.config/nixpkgs/home.nix'."
    echo "  -A ATTRIBUTE Optional attribute that selects a configuration"
    echo "               expression in the configuration file."
    echo "  -I PATH      Add a path to the Nix expression search path."
    echo "  -v           Verbose output"
    echo "  -n           Do a dry run, only prints what actions would be taken"
    echo "  -h           Print this help"
    echo
    echo "Commands"
    echo
    echo "  help         Print this help"
    echo
    echo "  build        Build configuration into result directory"
    echo
    echo "  switch       Build and activate configuration"
    echo
    echo "  generations  List all home environment generations"
    echo
    echo "  remove-generations ID..."
    echo "      Remove indicated generations. Use 'generations' command to"
    echo "      find suitable generation numbers."
    echo
    echo "  packages     List all packages installed in home-manager-path"
    echo
    echo "  news         Show news entries in a pager"
}

EXTRA_NIX_PATH=()
HOME_MANAGER_CONFIG_ATTRIBUTE=""

# As a special case, if the user has given --help anywhere on the
# command line then print help and exit.
for arg in "$@"; do
    if [[ $arg == "--help" ]]; then
        doHelp
        exit 0
    fi
done

while getopts f:I:A:vnh opt; do
    case $opt in
        f)
            HOME_MANAGER_CONFIG="$OPTARG"
            ;;
        I)
            EXTRA_NIX_PATH+=("$OPTARG")
            ;;
        A)
            HOME_MANAGER_CONFIG_ATTRIBUTE="$OPTARG"
            ;;
        v)
            export VERBOSE=1
            ;;
        n)
            export DRY_RUN=1
            ;;
        h)
            doHelp
            exit 0
            ;;
        *)
            errorEcho "Unknown option -$OPTARG"
            doHelp >&2
            exit 1
            ;;
    esac
done

# Get rid of the options.
shift "$((OPTIND-1))"

if [[ $# -eq 0 ]]; then
    doHelp >&2
    exit 1
fi

cmd="$1"
shift 1

case "$cmd" in
    build)
        doBuild
        ;;
    switch)
        doSwitch
        ;;
    generations)
        doListGens
        ;;
    remove-generations)
        doRmGenerations "$@"
        ;;
    packages)
        doListPackages
        ;;
    news)
        doShowNews --all
        ;;
    help|--help)
        doHelp
        ;;
    *)
        errorEcho "Unknown command: $cmd"
        doHelp >&2
        exit 1
        ;;
esac
