#!/usr/bin/env bash
#
# Dotfiles manager and install helper
# (c) Ismo Vuorinen <https://github.com/ivuorinen> 2022
# Licensed under MIT, see LICENSE
#
# shellcheck source-path=$HOME/.dotfiles/local/bin
#
# Helper variables, override with ENVs like `VERBOSE=1 dfm help`
: "${VERBOSE:=0}"
: "${DOTFILES:=$HOME/.dotfiles}"
: "${BREWFILE:=$DOTFILES/config/homebrew/Brewfile}"
: "${HOSTFILES:=$DOTFILES/hosts}"

SCRIPT=$(basename "$0")

if ! declare -f msg > /dev/null; then
  # Function to print messages if VERBOSE is enabled
  # $1 - message (string)
  msg()
  {
    [ "$VERBOSE" -eq 1 ] && echo "$1"
    return 0
  }
fi

if ! declare -f msg_err > /dev/null; then
  # Function to print error messages and exit
  # $1 - error message (string)
  msg_err()
  {
    echo "(!) ERROR: $1" >&2
    exit 1
  }
fi

if ! declare -f msg_done > /dev/null; then
  # Function to print done message
  # $1 - message (string)
  msg_done()
  {
    echo "✓ $1"
    return 0
  }
fi

if ! declare -f array_diff > /dev/null; then
  # Function to compare two arrays and return the difference
  # Example: array_diff DIFFERENCE ARRAY1 ARRAY2
  # $1 - variable to store the difference
  # $2 - first array
  # $3 - second array
  # Output to $1 the difference between $2 and $3
  # Source: https://stackoverflow.com/a/42399479/594940
  array_diff()
  {
    eval local ARR1=\(\"\${$2[@]}\"\)
    eval local ARR2=\(\"\${$3[@]}\"\)
    local IFS=$'\n'
    mapfile -t "$1" < <(comm -23 <(echo "${ARR1[*]}" | sort) <(echo "${ARR2[*]}" | sort))
  }
fi

VERSION_NVM="v0.39.5"

# Loads configs for better installation experience
source "$DOTFILES/config/shared.sh"

source "${DOTFILES}/local/bin/msgr"

# -- Menu builder -- #
menu_section()
{
  LINE=$(printf '%-18s [ %-15s ]\n' "$1" "$2")
  echo -e " $(__log_marker) $LINE"
}
menu_item()
{
  LINE=$(printf '%-15s %-30s\n' "$1" "$2")
  echo -e "$(__log_indent)$(__log_marker) $LINE"
}

# Takes a bash array ("cow:moo", "dinosaur:roar") and loops
# through the keys to build menu section listing.
menu_usage_header()
{
  MENU_CMD="$1"
  shift
  MENU_ARRAY=("$@")

  KEYS=""
  for item in "${MENU_ARRAY[@]}"; do
    CMD=$(echo "${item}" | awk -F ":" '{print $1}')
    KEYS+="${CMD} | "
  done

  # "???" removes 3 last characters, being " | " from the end
  menu_section "$MENU_CMD" "${KEYS%???}"
}

# Takes the usage command "$0 dotfiles" and a
# bash array ("cow:moo" "dinosaur:roar") and loops
# through in building a menu for dfm command usage listing.
menu_usage()
{
  MENU_CMD="$1"
  shift
  MENU_ARRAY=("$@")

  msg "$MENU_CMD"

  for item in "${MENU_ARRAY[@]}"; do
    CMD=$(echo "${item}" | awk -F ":" '{print $1}')
    DESC=$(echo "${item}" | awk -F ":" '{print $2}')
    menu_item "$CMD" "$DESC"
  done
}

section_install()
{
  USAGE_PREFIX="$SCRIPT install <command>"

  MENU=(
    "all:Installs everything in the correct order"
    "antigen:Updates the antigen.zsh file"
    "cargo:Install rust/cargo packages"
    "cheat-databases:Install cheat external cheatsheet databases"
    "composer:Install composer"
    "dotenv:Install dotenv-linter"
    "fonts:Install programming fonts"
    "gh:Install GitHub CLI Extensions"
    "go:Install Go Packages"
    "imagick:Install ImageMagick CLI"
    "starship:Install starship.rs"
    "macos:Setup nice macOS defaults"
    "neofetch:Install neofetch"
    "nvm:Install Node Version Manager (nvm)"
    "nvm-latest:Install latest lts node using nvm"
    "npm:Install NPM Packages"
    "ntfy:Install ntfy"
    "ohmybash:Install oh-my-bash"
    "pip:Install pip/python packages"
    "z:Install z"
  )

  case "$1" in
    all)
      msgr msg "Starting to install all and reloading configurations..."
      $0 install macos
      $0 install fonts
      $0 install antigen
      $0 brew install
      $0 install ohmyposh
      $0 install asdf
      # $0 install ohmybash
      # $0 install pip
      # $0 install cargo
      $0 install composer
      # $0 install dotenv
      $0 install fzf
      # $0 install gh
      # $0 install go
      $0 install cheat-databases
      $0 install imagick
      # $0 install neofetch
      # $0 install nvm
      # $0 install npm
      $0 install ntfy
      $0 install z
      msgr msg "Reloading configurations again..."
      source "$DOTFILES/config/shared.sh"
      msgr yay "All done!"
      ;;
    antigen)
      msg "Installing antigen..."
      curl -sSfL git.io/antigen -o "$DOTFILES/local/bin/antigen.zsh" \
        && msg_yay "New antigen installed!"
      ;;
    asdf)
      msg "Installing asdf..."
      bash "$DOTFILES/scripts/install-asdf.sh both" \
        && msg_yay "asdf installed!"
      ;;
    cargo)
      msg "Installing cargo packages..."
      bash "$DOTFILES/scripts/install-cargo-packages.sh" \
        && msg_yay "cargo packages installed!"
      ;;
    cheat-databases)
      msg "Installing cheat databases..."
      for database in "$DOTFILES"/scripts/install-cheat-*; do
        bash "$database" \
          && msg_yay "Cheat: $database run"
      done
      ;;
    composer)
      msg "Installing composer..."
      bash "$DOTFILES/scripts/install-composer.sh" \
        && msg_yay "composer installed!"
      ;;
    dotenv)
      msg "Installing dotenv-linter..."
      curl -sSfL \
        https://raw.githubusercontent.com/dotenv-linter/dotenv-linter/master/install.sh \
        | sh -s -- -b "$XDG_BIN_HOME" \
        && msg_yay "dotenv-linter installed!"
      ;;
    fonts)
      msg "Installing fonts..."
      bash "$DOTFILES/scripts/install-fonts.sh" \
        && msg_yay "Installed fonts!"
      ;;
    fzf)
      msg "Installing fzf..."
      bash "$DOTFILES/scripts/install-fzf.sh" \
        && msg_yay "fzf installed!"
      ;;
    gh)
      msg "Installing GitHub CLI Extensions..."
      bash "$DOTFILES/scripts/install-gh-extensions.sh" \
        && msg_yay "github cli extensions installed!"
      ;;
    go)
      msg "Installing Go Packages..."
      bash "$DOTFILES/scripts/install-go-packages.sh" \
        && msg_yay "go packages installed!"
      ;;
    imagick)
      msg "Installing ImageMagick CLI..."
      curl -L https://imagemagick.org/archive/binaries/magick > "$XDG_BIN_HOME/magick" \
        && chmod +x "$XDG_BIN_HOME/magick" \
        && msg_yay "imagick installed!"
      ;;
    starship)
      msg "Installing starship.rs..."
      curl -sS https://starship.rs/install.sh | sh -s -- \
        --bin-dir ~/.local/bin \
        --yes \
        && msg_yay "starship installed!"
      ;;
    macos)
      msg "Setting up macOS defaults..."
      bash "$DOTFILES/scripts/set-macos-defaults.sh" \
        && msg_yay "Brewfile defined apps has been installed!"
      ;;
    neofetch)
      msg "Installing neofetch..."
      bash "$DOTFILES/scripts/install-neofetch.sh" \
        && msg_yay "neofetch installed!"
      ;;
    nvm)
      msg "Installing nvm..."
      curl -o- "https://raw.githubusercontent.com/nvm-sh/nvm/$VERSION_NVM/install.sh" | bash
      git checkout "$DOTFILES/base/zshrc"
      git checkout "$DOTFILES/base/bashrc"
      $0 install nvm-latest
      msg_yay "nvm installed!"
      ;;
    nvm-latest)
      msg "Installing latest lts node..."
      if [ -n "$NVM_DIR" ]; then
        [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
      fi
      nvm install --lts --latest-npm --default
      git checkout "$DOTFILES/base/zshrc"
      git checkout "$DOTFILES/base/bashrc"
      msg_yay "latest lts node installed!"
      ;;
    npm)
      msg "NPM Packages install started..."
      bash "$DOTFILES/scripts/install-npm-packages.sh" \
        && msg_yay "NPM Packages have been installed!"
      ;;
    ntfy)
      msg "ntfy install started..."
      bash "$DOTFILES/scripts/install-ntfy.sh" \
        && msg_yay "ntfy installed!"
      ;;
    ohmybash)
      msg "oh-my-bash install started..."
      bash "$DOTFILES/scripts/install-ohmybash.sh" \
        && msg_yay "oh-my-bash installed!"
      ;;
    ohmyposh)
      msg "oh-my-posh install started..."
      bash "$DOTFILES/scripts/install-ohmyposh.sh" \
        && msg_yay "oh-my-posh installed!"
      ;;
    pip)
      msg "pip install started..."
      bash "$DOTFILES/scripts/install-pip-packages.sh"
      ;;
    z)
      msg "Installing z..."
      bash "$DOTFILES/scripts/install-z.sh" \
        && msg_yay "z has been installed!"
      ;;
    *) menu_usage "$USAGE_PREFIX" "${MENU[@]}" ;;
  esac
}

section_brew()
{
  USAGE_PREFIX="$SCRIPT brew <command>"

  MENU=(
    "install:Installs items defined in Brewfile"
    "update:Updates and upgrades brew packages"
    "updatebundle:Updates Brewfile with descriptions"
    "autoupdate:Setups brew auto-update and runs it immediately"
    "leaves:List brew leaves (installed on request)"
    "clean:Clean up brew packages"
    "untracked:List untracked brew packages"
  )

  x-have brew && {
    case "$1" in
      install)
        brew bundle install --file="$BREWFILE" --force --quiet && msg_yay "Done!"
        ;;
      update)
        brew update && brew outdated && brew upgrade && brew cleanup
        msg_yay "Done!"
        ;;
      updatebundle)
        # Updates .dotfiles/homebrew/Brewfile with descriptions
        brew bundle dump \
          --force \
          --file="$BREWFILE" \
          --cleanup \
          --tap \
          --formula \
          --cask \
          --describe && msg_yay "Done!"
        ;;
      leaves)
        brew leaves --installed-on-request
        ;;
      untracked)
        declare -a BREW_LIST_ALL
        while IFS= read -r line; do
          BREW_LIST_ALL+=("$line")
        done < <(brew list --formula --installed-on-request -1 --full-name)
        while IFS= read -r c; do
          BREW_LIST_ALL+=("$c")
        done < <(brew list --cask -1 --full-name)

        # Remove entries that are installed as dependencies
        declare -a BREW_LIST_DEPENDENCIES
        while IFS= read -r l; do
          BREW_LIST_DEPENDENCIES+=("$l")
        done < <(brew list -1 --installed-as-dependency)

        declare -a BREW_LIST_BUNDLED
        while IFS= read -r b; do
          BREW_LIST_BUNDLED+=("$b")
        done < <(brew bundle list --all --file="$BREWFILE")

        declare -a BREW_LIST_TRACKED_WITHOUT_DEPS
        for f in "${BREW_LIST_ALL[@]}"; do
          if [[ ! " ${BREW_LIST_DEPENDENCIES[@]} " =~ " ${f} " ]]; then
            BREW_LIST_TRACKED_WITHOUT_DEPS+=("$f")
          fi
        done

        array_diff BREW_LIST_UNTRACKED BREW_LIST_TRACKED_WITHOUT_DEPS BREW_LIST_BUNDLED

        # If there are no untracked packages, exit
        if [ ${#BREW_LIST_UNTRACKED[@]} -eq 0 ]; then
          msg_yay "No untracked packages found!"
          exit 0
        fi

        echo "Untracked:"
        for f in "${BREW_LIST_UNTRACKED[@]}"; do
          echo "  $f"
        done
        ;;
      clean)
        brew bundle cleanup --file="$BREWFILE" && msg_yay "Done!"
        ;;
      autoupdate)
        brew autoupdate delete
        brew autoupdate start 43200 --upgrade --cleanup --immediate
        ;;
      *)
        menu_usage "$USAGE_PREFIX" "${MENU[@]}"
        ;;
    esac
  }

  ! x-have brew && menu_section "$USAGE_PREFIX" "brew not available on this system"
}

section_asdf()
{
  USAGE_PREFIX="$SCRIPT asdf <command>"
  MENU=(
    "install:Install asdf"
    "current:Show asdf current versions"
    "global:Show asdf global versions"
    "installed:Show asdf installed versions"
    "local:Show asdf local versions"
    "versions:Show asdf versions"
    "where:Show asdf where"
    "which:Show asdf which"
  )

  case "$1" in
    install)
      msg "Installing asdf..."
      bash "$DOTFILES/scripts/install-asdf.sh both" \
        && msg_yay "asdf installed!"
      ;;
    current)
      asdf current
      ;;
    global)
      asdf global
      ;;
    installed)
      asdf list
      ;;
    local)
      asdf local
      ;;
    versions)
      asdf list all
      ;;
    where)
      asdf where
      ;;
    which)
      asdf which
      ;;
    *) menu_usage "$USAGE_PREFIX" "${MENU[@]}" ;;
  esac
}

section_helpers()
{
  USAGE_PREFIX="$SCRIPT helpers <command>"
  MENU=(
    "aliases:<shell> (bash, zsh) Show aliases for bash or zsh"
    "colors:Show colors"
    "env:Show environment variables"
    "functions:Show functions"
    "nvim:Show nvim keybindings"
    "path:Show \$PATH dir by dir"
    "tmux:Show tmux keybindings"
    "wezterm:Show wezterm keybindings"
  )

  CMD="$1"
  shift
  SECTION="$1"
  shift

  case "$CMD" in
    path)
      # shellcheck disable=2001
      for i in $(echo "$PATH" | sed 's/:/ /g'); do echo "$i"; done
      ;;
    aliases)
      case "$SECTION" in
        "zsh")
          zsh -ixc : 2>&1 | grep -E '> alias' | sed "s|$HOME|~|" | grep -v "(eval)"
          ;;
        "bash")
          bash -ixc : 2>&1 | grep -E '> alias' | sed "s|$HOME|~|" | grep -v "(eval)"
          ;;
        *)
          echo "$SCRIPT helpers aliases <shell> (bash, zsh)"
          ;;
      esac
      ;;
    "colors")
      for i in {0..255}; do echo -en "\e[38;5;${i}m${i} "; done
      ;;
    "env")
      env | sort
      ;;
    "functions")
      declare -F
      ;;
    "nvim")
      cat "$DOTFILES/docs/nvim-keybindings.md"
      ;;
    "tmux")
      cat "$DOTFILES/docs/tmux-keybindings.md"
      ;;
    "wezterm")
      cat "$DOTFILES/docs/wezterm-keybindings.md"
      ;;
    *) menu_usage "$USAGE_PREFIX" "${MENU[@]}" ;;
  esac
}

section_docs()
{
  USAGE_PREFIX="$SCRIPT docs <command>"

  MENU=(
    "all:Update all keybindings documentations"
    "tmux:Update tmux keybindings documentation"
    "nvim:Update nvim keybindings documentation"
    "wezterm:Update wezterm keybindings documentation"
  )

  case "$1" in
    all)
      $0 docs tmux
      $0 docs nvim
      $0 docs wezterm
      ;;
    tmux) bash "$DOTFILES/local/bin/x-dfm-docs-xterm-keybindings" ;;
    nvim) bash "$DOTFILES/scripts/create-nvim-keymaps.sh" ;;
    wezterm) bash "$DOTFILES/scripts/create-wezterm-keymaps.sh" ;;
    *) menu_usage "$USAGE_PREFIX" "${MENU[@]}" ;;
  esac
}

section_dotfiles()
{
  USAGE_PREFIX="$SCRIPT dotfiles <command>"

  MENU=(
    "fmt:Run all formatters"
    "yamlfmt:Run yamlfmt to all dotfiles, which are in our control"
    "shfmt:Run shfmt to all dotfiles"
    "reset_all:Reset everything, runs all configured reset actions"
    "reset_nvim:Resets nvim. Deletes caches, removes nvim folders and relinks nvim folders"
  )

  case "$1" in
    fmt)
      msg_ok "Running all formatters"
      $0 dotfiles yamlfmt
      $0 dotfiles shfmt
      msg_done "...done!"
      ;;
    reset_all)
      msg_ok "Running all reset commands"
      $0 dotfiles reset_nvim
      ;;
    reset_nvim)
      rm -rf \
        ~/.local/share/nvim \
        ~/.local/state/nvim \
        ~/.cache/nvim \
        ~/.config/nvim
      msg_ok "Deleted old nvim files (share, state and cache + config)"
      ln -s "$DOTFILES/config/nvim" ~/.config/nvim
      msg_ok "Linked nvim and astronvim"
      x-have npm && $0 install npm
      msg_ok "Installed packages"
      msg_done "nvim reset!"
      ;;
    yamlfmt)
      # format yaml files
      x-have yamlfmt && yamlfmt -conf "$DOTFILES/.yamlfmt"
      ! x-have yamlfmt && msg_err "yamlfmt not found"
      ;;
    shfmt)
      # If system doesn't have fd or shfmt installed, exit
      ! x-have fd && msg_err "fd not found, install with asdf"
      ! x-have shfmt && msg_err "shfmt not found, install with asdf"
      # Format shell scripts according to following rules.
      fd --full-path "$DOTFILES" -tx \
        --hidden \
        -E '*.pl' -E '*.php' -E '*.py' -E '*.zsh' -E 'plugins' -E 'fzf' -E 'dotbot' \
        -E 'test' -E '**/bin/asdf/**' -E '**/tldr/*' \
        -x shfmt \
        --language-dialect bash \
        --func-next-line --list --write \
        --indent 2 --case-indent --space-redirects \
        --binary-next-line {} \;
      msg_yay "dotfiles have been shfmt formatted!"
      ;;
    *) menu_usage "$USAGE_PREFIX" "${MENU[@]}" ;;
  esac
}

section_check()
{
  USAGE_PREFIX="$SCRIPT check <command>"
  X_HOSTNAME=$(hostname)
  X_ARCH=$(uname)

  MENU=(
    "arch <arch>:Empty <arch> returns current. Exit code 0=match to current, 1=no match."
    "host <host>:Empty <host> returns current. Exit code 0=match to current, 1=no match."
  )

  case "$1" in
    a | arch)
      [[ "$2" = "" ]] && echo "$X_ARCH" && exit 0
      [[ $X_ARCH = "$2" ]] && exit 0 || exit 1
      ;;
    h | host | hostname)
      [[ "$2" = "" ]] && echo "$X_HOSTNAME" && exit 0
      [[ $X_HOSTNAME = "$2" ]] && exit 0 || exit 1
      ;;
    *) menu_usage "$USAGE_PREFIX" "${MENU[@]}" ;;
  esac
}

# Secret menu for visual tests
section_tests()
{
  USAGE_PREFIX="$SCRIPT tests <command>"

  MENU=(
    "msg:List all log functions from shared.sh"
    "params:List all parameters"
  )

  case "$1" in
    params)
      echo "All parameters:"
      for i in "$@"; do
        echo "  $i"
      done
      ;;
    msg)
      msg "msg"
      msg_done "msg_done"
      msg_done_suffix "msg_done_suffix"
      msg_err "msg_err"
      msg_nested "msg_nested"
      msg_nested_done "msg_nested_done"
      msg_ok "msg_ok"
      msg_prompt "msg_prompt"
      msg_prompt_done "msg_prompt_done"
      msg_run "msg_run" "second_param"
      msg_run_done "msg_run_done" "second_param"
      msg_warn "msg_warn"
      msg_yay "msg_yay"
      msg_yay_done "msg_yay_done"
      ;;
    *) menu_usage "$USAGE_PREFIX" "${MENU[@]}" ;;
  esac
}

usage()
{
  echo ""
  msg_prompt "Usage: $SCRIPT <section> <command>"
  echo $"  Empty <command> prints <section> help."
  echo ""
  section_install
  echo ""
  section_asdf
  echo ""
  section_brew
  echo ""
  section_check
  echo ""
  section_dotfiles
  echo ""
  section_docs
  echo ""
  section_helpers
}

main()
{
  SECTION="$1"
  shift
  # The main loop. The first keyword after $0 triggers section, or help.
  case "$SECTION" in
    install) section_install "$@" ;;
    asdf) section_asdf "$@" ;;
    brew) section_brew "$@" ;;
    check) section_check "$@" ;;
    dotfiles) section_dotfiles "$@" ;;
    helpers) section_helpers "$@" ;;
    docs) section_docs "$@" ;;
    tests) section_tests "$@" ;;
    *) usage && exit 0 ;;
  esac
}

main "$@"
