# Author        : Allan Christensen
# First Created : 24-06-2025 (DD-MM-YYYY)
# Description   : Bash Git prompt for Linux
# License       : MIT License

#
# Set default theme to 1 if it's not already set
#
GIT_PROMPT_THEME="${GIT_PROMPT_THEME:-1}"

#
# Outside Git prompt configuration
# Purpose: privilege awareness (NOT git state)
# These apply ONLY when this script is sourced
#

#
# ANSI-safe colors for OUTSIDE Git prompts
#
C_USER_PS1_COLOR='\[\e[0;32m\]'
C_ROOT_PS1_COLOR='\[\e[0;31m\]'
C_PATH_COLOR='\[\e[38;5;178m\]'
C_RESET='\[\e[0m\]'

#
# Outside Git directory prompts
#
PS1_OUTSIDE_USER="${C_USER_PS1_COLOR}\u@\h${C_RESET} ${C_PATH_COLOR}\w${C_RESET} \$ "
PS1_OUTSIDE_ROOT="${C_ROOT_PS1_COLOR}\u@\h${C_RESET} ${C_PATH_COLOR}\w${C_RESET} # "

#
# ANSI-safe base colors for GIT prompts only
#
C_USER='\[\e[0;32m\]'
C_PATH='\[\e[38;5;178m\]'

#
# Default git reset (theme-aware override below)
#
C_GIT_RESET="$C_RESET"

#
# Powerline glyph
#
PL_SEP=""

#
# Theme icon definitions
#
set_git_prompt_theme_icons() {
  case "$GIT_PROMPT_THEME" in
    1)
      BRANCH_ICON=" "
      STAGED_ICON=""
      CONFLICT_ICON="≠"
      CHANGED_ICON="±"
      UNTRACKED_ICON="…"
      STASHED_ICON=""
      AHEAD_ICON=""
      BEHIND_ICON=""
      NO_REMOTE_ICON="󰊠"
      CLEAN_ICON="✔"
      DIRTY_ICON="✘"
      ;;
    2)
      BRANCH_ICON=" "
      STAGED_ICON=""
      CONFLICT_ICON="≠"
      CHANGED_ICON="±"
      UNTRACKED_ICON="…"
      STASHED_ICON=""
      AHEAD_ICON=""
      BEHIND_ICON=""
      NO_REMOTE_ICON="󰊠"
      CLEAN_ICON="✔"
      DIRTY_ICON="✘"
      ;;
    3)
      BRANCH_ICON=" "
      STAGED_ICON=""
      CONFLICT_ICON="≠"
      CHANGED_ICON="±"
      UNTRACKED_ICON="…"
      STASHED_ICON=""
      AHEAD_ICON=""
      BEHIND_ICON=""
      NO_REMOTE_ICON="󰊠"
      CLEAN_ICON="✔"
      DIRTY_ICON="✘"
      ;;
    4)
      BRANCH_ICON="⎇  "
      STAGED_ICON="o"
      CONFLICT_ICON="!"
      CHANGED_ICON="±"
      UNTRACKED_ICON="…"
      STASHED_ICON="☰"
      AHEAD_ICON="⇡"
      BEHIND_ICON="⇣"
      NO_REMOTE_ICON="-"
      CLEAN_ICON="✓"
      DIRTY_ICON="✗"
      ;;
    5)
      BRANCH_ICON=":: "
      STAGED_ICON="+"
      CONFLICT_ICON="x"
      CHANGED_ICON="*"
      UNTRACKED_ICON="?"
      STASHED_ICON="s"
      AHEAD_ICON="a"
      BEHIND_ICON="b"
      NO_REMOTE_ICON="no"
      CLEAN_ICON="ok"
      DIRTY_ICON="!"
      ;;
  esac
}

#
# Theme-aware git colors (Git context ONLY)
#
set_git_prompt_theme_colors() {
  case "$GIT_PROMPT_THEME" in
    1)
      GC_BRANCH='\[\e[38;5;117m\]'
      GC_STAGED='\[\e[38;5;196m\]'
      GC_CONFLICT='\[\e[38;5;196m\]'
      GC_CHANGED='\[\e[38;5;69m\]'
      GC_UNTRACKED='\[\e[38;5;41m\]'
      GC_STASHED='\[\e[38;5;226m\]'
      GC_AHEAD='\[\e[0;37m\]'
      GC_BEHIND='\[\e[0;37m\]'
      GC_NOREMOTE='\[\e[38;5;250m\]'
      GC_CLEAN='\[\e[0;32m\]'
      GC_DIRTY='\[\e[38;5;196m\]'
      C_GIT_RESET="$C_RESET"
      ;;
    2)
      PL_GIT_BG='\[\e[44m\]'
      PL_GIT_FG='\[\e[97m\]'
      PL_GIT_SEP='\[\e[30;44m\]'
      PL_PATH_BG='\[\e[107m\]'
      PL_PATH_FG='\[\e[30m\]'
      PL_PATH_SEP='\[\e[34;107m\]'
      PL_END_SEP='\[\e[97;49m\]'
      GC_BRANCH='\[\e[97m\]'
      GC_STAGED='\[\e[97m\]'
      GC_CONFLICT='\[\e[97m\]'
      GC_CHANGED='\[\e[97m\]'
      GC_UNTRACKED='\[\e[97m\]'
      GC_STASHED='\[\e[97m\]'
      GC_AHEAD='\[\e[97m\]'
      GC_BEHIND='\[\e[97m\]'
      GC_NOREMOTE='\[\e[97m\]'
      GC_CLEAN='\[\e[97m\]'
      GC_DIRTY='\[\e[97m\]'
      C_GIT_RESET="${PL_GIT_BG}${PL_GIT_FG}"
      ;;
    3)
      GC_BRANCH='\[\e[38;5;117m\]'
      GC_STAGED='\[\e[38;5;196m\]'
      GC_CONFLICT='\[\e[38;5;196m\]'
      GC_CHANGED='\[\e[38;5;69m\]'
      GC_UNTRACKED='\[\e[38;5;41m\]'
      GC_STASHED='\[\e[38;5;226m\]'
      GC_AHEAD='\[\e[0;37m\]'
      GC_BEHIND='\[\e[0;37m\]'
      GC_NOREMOTE='\[\e[38;5;250m\]'
      GC_CLEAN='\[\e[0;32m\]'
      GC_DIRTY='\[\e[38;5;196m\]'
      C_GIT_RESET="$C_RESET"
      ;;
    4)
      GC_BRANCH='\[\e[0;37m\]'
      GC_STAGED='\[\e[0;31m\]'
      GC_CONFLICT='\[\e[0;31m\]'
      GC_CHANGED='\[\e[0;33m\]'
      GC_UNTRACKED='\[\e[0;32m\]'
      GC_STASHED='\[\e[0;36m\]'
      GC_AHEAD='\[\e[0;37m\]'
      GC_BEHIND='\[\e[0;37m\]'
      GC_NOREMOTE='\[\e[0;37m\]'
      GC_CLEAN='\[\e[0;32m\]'
      GC_DIRTY='\[\e[0;31m\]'
      C_GIT_RESET="$C_RESET"
      ;;
    5)
      GC_BRANCH='\[\e[0;37m\]'
      GC_STAGED='\[\e[0;31m\]'
      GC_CONFLICT='\[\e[0;31m\]'
      GC_CHANGED='\[\e[0;33m\]'
      GC_UNTRACKED='\[\e[0;32m\]'
      GC_STASHED='\[\e[0;36m\]'
      GC_AHEAD='\[\e[0;37m\]'
      GC_BEHIND='\[\e[0;37m\]'
      GC_NOREMOTE='\[\e[0;37m\]'
      GC_CLEAN='\[\e[0;32m\]'
      GC_DIRTY='\[\e[0;31m\]'
      C_GIT_RESET="$C_RESET"
      ;;
  esac
}

set_git_prompt_theme_icons
set_git_prompt_theme_colors

#
# Cache (per shell tree)
#
GIT_CACHE_FILE="/dev/shm/git_prompt_${USER}_${PPID}.cache"

#
# State collection
#
git_collect_state() {

  GIT_STATE_HEAD=$(git rev-parse HEAD 2>/dev/null) || return 1
  GIT_STATE_STATUS=$(git status --porcelain=v1 --branch 2>/dev/null) || return 1
  GIT_STATE_STASH_LIST=$(git stash list 2>/dev/null || true)

  local untracked_raw
  untracked_raw=$(git ls-files --others --exclude-standard 2>/dev/null || true)

  GIT_STATE_UNTRACKED=""
  while IFS= read -r f; do
    for d in node_modules vendor dist build cache uploads public/uploads tmp .terraform .cache; do
      [[ "$f" == "$d/"* ]] && continue 2
    done
    GIT_STATE_UNTRACKED+="$f"$'\n'
  done <<< "$untracked_raw"

  GIT_STATE_FINGERPRINT=$(printf '%s%s%s%s%s' \
    "$GIT_STATE_HEAD" \
    "$GIT_STATE_STATUS" \
    "$GIT_STATE_STASH_LIST" \
    "$GIT_STATE_UNTRACKED" \
    "$GIT_PROMPT_THEME" | cksum | awk '{print $1}')
}

#
# Render logic
#
actual_git_prompt_info_logic() {

  local branch
  branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "(detached)")

  local staged conflicts changed untracked stashed
  staged=$(grep -cE '^[AMDR] ' <<<"$GIT_STATE_STATUS")
  conflicts=$(grep -cE '^UU ' <<<"$GIT_STATE_STATUS")
  changed=$(grep -cE '^.[MD] ' <<<"$GIT_STATE_STATUS")
  untracked=$(grep -c . <<<"$GIT_STATE_UNTRACKED")
  stashed=$(grep -c . <<<"$GIT_STATE_STASH_LIST")

  local first_line=${GIT_STATE_STATUS%%$'\n'*}
  local ahead=0 behind=0 has_remote=0
  [[ "$first_line" == *"..."* ]] && has_remote=1
  [[ "$first_line" =~ ahead\ ([0-9]+) ]] && ahead=${BASH_REMATCH[1]}
  [[ "$first_line" =~ behind\ ([0-9]+) ]] && behind=${BASH_REMATCH[1]}

  local p=""
  p+="${GC_BRANCH}${BRANCH_ICON}${branch}${C_GIT_RESET}"

  ((staged > 0))    && p+=" ${GC_STAGED}${STAGED_ICON}${staged}${C_GIT_RESET}"
  ((conflicts > 0)) && p+=" ${GC_CONFLICT}${CONFLICT_ICON}${conflicts}${C_GIT_RESET}"
  ((changed > 0))   && p+=" ${GC_CHANGED}${CHANGED_ICON}${changed}${C_GIT_RESET}"
  ((untracked > 0)) && p+=" ${GC_UNTRACKED}${UNTRACKED_ICON}${untracked}${C_GIT_RESET}"
  ((stashed > 0))   && p+=" ${GC_STASHED}${STASHED_ICON}${stashed}${C_GIT_RESET}"
  ((ahead > 0))     && p+=" ${GC_AHEAD}${AHEAD_ICON}${ahead}${C_GIT_RESET}"
  ((behind > 0))    && p+=" ${GC_BEHIND}${BEHIND_ICON}${behind}${C_GIT_RESET}"
  ((has_remote == 0)) && p+=" ${GC_NOREMOTE}${NO_REMOTE_ICON}${C_GIT_RESET}"

  if (( staged + conflicts + changed + untracked == 0 )); then
    p+=" ${GC_CLEAN}${CLEAN_ICON}${C_GIT_RESET}"
  else
    p+=" ${GC_DIRTY}${DIRTY_ICON}${C_GIT_RESET}"
  fi

  echo "$p"
}

#
# Prompt logic (cache-safe)
#
git_prompt_info() {
  git_collect_state || return

  if [[ -f "$GIT_CACHE_FILE" ]]; then
    local cached_fp
    read -r cached_fp < "$GIT_CACHE_FILE"
    if [[ "$cached_fp" == "$GIT_STATE_FINGERPRINT" ]]; then
      sed '1d' "$GIT_CACHE_FILE"
      return
    fi
  fi

  local output
  output=$(actual_git_prompt_info_logic)

  {
    echo "$GIT_STATE_FINGERPRINT"
    echo "$output"
  } > "$GIT_CACHE_FILE"

  echo "$output"
}

#
# Update prompt
#
update_git_prompt() {
  local PROMPT_CHAR='$'
  [[ $EUID -eq 0 ]] && PROMPT_CHAR='#'

  local GIT_PS1
  GIT_PS1=$(git_prompt_info)

  #
  # Outside Git repository
  #
  if [[ -z "$GIT_PS1" ]]; then
    if [[ $EUID -eq 0 ]]; then
      PS1="$PS1_OUTSIDE_ROOT"
    else
      PS1="$PS1_OUTSIDE_USER"
    fi
    return
  fi

  #
  # Theme 2: Powerline
  #
  if [[ "$GIT_PROMPT_THEME" -eq 2 ]]; then
    PS1="${PL_GIT_SEP}${PL_SEP}${PL_GIT_BG}${PL_GIT_FG} ${GIT_PS1} ${PL_PATH_SEP}${PL_SEP}${PL_PATH_BG}${PL_PATH_FG} \w ${PL_END_SEP}${PL_SEP}${C_RESET} ${PROMPT_CHAR} "
    return
  fi

  #
  # Theme 3: Two-liner
  #
  if [[ "$GIT_PROMPT_THEME" -eq 3 ]]; then
    PS1="┌─ ${C_USER}\u@\h${C_RESET} [${GIT_PS1}] ${C_PATH}\w${C_RESET}\n└─ ${PROMPT_CHAR} "
    return
  fi

  #
  # Themes 1, 4, 5
  #
  PS1="${C_USER}\u@\h${C_RESET} [${GIT_PS1}] ${C_PATH}\w${C_RESET} ${PROMPT_CHAR} "
}

PROMPT_COMMAND='update_git_prompt; printf "\e[0m"'

#
# Theme switcher
#
gpchange() {
  local theme="$1"

  if [[ "$theme" =~ ^[1-5]$ ]]; then
    export GIT_PROMPT_THEME="$theme"
    set_git_prompt_theme_icons
    set_git_prompt_theme_colors
    update_git_prompt
    echo "Switched to Git prompt theme $theme"
  else
    echo "Usage: gpchange <1-5>"
  fi
}

#
# End of script
#

