# 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}"

#
# Theme icon definitions
#
set_git_prompt_theme_icons() {
  case "$GIT_PROMPT_THEME" in
    2|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="!"
      ;;
    *)
      BRANCH_ICON=" "
      STAGED_ICON=""
      CONFLICT_ICON="✘"
      CHANGED_ICON="±"
      UNTRACKED_ICON=""
      STASHED_ICON=""
      AHEAD_ICON=""
      BEHIND_ICON=""
      NO_REMOTE_ICON=""
      CLEAN_ICON="✔"
      DIRTY_ICON="✘"
      ;;
  esac
}
set_git_prompt_theme_icons

#
# Caching configuration and setup
#
GIT_CACHE_FILE="/tmp/git_prompt_cache.$$"

# Collected state variables
GIT_STATE_HEAD=""
GIT_STATE_STATUS=""
GIT_STATE_STASH_LIST=""
GIT_STATE_UNTRACKED=""
GIT_STATE_FINGERPRINT=""

# Directories to skip for speed
HEAVY_DIRS=(
  node_modules
  vendor
  dist
  build
  cache
  uploads
  public/uploads
  tmp
  .terraform
  .cache
)

#
# Curent state collection
#
git_collect_state() {

  # HEAD
  GIT_STATE_HEAD=$(git rev-parse HEAD 2>/dev/null) || return 1

  # Full status (branch + staged/modified)
  GIT_STATE_STATUS=$(GIT_OPTIONAL_LOCKS=0 git status --porcelain --branch 2>/dev/null) || return 1

  # Stash list
  GIT_STATE_STASH_LIST=$(GIT_OPTIONAL_LOCKS=0 git stash list 2>/dev/null || true)

  # Smart untracked detection
  # Much faster than git status because it only lists untracked, not modified
  local untracked_raw
  untracked_raw=$(git ls-files --others --exclude-standard 2>/dev/null || true)

  # Apply directory skipping
  GIT_STATE_UNTRACKED=""
  while IFS= read -r f; do
    skip=0
    for d in "${HEAVY_DIRS[@]}"; do
      [[ "$f" == "$d/"* ]] && skip=1 && break
    done
    (( skip == 0 )) && GIT_STATE_UNTRACKED+="$f"$'\n'
  done <<< "$untracked_raw"

  # Build fingerprint
  if command -v sha1sum >/dev/null; then
    GIT_STATE_FINGERPRINT=$(
      printf '%s\n%s\n%s\n%s\n%s\n' \
        "$GIT_STATE_HEAD" \
        "$GIT_STATE_STATUS" \
        "$GIT_STATE_STASH_LIST" \
        "$GIT_STATE_UNTRACKED" \
        "$GIT_PROMPT_THEME" \
      | sha1sum | awk '{print $1}'
    )
  else
    GIT_STATE_FINGERPRINT=$(printf '%s\n%s\n%s\n%s\n' "$GIT_STATE_HEAD" "$GIT_STATE_STATUS" "$GIT_STATE_STASH_LIST" "$GIT_STATE_UNTRACKED")
  fi

  return 0
}

#
# Render logic
#
actual_git_prompt_info_logic() {

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

  local status="$GIT_STATE_STATUS"
  local stash_list="$GIT_STATE_STASH_LIST"

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

  # Parse branch/remote info
  local first_line=${status%%$'\n'*}
  local ahead=0 behind=0
  has_remote=0

  if [[ "$first_line" == "## "* ]]; then
    [[ "$first_line" == *"..."* ]] && has_remote=1
    [[ "$first_line" =~ ahead\ ([0-9]+) ]] && ahead=${BASH_REMATCH[1]}
    [[ "$first_line" =~ behind\ ([0-9]+) ]] && behind=${BASH_REMATCH[1]}
  fi

  is_clean=0
  (( staged == 0 && conflicts == 0 && changed == 0 && untracked == 0 )) && is_clean=1

  #
  # Render themes
  #
  if [[ "$GIT_PROMPT_THEME" == "2" ]]; then
    printf "\[\e[30;44m\]"
    printf "\[\e[97;44m\] %s%s " "$BRANCH_ICON" "$branch"
    ((staged > 0))    && printf " %s%d" "$STAGED_ICON" "$staged"
    ((conflicts > 0)) && printf " %s%d" "$CONFLICT_ICON" "$conflicts"
    ((changed > 0))   && printf " %s%d" "$CHANGED_ICON" "$changed"
    ((untracked > 0)) && printf " %s%d" "$UNTRACKED_ICON" "$untracked"
    ((stashed > 0))   && printf " %s%d" "$STASHED_ICON" "$stashed"
    ((ahead > 0))     && printf " %s%d" "$AHEAD_ICON" "$ahead"
    ((behind > 0))    && printf " %s%d" "$BEHIND_ICON" "$behind"
    ((has_remote == 0)) && printf " %s" "$NO_REMOTE_ICON"
    ((is_clean)) && printf " %s" "$CLEAN_ICON" || printf " %s" "$DIRTY_ICON"
    printf "\[\e[34;107m\]"
    printf "\[\e[30;107m\] \w"
    printf "\[\e[97;49m\]"
    printf "\[\e[0m\]"
  elif [[ "$GIT_PROMPT_THEME" == "3" ]]; then
    printf "\n\[\e[0;37m\]┌──[\e[0m"
    printf "\e[38;5;117m%s%s\e[0m" "$BRANCH_ICON" "$branch"
    ((staged > 0))    && printf " \e[38;5;196m%s%d\e[0m" "$STAGED_ICON" "$staged"
    ((conflicts > 0)) && printf " \e[38;5;196m%s%d\e[0m" "$CONFLICT_ICON" "$conflicts"
    ((changed > 0))   && printf " \e[38;5;69m%s%d\e[0m"  "$CHANGED_ICON" "$changed"
    ((untracked > 0)) && printf " \e[38;5;41m%s%d\e[0m"  "$UNTRACKED_ICON" "$untracked"
    ((stashed > 0))   && printf " \e[38;5;226m%s%d\e[0m" "$STASHED_ICON" "$stashed"
    ((ahead > 0))     && printf " \e[0;37m%s%d\e[0m"     "$AHEAD_ICON" "$ahead"
    ((behind > 0))    && printf " \e[0;37m%s%d\e[0m"     "$BEHIND_ICON" "$behind"
    ((has_remote == 0)) && printf " \e[38;5;250m%s\e[0m" "$NO_REMOTE_ICON"
    ((is_clean)) && printf " \e[0;32m%s\e[0m" "$CLEAN_ICON" || printf " \e[38;5;196m%s\e[0m" "$DIRTY_ICON"
    printf "\e[0;37m]\e[0m \[\e[38;5;178m\]\w\[\e[0m\]"
    printf "\n\[\e[0;37m\]└──\[\e[0m\]"
  else
    printf "\e[0;37m[\e[0m"
    printf "\e[38;5;117m%s%s\e[0m" "$BRANCH_ICON" "$branch"
    ((staged > 0))    && printf " \e[38;5;196m%s%d\e[0m" "$STAGED_ICON" "$staged"
    ((conflicts > 0)) && printf " \e[38;5;196m%s%d\e[0m" "$CONFLICT_ICON" "$conflicts"
    ((changed > 0))   && printf " \e[38;5;69m%s%d\e[0m"  "$CHANGED_ICON" "$changed"
    ((untracked > 0)) && printf " \e[38;5;41m%s%d\e[0m"  "$UNTRACKED_ICON" "$untracked"
    ((stashed > 0))   && printf " \e[38;5;226m%s%d\e[0m" "$STASHED_ICON" "$stashed"
    ((ahead > 0))     && printf " \e[0;37m%s%d\e[0m"     "$AHEAD_ICON" "$ahead"
    ((behind > 0))    && printf " \e[0;37m%s%d\e[0m"     "$BEHIND_ICON" "$behind"
    ((has_remote == 0)) && printf " \e[38;5;250m%s\e[0m" "$NO_REMOTE_ICON"
    ((is_clean)) && printf " \e[0;32m%s\e[0m" "$CLEAN_ICON" || printf " \e[38;5;196m%s\e[0m" "$DIRTY_ICON"
    printf "\e[0;37m]\e[0m"
  fi
}

#
# Main prompt logic
#
git_prompt_info() {
  git rev-parse --is-inside-work-tree &>/dev/null || return

  git_collect_state || return

  local fingerprint="$GIT_STATE_FINGERPRINT"

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

  local output
  output=$(actual_git_prompt_info_logic) || return

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

  echo "$output"
}

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

  GIT_PS1="$(git_prompt_info)"

  #
  # PS1 custom for all themes inside a git repository
  #
  local PS1_CUSTOM='\[\e[0;32m\]\u@\h\[\e[0m\]'

  if [[ -n "$GIT_PS1" ]]; then
    if [[ "$GIT_PROMPT_THEME" == "3" ]]; then
      PS1="${PS1_CUSTOM} ${GIT_PS1} ${PROMPT_CHAR} "
    elif [[ "$GIT_PROMPT_THEME" == "2" ]]; then
      PS1="${PS1_CUSTOM} ${GIT_PS1}\[\e[0m\] ${PROMPT_CHAR} "
    else
      PS1="${PS1_CUSTOM} \[${GIT_PS1}\] \[\e[38;5;178m\]\w\[\e[0m\] ${PROMPT_CHAR} "
    fi

  else
    #
    # PS1 cusom for all themes outside a git repository
    #
    PS1='\[\e[0;32m\]\u@\h\[\e[0m\]:\[\e[38;5;178m\]\w\[\e[0m\] '"${PROMPT_CHAR} "
  fi
}

PROMPT_COMMAND=update_git_prompt

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