#!/usr/bin/env bash
#
# Dotfiles manager and install helper
# (c) Ismo Vuorinen <https://github.com/ivuorinen> 2022
# Licensed under MIT, see LICENSE
#
# vim: ft=bash ts=2 sw=2 et
# 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")

# Detect the current shell
CURRENT_SHELL=$(ps -p $$ -ocomm= | awk -F/ '{print $NF}')

# Function to source files based on the shell
source_file()
{
  local file=$1
  case "$CURRENT_SHELL" in
    fish)
      if [[ -f "$file.fish" ]]; then
        source "$file.fish"
      else
        echo "Fish shell file not found: $file.fish"
        exit 1
      fi
      ;;
    sh | bash | zsh)
      source "$file"
      ;;
    *)
      echo "Unsupported shell: $CURRENT_SHELL"
      exit 1
      ;;
  esac
}

# Modify the source commands to use the new function
source_file "$DOTFILES/config/shared.sh"
source_file "${DOTFILES}/local/bin/msgr"

# Menu builder
menu_builder()
{
  local title=$1
  local commands=("${@:2}")
  local width=60

  printf "\n%s\n" "$(printf '%.s─' $(seq 1 $width))"
  printf "%-${width}s\n" "  $title"
  printf "%s\n" "$(printf '%.s─' $(seq 1 $width))"

  for cmd in "${commands[@]}"; do
    local name=${cmd%%:*}
    local desc=${cmd#*:}
    printf "  %-20s %s\n" "$name" "$desc"
  done
}

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

  MENU=(
    "all:Installs everything in the correct order"
    "cargo:Install rust/cargo packages"
    "cheat-databases:Install cheat external cheatsheet databases"
    "composer:Install composer"
    "fonts:Install programming fonts"
    "gh:Install GitHub CLI Extensions"
    "go:Install Go Packages"
    "imagick:Install ImageMagick CLI"
    "macos:Setup nice macOS defaults"
    "npm-packages:Install NPM Packages"
    "ntfy:Install ntfy"
    "nvm-latest:Install latest lts node using nvm"
    "nvm:Install Node Version Manager (nvm)"
    "z:Install z"
  )

  case "$1" in
    all)
      msgr msg "Starting to install all and reloading configurations..."
      $0 install macos
      $0 install fonts
      $0 brew install
      $0 install cargo
      $0 install go
      $0 install composer
      $0 install cheat-databases
      $0 install nvm
      $0 install npm-packages
      $0 install z
      msgr msg "Reloading configurations again..."
      source "$DOTFILES/config/shared.sh"
      msgr yay "All done!"
      ;;

    cargo)
      msgr run "Installing cargo packages..."
      bash "$DOTFILES/scripts/install-cargo-packages.sh" \
        && msgr yay "cargo packages installed!"
      ;;

    cheat-databases)
      msgr run "Installing cheat databases..."
      for database in "$DOTFILES"/scripts/install-cheat-*; do
        bash "$database" \
          && msgr run_done "Cheat: $database run"
      done
      ;;

    composer)
      msgr run "Installing composer..."
      bash "$DOTFILES/scripts/install-composer.sh" \
        && msgr run_done "composer installed!"
      ;;

    fonts)
      msgr run "Installing fonts..."
      bash "$DOTFILES/scripts/install-fonts.sh" \
        && msgr yay "Installed fonts!"
      ;;

    gh)
      msgr run "Installing GitHub CLI Extensions..."
      bash "$DOTFILES/scripts/install-gh-extensions.sh" \
        && msgr yay "github cli extensions installed!"
      ;;

    go)
      msgr run "Installing Go Packages..."
      bash "$DOTFILES/scripts/install-go-packages.sh" \
        && msgr yay "go packages installed!"
      ;;

    imagick)
      msgr run "Downloading and installing ImageMagick CLI..."
      curl -L https://imagemagick.org/archive/binaries/magick > "$XDG_BIN_HOME/magick" \
        && chmod +x "$XDG_BIN_HOME/magick" \
        && msgr yay "imagick downloaded and installed!"
      ;;

    macos)
      msgr run "Setting up macOS defaults..."
      bash "$DOTFILES/scripts/install-macos-defaults.sh" \
        && msgr yay "macOS defaults set!"
      ;;

    nvm)
      msgr run "Installing nvm..."
      local NVM_VERSION
      NVM_VERSION=$(x-gh-get-latest-version nvm-sh/nvm)
      msgr ok "Latest nvm version: $NVM_VERSION"
      local NVM_INSTALL="https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh"
      local NVM_CURL="curl -o- \"$NVM_INSTALL\" | bash"
      PROFILE=/dev/null bash -c "$NVM_CURL"
      $0 install nvm-latest
      msgr yay "nvm installed!"
      ;;

    nvm-latest)
      msgr run "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"
      msgr yay "latest lts node installed!"
      ;;

    npm-packages)
      msgr run "NPM Packages install started..."
      bash "$DOTFILES/scripts/install-npm-packages.sh" \
        && msgr yay "NPM Packages have been installed!"
      ;;

    z)
      msgr run "Installing z..."
      bash "$DOTFILES/scripts/install-z.sh" \
        && msgr yay "z has been installed!"
      ;;

    *) menu_builder "$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 && msgr yay "Done!"
        ;;

      update)
        brew update && brew outdated && brew upgrade && brew cleanup
        msgr yay "Done!"
        ;;

      updatebundle)
        # Updates .dotfiles/homebrew/Brewfile with descriptions
        brew bundle dump \
          --force \
          --file="$BREWFILE" \
          --cleanup \
          --tap \
          --formula \
          --cask \
          --describe && msgr 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
          # shellcheck disable=SC2199
          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
          msgr yay "No untracked packages found!"
          exit 0
        fi

        echo "Untracked:"
        for f in "${BREW_LIST_UNTRACKED[@]}"; do
          echo "  $f"
        done
        ;;

      autoupdate)
        brew autoupdate delete
        brew autoupdate start 43200 --upgrade --cleanup --immediate
        ;;

      clean) brew bundle cleanup --file="$BREWFILE" && msgr yay "Done!" ;;

      *) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;;
    esac
  }

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

section_helpers()
{
  USAGE_PREFIX="$SCRIPT helpers <command>"
  MENU=(
    "aliases:<shell> (bash, zsh, fish) Show aliases"
    "colors:Show colors"
    "env:Show environment variables"
    "functions:Show functions"
    "aerospace:Show aerospace keybindings"
    "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)"
          ;;
        "fish")
          fish -ic "alias" | sed "s|$HOME|~|"
          ;;
        *)
          echo "$SCRIPT helpers aliases <shell> (bash, zsh, fish)"
          ;;
      esac
      ;;

    "colors")
      max=255
      start=0

      while [ "$start" -le "$max" ]; do
        for i in $(seq "$start" $((start + 9))); do
          if [ "$i" -le "$max" ]; then
            # Outputs colored number
            # printf " \e[38;5;%sm%4s\e[0m" "$i" "$i"

            # Outputs colored block with number inside
            # printf " \e[48;5;%sm\e[38;5;15m%5s \e[0m" "$i" "$i"

            # Outputs colored block and color number
            # printf " \e[48;5;%sm   \e[0m %3d" "$i" "$i"

            # Outputs color number and colored block
            printf "%3d \e[48;5;%sm   \e[0m " "$i" "$i"
          fi
        done
        printf "\n"
        start=$((start + 10))
      done
      ;;

    "env") env | sort ;;
    "functions") declare -F ;;
    "aerospace") cat "$DOTFILES/docs/aerospace-keybindings.md" ;;
    "nvim") cat "$DOTFILES/docs/nvim-keybindings.md" ;;
    "tmux") cat "$DOTFILES/docs/tmux-keybindings.md" ;;
    "wezterm") cat "$DOTFILES/docs/wezterm-keybindings.md" ;;
    *) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;;
  esac
}

section_apt()
{
  USAGE_PREFIX="$SCRIPT apt <command>"
  MENU=(
    "upkeep:Run update, upgrade, autoremove and clean"
    'install:Install packages from $DOTFILES/tools/apt.txt'
    "update:Update apt packages"
    "upgrade:Upgrade apt packages"
    "autoremove:Remove unused apt packages"
    "clean:Clean apt cache"
  )

  x-have apt && {
    case "$1" in
      upkeep)
        sudo apt update \
          && sudo apt upgrade -y \
          && sudo apt autoremove -y \
          && sudo apt clean
        ;;

      install)
        # if apt.txt is not found, exit
        [ ! -f "$DOTFILES/tools/apt.txt" ] && msgr err "apt.txt not found" && exit 0

        # Load apt.txt, remove comments (even if trailing comment) and empty lines.
        #
        # Ignoring "Quote this to prevent word splitting."
        # shellcheck disable=SC2046
        sudo apt install \
          -y $(
            grep -vE '^\s*#' "$DOTFILES/tools/apt.txt" \
              | sed -e 's/#.*//' \
              | tr '\n' ' '
          )

        # If there's a apt.txt file under hosts/$hostname/apt.txt,
        # run install on those lines too.
        HOSTNAME=$(hostname -s)
        HOST_APT="$DOTFILES/hosts/$HOSTNAME/apt.txt"
        [[ -f $HOST_APT ]] && {
          # shellcheck disable=SC2046
          sudo apt install -y $(
            grep -vE '^\s*#' "$HOST_APT" \
              | sed -e 's/#.*//' \
              | tr '\n' ' '
          )
        }

        # Try this for an alternative way to install packages
        # xargs -a <(awk '! /^ *(#|$)/' "$packagelist") -r -- sudo apt-get install -y
        ;;

      update) sudo apt update ;;
      upgrade) sudo apt upgrade -y ;;
      autoremove) sudo apt autoremove -y ;;
      clean) sudo apt clean ;;
      *) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;;
    esac
  }
  ! x-have apt && menu_builder "$USAGE_PREFIX" "apt not available on this system"
}

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

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

  case "$1" in
    all)
      $0 docs aerospace
      $0 docs tmux
      $0 docs nvim
      $0 docs wezterm
      ;;
    aerospace) bash "$DOTFILES/scripts/create-aerospace-keymaps.php" ;;
    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_builder "$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)
      msgr run "Running all formatters"
      $0 dotfiles yamlfmt
      $0 dotfiles shfmt
      msgr run_done "...done!"
      ;;

    reset_all)
      msgr ok "Running all reset commands"
      $0 dotfiles reset_nvim
      ;;

    reset_nvim)
      msgr run "Cleaning nvim state, cache and config"
      rm -rf \
        ~/.local/share/nvim \
        ~/.local/state/nvim \
        ~/.cache/nvim \
        ~/.config/nvim
      msgr ok "Deleted old nvim files (share, state and cache + config)"
      ln -s "$DOTFILES/config/nvim" ~/.config/nvim
      msgr ok "Linked nvim and astronvim"
      x-have npm && $0 install npm
      msgr ok "Installed packages"
      msgr run_done "nvim reset!"
      ;;

    yamlfmt)
      # format yaml files
      x-have yamlfmt && yamlfmt -conf "$DOTFILES/.yamlfmt"
      ! x-have yamlfmt && msgr err "yamlfmt not found"
      ;;

    shfmt)
      # If system doesn't have fd or shfmt installed, exit
      ! x-have fd && msgr err "fd not found, install it to continue"
      ! x-have shfmt && msgr err "shfmt not found, install it to continue"
      # 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 '**/tldr/*' \
        -x shfmt \
        --language-dialect bash \
        --func-next-line --list --write \
        --indent 2 --case-indent --space-redirects \
        --binary-next-line {} \;
      msgr yay "dotfiles have been shfmt formatted!"
      ;;

    *) menu_builder "$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_builder "$USAGE_PREFIX" "${MENU[@]}" ;;
  esac
}

section_scripts()
{
  USAGE_PREFIX="$SCRIPT scripts <command>"

  # Get description from a file
  get_script_description()
  {
    local file
    local desc
    file="$1"
    desc=$(sed -n '/@description/s/.*@description *\(.*\)/\1/p' "$file" | head -1)
    echo "${desc:-No description available}"
  }

  # Collect scripts and their descriptions
  declare -A SCRIPT_MENU
  for script in "$DOTFILES/scripts/install-"*.sh; do
    if [ -f "$script" ]; then
      name=$(basename "$script" .sh | sed 's/install-//')
      desc=$(get_script_description "$script")
      SCRIPT_MENU[$name]="$desc"
    fi
  done

  case "$1" in
    "")
      # Show the menu
      local menu_items=()
      for name in "${!SCRIPT_MENU[@]}"; do
        menu_items+=("$name:${SCRIPT_MENU[$name]}")
      done
      menu_builder "$USAGE_PREFIX" "${menu_items[@]}"
      ;;
    *)
      # Run the chosen script
      script_path="$DOTFILES/scripts/install-$1.sh"
      if [ -f "$script_path" ]; then
        bash "$script_path"
      else
        msgr err "Script not found: $1"
      fi
      ;;
  esac
}

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

  MENU=(
    "msgr:List all available msgr message types"
    "params:List all parameters"
  )

  case "$1" in
    params)
      echo "All parameters:"
      for i in "$@"; do
        echo "  $i"
      done
      ;;
    msg)
      # shellcheck disable=SC1010
      msgr done "msgr done"
      msgr done_suffix "msgr done_suffix"
      msgr err "msgr err"
      msgr nested "msgr nested"
      msgr nested_done "msgr nested_done"
      msgr ok "msgr ok"
      msgr prompt "msgr prompt"
      msgr prompt_done "msgr prompt_done"
      msgr run "msgr run" "second_param"
      msgr run_done "msgr run_done" "second_param"
      msgr warn "msgr warn"
      msgr yay "msgr yay"
      msgr yay_done "msgr yay_done"
      ;;
    *) menu_builder "$USAGE_PREFIX" "${MENU[@]}" ;;
  esac
}

usage()
{
  echo ""
  msgr prompt "Usage: $SCRIPT <section> <command>"
  echo $"  Empty <command> prints <section> help."
  echo ""
  section_install
  echo ""
  section_apt
  echo ""
  section_brew
  echo ""
  section_check
  echo ""
  section_dotfiles
  echo ""
  section_docs
  echo ""
  section_scripts
  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 "$@" ;;
    apt) section_apt "$@" ;;
    brew) section_brew "$@" ;;
    check) section_check "$@" ;;
    dotfiles) section_dotfiles "$@" ;;
    helpers) section_helpers "$@" ;;
    docs) section_docs "$@" ;;
    scripts) section_scripts "$@" ;;
    tests) section_tests "$@" ;;
    *) usage && exit 0 ;;
  esac
}

main "$@"
