#!/usr/bin/env bash

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)
readonly SCRIPT_DIR
readonly PROFILES_DIR=${SCRIPT_DIR}/profiles
readonly -a STOW_ARGS=(--no-folding --adopt --dir "${PROFILES_DIR}" --target "${HOME}")
readonly SCRIPT_NAME=${0##*/}

print_opt() {
    local -r option=$1
    local -r description=$2
    printf "  %-22s %s\n" "${option}" "${description}"
}

shell_quote() {
    local -r string=$1
    printf "'%s'" "${string//'/'\\''}"
}

array_contains() {
    local -r target_item=$1; shift
    local -a -r array=("$@")
    local item
    for item in "${array[@]}"; do
        if [[ ${item} == "${target_item}" ]]; then
            return 0
        fi
    done
    return 1
}

die() {
    local -r message=$1
    printf "%s: %b\n" "${SCRIPT_NAME}" "${message}" >&2
    exit 1
}

invalid_option() {
    local -r option=$1
    die "invalid option $(shell_quote "${option}")\nTry '${SCRIPT_NAME} --help' for usage."
}

check_deps() {
    if ! command -v stow >/dev/null 2>&1; then
        cat >&2 <<EOF
Error: 'stow' not found in PATH.

GNU Stow is a symlink farm manager used to deploy dotfiles/packages
by creating symbolic links into \${HOME}.

Install it first:
  Arch Linux:     pacman -S stow
  Debian/Ubuntu:  apt install stow
  Fedora:         dnf install stow
Typically, on other Linux distributions you can use their own package
managers. For other systems, you can find in the Internet installation
instructions appropriate to them or build manually GNU Stow from the
source if GNU Stow is not packaged or your system requires it.

Project: https://www.gnu.org/software/stow/
EOF
        exit 1
    fi
}

require_profiles_dir() {
    if [[ ! -d ${PROFILES_DIR} ]]; then
        cat >&2 <<EOF
Error: profiles directory not found: ${PROFILES_DIR}

This directory must contain profile subdirectories with dotfiles.

Make sure you:
  - run the script from the repository root, or
  - create the directory, or
  - clone the dotfiles repository correctly.

More information about what is profile can be found using '${SCRIPT_NAME} --help profile'.
EOF
        exit 1
    fi
    mapfile -t PROFILES < <(find "${PROFILES_DIR}" -mindepth 1 -maxdepth 1 -type d -printf '%f\n')
    readonly PROFILES
}

parse_args() {
    while (( $# > 0 )); do
        case $1 in
            -h)
                show_general_help
                exit 0
                ;;
            --help)
                if (( $# >= 2 )) && [[ $2 ]] && [[ $2 != -* ]]; then
                    show_help_topic "$2"
                else
                    show_general_help
                fi
                exit 0
                ;;
            --help=*)
                local -r topic=${1#*=}
                [[ -z ${topic} ]] && show_general_help && exit 0
                show_help_topic "${topic}"
                exit 0
                ;;
            -l|--list-profiles) list_profiles=true; shift ;;
            -P|--prune-symlinks) prune_symlinks=true; shift ;;
            -v|--verbose) verbose=true; shift ;;
            -p|--profile)
                ( (( $# < 2 )) || [[ -z "$2" ]] ) && die "--profile requires an argument"
                profile=$2
                shift 2
                ;;
            *) invalid_option "$1" ;;
        esac
    done
}

validate_args() {
    if [[ ${list_profiles} && ( ${prune_symlinks} || ${verbose} || ${profile} ) ]]; then
        die "options '--list-profiles' and others cannot be used together"
    fi

    if [[ ${profile} ]] && ! array_contains "${profile}" "${PROFILES[@]}"; then
        die "profile $(shell_quote "${profile}") cannot be found"
    fi
}

show_general_help() {
    echo "Usage: ${SCRIPT_NAME} [options]"
    echo "       ${SCRIPT_NAME} -l | --list-profiles"
    echo "       ${SCRIPT_NAME} --help[=TOPIC]"
    echo
    echo "Options:"
    print_opt "-h, --help[=TOPIC]" "show general help or help for a specific topic"
    print_opt "-p, --profile PROFILE" "select profile"
    print_opt "-v, --verbose" "increase verbosity"
    print_opt "-P, --prune-symlinks" "remove dangling symlinks in home directory"
    print_opt "-l, --list-profiles" "list available profiles (cannot be combined with other options)"
    echo
    echo "Topics:"
    print_opt "profile" "explanation of profile structure and usage"
    echo
    echo "Profile:"
    echo "  Directory containing dotfiles plus optional parent and hooks/."
    echo "  Used by the installer to group and deploy related configurations."
}

show_help_topic() {
    local topic=$1
    case ${topic} in
        profile)
            cat <<EOF
A profile is a directory under 'profiles/' that groups dotfiles to be installed
together using GNU Stow.

Contents:
  dotfiles/        files or directories to deploy (e.g. .config)
  parent           optional text file with parent profile name
  hooks/pre/       scripts run before installation
  hooks/post/      scripts run after installation

Only executable files inside hooks are executed.

Profiles may inherit from a parent. The installer builds the dependency
tree automatically and installs profiles in the correct order.

Use .stow-local-ignore to prevent linking internal files such as hooks/ and parent.
EOF
            ;;
        *) die "unknown help topic: ${topic}" ;;
    esac
}

run_pre_hooks() {
    local -r target_profile=$1
    for hook in "${PROFILES_DIR}/${target_profile}"/hooks/pre/*; do
        if [[ -f ${hook} && -x ${hook} ]]; then
            echo "Running pre-install hook ${hook##*/}"
            "${hook}"
        fi
    done
}

run_post_hooks() {
    local -r target_profile=$1
    for hook in "${PROFILES_DIR}/${target_profile}"/hooks/post/*; do
        if [[ -f ${hook} && -x ${hook} ]]; then
            echo "Running post-install hook ${hook##*/}"
            "${hook}"
        fi
    done
}

prune_symlinks() {
    echo -n "Removing dangling symlinks in home directory... "

    pushd "${HOME}" >/dev/null || exit 1
    local common_files
    local -r target_profile=$1
    mapfile -t common_files < <(comm -12 <(find "${HOME}" -type l -printf '%P\n' 2>/dev/null | sort) \
        <(find "${PROFILES_DIR}/${target_profile}" -type f -printf '%P\n' | sort))
    local target_file
    for target_file in "${common_files[@]}"; do
        local -a rm_args=(--force)
        [[ ${verbose} ]] && rm_args+=(--verbose)
        rm "${rm_args[@]}" -- "${target_file}"
    done
    popd >/dev/null || exit 1

    echo "OK"
}

stow_profile() {
    local -r target_profile=$1
    if stow "${STOW_ARGS[@]}" "${target_profile}" 2>&1; then
        echo "Profile ${target_profile} has been stowed"
    else
        echo "Failed to create symlinks pointing to dotfiles in profile ${target_profile}"
    fi
}

fetch_parents() {
    local -r target_profile=$1
    if [[ -s ${PROFILES_DIR}/${target_profile}/parent ]]; then
        local parent
        parent=$(cat "${PROFILES_DIR}/${target_profile}/parent")
        if [[ -s ${PROFILES_DIR}/${parent}/parent ]]; then
            fetch_parents "${parent}"
        fi
        echo "${parent}"
    fi
}

build_deptree() {
    local -r target_profile=$1
    fetch_parents "${target_profile}"
    echo "${target_profile}"
}

install_dotfiles() {
    local target_profile
    for target_profile in $(build_deptree "${profile}"); do
        run_pre_hooks "${target_profile}"
        [[ ${prune_symlinks} ]] && prune_symlinks "${target_profile}"
        stow_profile "${target_profile}"
        run_post_hooks "${target_profile}"
    done
}

main() {
    check_deps
    parse_args "$@"
    require_profiles_dir
    (( $# == 0 )) && show_general_help && exit 1
    validate_args
    [[ ${list_profiles} ]] && echo "${PROFILES[@]}" && exit 0
    install_dotfiles
}

main "$@"
