#!/bin/bash
#
# install-dotfiles
#
# Installs symlinks to dotfiles in this repo into current user's $HOME dir.
#
# Never clobbers anything by default -- an existing regular file, real directory, or
# symlink pointing anywhere unexpected is left alone and reported as skipped. An existing
# symlink already pointing at the right place is left alone too, silently.
#
# By default, a symlink that's recognizable as repo-managed (its current target is
# somewhere under this repo) but currently points at the wrong place within it -- e.g.
# after a file moves -- gets force-updated. Pass --no-relink to leave those alone too,
# reported as skipped, like the old default. A symlink pointing somewhere NOT
# recognizable as this repo is never touched by relinking either way; it needs --clobber.
#
# --clobber replaces that: any pre-existing local content this run would otherwise skip,
# repo-managed or not. Requires --yes-really alongside it, or it refuses to run at all.
# The original is always renamed to a `.bak` file first, never deleted outright.
# --clobber implies relinking repo-managed links too; combining it with --no-relink is
# refused outright as a likely mistake, rather than silently picking a side.
#
# Two source trees, siblings under each `dots/<osname>/` root. ("all-os" applies to
# all OSes; per-OS entries take precedence over it in both trees.)
#
#   <osname>/flat/     Entries map to ~/.<name>, linked whole -- a directory here becomes
#                      one symlink, which is how ~/.dotlib works. A leading dot is always
#                      added, and a .sh/.zsh/.bash extension is stripped.
#   <osname>/nested/   Mirrors $HOME. Parent dirs are created for real, leaf regular files
#                      are symlinked. Per path component, `_x` installs as `.x` and `__x`
#                      escapes to a literal `_x`. Components that really start with a dot
#                      are this repo's own control files (.gitkeep) and are skipped. A
#                      directory containing a `.linkdir` file links whole instead of being
#                      walked; a directory containing a `.linkdirs` file has each of its
#                      immediate child directories link whole.
#
# Name munging exists so the repo holds no dotfiles of its own except real repo-control
# ones. The two trees munge differently because they differ: every flat entry becomes a
# ~/.foo, so the dot is implicit and needs no marker, while in nested/ only some
# components are dotted, so they have to be marked. See dots/README.md for the longer
# version.
#
# The linking engine itself lives in lib/dotinstall-lib.sh, shared with other repos'
# installers (e.g. dotfiles-private's install-dotfiles-private).

# Load JXL. See "Loading JXL" in dots/README.md.
source "$(cd "$(dirname "$0")" && pwd -P)/dots/all-os/flat/dotlib/jxl-lib.sh" \
    || { echo >&2 "ERROR: failed to load jxl-lib.sh"; exit 1; }

jxl::init_script
jxl::use_short_names

source "$(cd "$(dirname "$0")" && pwd -P)/lib/dotinstall-lib.sh" \
    || { echo >&2 "ERROR: failed to load dotinstall-lib.sh"; exit 1; }


function usage() {
  cat <<EOF
$PROGRAM_NAME - install dotfile symlinks to the repo this script is in

Usage:
  $PROGRAM_NAME [--trace] [--verbose]
  $PROGRAM_NAME --help

Options:
    -x, --trace         trace program execution
        --dots-dir DIR  install from DIR instead of the repo's own dots/ (for tests)
        --relink        force-relink a symlink recognized as repo-managed but pointing
                         somewhere else (this is the default; --relink is accepted
                         explicitly too)
        --no-relink     leave a repo-managed-but-stale symlink alone instead, reporting
                         it as skipped -- the old default. Refused together with
                         --clobber.
        --clobber       replace existing local content this run would otherwise skip
                         (repo-managed or not) -- backed up to a .bak file first, never
                         just deleted. Implies relinking repo-managed links too, so it
                         cannot be combined with --no-relink. Requires --yes-really.
        --yes-really    confirms a dangerous operation (currently: --clobber) is really
                         wanted; doesn't change what it does, only whether it's allowed
                         to run at all
$(jxl::std_options_help)
EOF
}

OPT_TRACE=0
OPT_DOTS_DIR=''
OPT_RELINK=1
OPT_CLOBBER=0
OPT_YES_REALLY=0

function main() {
  if [[ $OPT_TRACE = 1 ]]; then
    set -o xtrace
  fi
  # Absolute, so the links we write don't depend on the cwd we were invoked from. Run as
  # `./install-dotfiles`, a relative $sourcedir put a literal "./bin" in ~/bin, which from
  # ~ points at itself.
  sourcedir=$(cd "$(dirname "$0")" && pwd)
  dotfiles_repo_dir="$sourcedir"
  dotdir="${OPT_DOTS_DIR:-$sourcedir/dots}"
  todir=$HOME

  dotinstall::detect_os_type
  cd "$todir"
  _DOTINSTALL_OPT_RELINK="$OPT_RELINK"
  _DOTINSTALL_OPT_CLOBBER="$OPT_CLOBBER"
  _DOTINSTALL_REPO_ROOTS=("$sourcedir")

  dotinstall::install_flat_tree "$dotdir"
  dotinstall::install_nested_tree "$dotdir"
  install_bin

  local rc=0
  dotinstall::summary || rc=1

  info "Dotfiles are now linked to $dotdir."
  # Last, so it is the line still on screen. Everything above describes what a real run
  # would have done, and read on its own would be misleading.
  if is_dry_run; then
    info 'DRY-RUN: This was a dry run. No data was actually written.'
  fi
  exit "$rc"
}

function parse_cli() {
  local arg

  while [[ $# -ge 1 ]]; do
    arg="$1"; shift
    case "$arg" in
      --trace | -x)   OPT_TRACE=1 ;;
      --dots-dir)     OPT_DOTS_DIR="$1"; shift ;;
      --relink)       OPT_RELINK=1 ;;
      --no-relink)    OPT_RELINK=0 ;;
      --clobber)      OPT_CLOBBER=1 ;;
      --yes-really)   OPT_YES_REALLY=1 ;;
      *)
        jxl::std_opt "$arg" \
            || die "Unexpected argument: ${arg}. See '$PROGRAM_NAME --help' for usage"
        if [[ $_JXL_WANT_HELP == 1 ]]; then usage; exit 0; fi
        ;;
    esac
  done

  if [[ $OPT_CLOBBER == 1 && $OPT_YES_REALLY != 1 ]]; then
    die "You must also specify --yes-really when using --clobber."
  fi
  # OPT_RELINK can only be 0 here via an explicit --no-relink (the default is 1), so this
  # unambiguously means both were given together.
  if [[ $OPT_CLOBBER == 1 && $OPT_RELINK == 0 ]]; then
    die "--no-relink and --clobber cannot be combined -- --clobber implies relinking" \
        "repo-managed links too, and a carve-out for --no-relink is very likely a mistake."
  fi

  readonly OPT_TRACE OPT_DOTS_DIR OPT_RELINK OPT_CLOBBER OPT_YES_REALLY
}

# ========== Script-specific code ==========

function install_bin() {
  # repo home-bin/ -> ~/bin. Pre: cwd is $HOME. Reads $dotfiles_repo_dir.
  local _DOTINSTALL_THIS_CHANGED
  dotinstall::symlink "${dotfiles_repo_dir}/home-bin" "bin"
  if [[ $_DOTINSTALL_THIS_CHANGED == 1 ]]; then
    echo "Linked the ~/bin directory. Don't forget to add it to your \$PATH."
  fi
}


# ========== Main script ==========

parse_cli "$@"
main   # exits with its own status; nothing here is reachable
