#!/usr/bin/env bash
# Get git repository status for all subdirectories
# recursively in specified dir.
#
# Check the default dir:
#   `git-dirty.sh`
# Check specific dir:
#   `git-dirty.sh ~/Projects`
# Override default dir with env:
#   `GIT_DIRTY_DIR=$HOME/Projects git-dirty.sh`
#
# If you want to skip directory from checks, just add `.ignore` file next
# to the `.git` folder. ProTip: Add `.ignore` to your global `.gitignore`.
#
# The script automatically skips folders:
#   node_modules, vendor
#
# Copyright Ismo Vuorinen (2023)
# Licensed under the MIT License

# More robust error handling
set -o pipefail # Fail if any command in a pipe fails

# SET Defaults:
# Default dir to check, can be overridden in env (.bashrc, .zshrc, ...)
: "${GIT_DIRTY_DIR:=$HOME/Code}"
: "${VERBOSE:=0}"                                                            # Enable verbosity with VERBOSE=1
: "${PARALLEL:=0}"                                                           # Enable parallel processing with PARALLEL=1
: "${GIT_DIRTY_DEPTH:=5}"                                                    # How deep to show non-git directories
: "${GIT_DIRTY_MAXDEPTH:=15}"                                                # Maximum recursion depth
: "${GIT_DIRTY_CHECK_STASH:=0}"                                              # Whether to check if there are stashed changes
: "${GIT_DIRTY_CHECK_UNTRACKED:=1}"                                          # Whether to report untracked files
: "${GIT_DIRTY_EXCLUDE:=node_modules vendor .cache build dist .tests .test}" # Directories to exclude
: "${GIT_DIRTY_COLOR:=1}"                                                    # Enable colorized output (0 to disable)
: "${GIT_DIRTY_TRUNCATE:=1}"                                                 # Truncate paths to be relative to base dir
: "${GIT_DIRTY_SHOW_BRANCH:=1}"                                              # Show current branch name after directory
: "${GIT_DIRTY_MAIN_BRANCHES:=main master trunk}"                            # List of main branches, don't show names

# Load configuration from file
config_file="${XDG_CONFIG_HOME:-$HOME/.config}/git-dirty/config"
if [[ -f $config_file ]]; then
  # shellcheck source=/dev/null
  source "$config_file"
fi

# Check if colors should be enabled
use_color()
{
  [[ $GIT_DIRTY_COLOR -eq 1 && -t 1 ]]
}

# Color printing function
print_color()
{
  local color text
  color="$1"
  text="$2"

  if use_color; then
    printf "\033[%sm%s\033[0m" "$color" "$text"
  else
    printf "%s" "$text"
  fi
}

# Define colored icons
if use_color; then
  GIT_CLEAN=$(print_color "32" "✅") # Green check mark
  GIT_DIRTY=$(print_color "31" "❌") # Red X
  NOT_GIT=$(print_color "33" "⚠️")  # Yellow warning
  BRANCH_COLOR="35"                 # Purple for branch names
  STATUS_COLOR="33"                 # Yellow for status codes
else
  GIT_CLEAN="✅"
  GIT_DIRTY="❌"
  NOT_GIT="⚠️"
  BRANCH_COLOR=""
  STATUS_COLOR=""
fi

# Global base directory (used for path truncation)
BASE_DIR=""

# Temporary file for storing results when not using parallel mode
RESULTS_FILE=""

# Keep track of directories that contain git repos
declare -A DIR_HAS_REPOS

# Record start time
START_TIME=$(date +%s)

# Logging functions
log_error()
{
  print_color "31" "ERROR:" >&2
  echo " $*" >&2
}

log_info()
{
  if [[ $VERBOSE -eq 1 ]]; then
    print_color "36" "INFO:"
    echo " $*"
  fi
}

log_warn()
{
  print_color "33" "WARNING:" >&2
  echo " $*" >&2
}

# Truncate a path to be relative to base directory
truncate_path()
{
  local path
  path="$1"

  if [[ $GIT_DIRTY_TRUNCATE -eq 1 && -n $BASE_DIR && $path == "$BASE_DIR"* ]]; then
    # First show the base dir only for the root
    if [[ $path == "$BASE_DIR" ]]; then
      echo "$path"
    else
      # For other paths, make them relative to base dir
      echo "${path#"$BASE_DIR"/}"
    fi
  else
    # If truncation is disabled, show full paths
    echo "$path"
  fi
}

# Check if a branch is a main branch
is_main_branch()
{
  local branch
  branch="$1"
  for main in $GIT_DIRTY_MAIN_BRANCHES; do
    if [[ $branch == "$main" ]]; then
      return 0 # It's a main branch (exit code 0 means true in bash)
    fi
  done
  return 1 # Not a main branch (exit code 1 means false in bash)
}

# Error handling function
handle_error()
{
  local exit_code line_no
  exit_code=$1
  line_no=$2

  case $exit_code in
    1) log_error "General error at line $line_no" ;;
    126) log_error "Command not executable at line $line_no" ;;
    127) log_error "Command not found at line $line_no" ;;
    128) log_error "Invalid argument to exit at line $line_no" ;;
    130) log_info "Script terminated by Ctrl+C" ;;
    139 | 11) log_error "Segmentation fault occurred at line $line_no" ;;
    *) log_error "Unknown error $exit_code at line $line_no" ;;
  esac
}

# Get current branch name
get_branch_name()
{
  git symbolic-ref --short HEAD 2> /dev/null \
    || git describe --tags --exact-match 2> /dev/null \
    || git rev-parse --short HEAD 2> /dev/null \
    || echo "detached"
}

# More comprehensive git status check
check_git_status()
{
  local repo_path status branch
  repo_path="$1"
  status=""

  # Check for dirty files (modified/staged)
  git -C "$repo_path" diff --no-ext-diff --quiet 2> /dev/null || status+="M"
  git -C "$repo_path" diff --no-ext-diff --cached --quiet 2> /dev/null || status+="S"

  # Check for untracked files if enabled
  if [[ $GIT_DIRTY_CHECK_UNTRACKED -eq 1 ]]; then
    [[ -n $(git -C "$repo_path" ls-files --others --exclude-standard 2> /dev/null) ]] && status+="?"
  fi

  # Check for stashed changes if enabled
  if [[ $GIT_DIRTY_CHECK_STASH -eq 1 ]]; then
    [[ -f "$(git -C "$repo_path" rev-parse --git-dir 2> /dev/null)/refs/stash" ]] && status+="$"
  fi

  # Check for unpushed commits
  branch=$(git -C "$repo_path" symbolic-ref --short HEAD 2> /dev/null)
  if [[ -n $branch ]]; then
    git -C "$repo_path" rev-list "@{u}".. --count 2> /dev/null \
      | grep -q '^0$' 2> /dev/null \
      || status+="↑"
  fi

  # Return the status
  echo "$status"
}

# Function to check if a directory should be excluded
should_exclude()
{
  local dir basename
  dir="$1"
  basename="${dir##*/}"

  # Quick path for node_modules and vendor which are common
  [[ $basename == "node_modules" || $basename == "vendor" ]] && return 0

  # Check explicit exclude list
  for exclude in $GIT_DIRTY_EXCLUDE; do
    [[ $basename == "$exclude" ]] && return 0
  done

  # Check for ignore file - only if we're not already checking exclude patterns
  [[ -e "$dir/.ignore" ]] && return 0

  return 1
}

# Format a path for sorting to maintain tree structure
# Add a special suffix to directory entries to ensure they appear first
format_path_for_sorting()
{
  local path is_dir normalized

  path="$1"
  is_dir="$2" # 1 for directory, 0 for file/git repo

  # Normalize path to use / separator and remove trailing slashes
  normalized=$(echo "$path" | tr -s '/' | sed 's:/*$::')

  # Add a suffix to control sort order - directories first
  if [[ $is_dir -eq 1 ]]; then
    echo "${normalized}/0" # Make directories come first in sort by adding '/0'
  else
    echo "${normalized}/1" # Regular git repos get '/1' suffix
  fi
}

# Extract the basename of a path
get_basename()
{
  local path
  path="$1"
  basename "$path"
}

# Format time duration in a human-readable way
format_duration()
{
  local seconds minutes remaining_seconds
  seconds=$1
  minutes=$((seconds / 60))
  remaining_seconds=$((seconds % 60))

  if [[ $minutes -gt 0 ]]; then
    echo "${minutes}m ${remaining_seconds}s"
  else
    echo "${seconds}s"
  fi
}

# Mark a directory and all its parents as containing git repos
mark_directory_with_repos()
{
  local dir path
  dir="$1"
  path="$dir"

  while [[ $path != "$BASE_DIR" && $path != "/" && -n $path ]]; do
    DIR_HAS_REPOS["$path"]=1
    path=$(dirname "$path")
  done

  # Mark the base directory too
  DIR_HAS_REPOS["$BASE_DIR"]=1
}

# Better output formatting with tree-like structure and saving to file
print_result()
{
  local depth path icon status branch indent \
    is_dir display_path basename_path sort_path \
    output_line status_display

  depth=$1
  path=$2
  icon=$3
  status=$4
  branch=$5
  indent=$(printf "%*s" $((depth * 2)) "")
  is_dir=0

  # Only continue if this is a git repository or a container directory
  if [[ $icon == "$NOT_GIT" && $VERBOSE -eq 0 && -z ${DIR_HAS_REPOS["$path"]} ]]; then
    return
  fi

  # Mark as directory if it's not a git repo
  if [[ $icon == "$NOT_GIT" ]]; then
    is_dir=1
  fi

  # Truncate path if enabled
  display_path=$(truncate_path "$path")

  # For display, we only want the basename of the path
  basename_path=$(get_basename "$display_path")

  # For root node, use the full path
  if [[ $path == "$BASE_DIR" ]]; then
    basename_path="$display_path"
  fi

  # Format path for sorting - directories first
  sort_path=$(format_path_for_sorting "$display_path" "$is_dir")

  # Format output with status, branch name if available and enabled
  if [[ $icon == "$NOT_GIT" ]]; then
    # Non-git directory
    output_line=$(printf "%s %s %s" "$indent" "$icon" "$basename_path")
  elif [[ -z $status ]]; then
    # Clean git repo
    if [[ $GIT_DIRTY_SHOW_BRANCH -eq 1 && -n $branch ]]; then
      # Only show branch if it's not a main branch
      if is_main_branch "$branch"; then
        output_line=$(printf "%s %s %s" "$indent" "$icon" "$basename_path")
      else
        output_line=$(printf "%s %s %s %s" "$indent" "$icon" "$basename_path" "$(print_color "$BRANCH_COLOR" "($branch)")")
      fi
    else
      output_line=$(printf "%s %s %s" "$indent" "$icon" "$basename_path")
    fi
  else
    # Dirty git repo
    status_display="$(print_color "$STATUS_COLOR" "[$status]")"
    if [[ $GIT_DIRTY_SHOW_BRANCH -eq 1 && -n $branch ]]; then
      # Only show branch if it's not a main branch
      if is_main_branch "$branch"; then
        output_line=$(printf "%s %s %s %s" "$indent" "$icon" "$basename_path" "$status_display")
      else
        output_line=$(printf "%s %s %s %s %s" "$indent" "$icon" "$basename_path" "$status_display" "$(print_color "$BRANCH_COLOR" "($branch)")")
      fi
    else
      output_line=$(printf "%s %s %s %s" "$indent" "$icon" "$basename_path" "$status_display")
    fi
  fi

  # If we're collecting results for sorting, add to file with a path-based sorting key
  if [[ -n $RESULTS_FILE ]]; then
    echo "$sort_path:$output_line" >> "$RESULTS_FILE"
  else
    # Direct output
    echo "$output_line"
  fi
}

# Display sorted results from the results file
display_sorted_results()
{
  if [[ -n $RESULTS_FILE && -f $RESULTS_FILE ]]; then
    # Sort by path for proper tree hierarchy, with our special suffix ensuring directories come first
    LC_ALL=C sort "$RESULTS_FILE" | cut -d: -f2- | sed 's/^://'
    rm -f "$RESULTS_FILE"
  fi
}

# Function to check the git status of a directory
# $1 - directory (string)
# $2 - depth (number, optional)
git_dirty()
{
  local d depth orig_dir status_text branch_name icon elapsed_time rate has_git_repos

  d="$1"
  depth="${2:-0}"
  orig_dir=$(pwd)

  # Initialize a global counter for git repos if at root level
  if [[ $depth -eq 0 ]]; then
    GIT_REPO_COUNT=0
    echo "Finding git repositories..."

    # Create a temporary file for results if needed
    if [[ -z $RESULTS_FILE ]]; then
      RESULTS_FILE=$(mktemp)
    fi

    # Reset the directory tracking
    declare -g -A DIR_HAS_REPOS=()
  fi

  if [[ ! -d $d ]]; then
    return
  fi

  # Check if directory should be excluded
  if should_exclude "$d"; then
    log_info "Skipping excluded directory: $d"
    return
  fi

  # Check depth limit
  if [[ $depth -gt $GIT_DIRTY_MAXDEPTH ]]; then
    log_info "Max depth reached at $d"
    return
  fi

  cd "$d" 2> /dev/null || {
    log_warn "Cannot access $d"
    return
  }

  # If we have `.git` folder, check it
  if [[ -d ".git" ]]; then
    GIT_REPO_COUNT=$((GIT_REPO_COUNT + 1))

    # Mark this directory and all parent directories as containing repos
    mark_directory_with_repos "$d"

    # Get status and branch information
    status_text=$(check_git_status "$d")
    branch_name=$(get_branch_name)

    # Determine icon based on status
    if [[ -z $status_text ]]; then
      icon="$GIT_CLEAN"
    else
      icon="$GIT_DIRTY"
    fi

    # Add to results
    print_result "$depth" "$(pwd)" "$icon" "$status_text" "$branch_name"

    # Show count when the repo count changes
    if [[ $depth -eq 0 || $((GIT_REPO_COUNT % 5)) -eq 0 ]]; then
      elapsed_time=$(($(date +%s) - START_TIME))
      rate=$((GIT_REPO_COUNT > 0 && elapsed_time > 0 ? GIT_REPO_COUNT / elapsed_time : 0))
      echo -ne "Found $GIT_REPO_COUNT git repositories... (${rate} repos/sec)\r"
    fi
  else
    # Process all subdirectories recursively (sorted)
    local subdirs=()
    while IFS= read -r subdir; do
      if [[ -d $subdir && ! -L $subdir ]]; then # Not a symlink
        subdirs+=("$subdir")
      fi
    done < <(find . -mindepth 1 -maxdepth 1 -type d 2> /dev/null | LC_ALL=C sort)

    # Process each subdirectory
    has_git_repos=0
    for subdir in "${subdirs[@]}"; do
      local full_path
      full_path="$d/${subdir#./}"
      git_dirty "$full_path" $((depth + 1))

      # Check if this subdirectory contains git repos
      if [[ -n ${DIR_HAS_REPOS["$full_path"]} ]]; then
        has_git_repos=1
      fi
    done

    # After processing subdirectories, check if this directory contains repos
    if [[ $has_git_repos -eq 1 || -n ${DIR_HAS_REPOS["$d"]} ]]; then
      # This is a non-git directory but contains git repos - print it
      print_result "$depth" "$d" "$NOT_GIT" "" ""
    fi
  fi

  # Print final count when done with root scan
  if [[ $depth -eq 0 ]]; then
    echo -ne "                                                         \r" # Clear the progress line

    # Display sorted results
    display_sorted_results

    local end_time elapsed_time formatted_time

    # Calculate and display elapsed time
    end_time=$(date +%s)
    elapsed_time=$((end_time - START_TIME))
    formatted_time=$(format_duration $elapsed_time)

    # Show summary
    echo -e "\nFound $GIT_REPO_COUNT git repositories in $formatted_time"
  fi

  cd "$orig_dir" || {
    log_error "Failed to return to original directory"
    exit 1
  }
}

# Function to process directories in parallel
process_in_parallel()
{
  local base_dir search_start_time
  base_dir="$1"
  search_start_time=$(date +%s)

  echo "Finding git repositories..."

  # Find all git repositories efficiently
  local repos=()
  declare -A repo_parents=()

  while IFS= read -r repo; do
    if [[ -d $repo ]]; then
      repos+=("$repo")

      # Track the parent directories for each repo
      local parent
      parent=$(dirname "$repo")
      while [[ $parent != "$base_dir" && $parent != "/" ]]; do
        repo_parents["$parent"]=1
        parent=$(dirname "$parent")
      done
    fi
  done < <(find "$base_dir" -type d -name ".git" -prune 2> /dev/null | sed 's/\/\.git$//')

  local total search_time
  total=${#repos[@]}
  search_time=$(($(date +%s) - search_start_time))
  echo "Found $total git repositories in $(format_duration $search_time)"

  if [[ $total -eq 0 ]]; then
    echo "No git repositories found in $base_dir"
    return
  fi

  # Check if we have GNU parallel specifically
  local have_gnu_parallel
  have_gnu_parallel=0
  if command -v parallel &> /dev/null; then
    if parallel --version 2> /dev/null | grep -q "GNU parallel"; then
      have_gnu_parallel=1
      echo "Processing $total repositories using GNU parallel..."
    else
      echo "Non-GNU parallel detected (moreutils). Using manual parallelization..."
    fi
  else
    echo "Parallel not found. Using manual parallelization..."
  fi

  local cores max_jobs processing_start_time

  # Use max CPU cores but not more than 8
  cores=$(sysctl -n hw.ncpu 2> /dev/null || nproc 2> /dev/null || echo 4)
  max_jobs=$((cores > 8 ? 8 : cores))

  processing_start_time=$(date +%s)

  if [[ $have_gnu_parallel -eq 1 ]]; then
    # GNU parallel approach
    export -f check_git_status truncate_path print_color \
      get_branch_name format_path_for_sorting get_basename is_main_branch
    export GIT_DIRTY_TRUNCATE BASE_DIR GIT_CLEAN GIT_DIRTY \
      GIT_DIRTY_CHECK_STASH GIT_DIRTY_CHECK_UNTRACKED \
      GIT_DIRTY_SHOW_BRANCH GIT_DIRTY_MAIN_BRANCHES BRANCH_COLOR STATUS_COLOR

    # Create a temporary file for results
    local parallel_results
    parallel_results=$(mktemp)

    # Create a shell function for parallel to use
    process_repo()
    {
      local repo
      repo=$1
      cd "$repo" 2> /dev/null || return

      local status_text display_path basename_path branch rel_path depth indent
      status_text=$(check_git_status "$repo")
      display_path=$(truncate_path "$repo")
      basename_path=$(get_basename "$display_path")
      branch=$(get_branch_name)

      # Calculate depth for proper indentation
      rel_path="${repo#"$BASE_DIR"/}"
      depth=$(echo "$rel_path" | tr -cd '/' | wc -c)
      indent=$(printf "%*s" $((depth * 2)) "")

      # Determine icon based on status
      local icon
      if [[ -z $status_text ]]; then
        icon="$GIT_CLEAN"
      else
        icon="$GIT_DIRTY"
      fi

      # Format output with status and branch name if needed
      local output
      if [[ -z $status_text ]]; then
        # Clean git repo
        if [[ $GIT_DIRTY_SHOW_BRANCH -eq 1 && -n $branch ]]; then
          # Only show branch if it's not a main branch
          if is_main_branch "$branch"; then
            output="$indent $icon $basename_path"
          else
            output="$indent $icon $basename_path $(print_color "$BRANCH_COLOR" "($branch)")"
          fi
        else
          output="$indent $icon $basename_path"
        fi
      else
        # Dirty git repo
        local status_display
        status_display="$(print_color "$STATUS_COLOR" "[$status_text]")"
        if [[ $GIT_DIRTY_SHOW_BRANCH -eq 1 && -n $branch ]]; then
          # Only show branch if it's not a main branch
          if is_main_branch "$branch"; then
            output="$indent $icon $basename_path $status_display"
          else
            output="$indent $icon $basename_path $status_display $(print_color "$BRANCH_COLOR" "($branch)")"
          fi
        else
          output="$indent $icon $basename_path $status_display"
        fi
      fi

      # Path-based sorting key for proper tree structure - append '/1' to make git repos come after directories
      echo "$rel_path/1:$output"
    }
    export -f process_repo

    parallel --bar -j "$max_jobs" process_repo ::: "${repos[@]}" > "$parallel_results"

    # Process parent directories to ensure tree structure
    for parent in "${!repo_parents[@]}"; do
      local rel_path depth basename indent
      rel_path="${parent#"$BASE_DIR"/}"
      depth=$(echo "$rel_path" | tr -cd '/' | wc -c)
      basename=$(get_basename "$parent")
      indent=$(printf "%*s" $((depth * 2)) "")
      # Append '/0' to make directories come before their contents
      echo "$rel_path/0:$indent $NOT_GIT $basename" >> "$parallel_results"
    done

    # Also add the root directory (with empty sort key to put it first)
    echo "/0:$NOT_GIT $BASE_DIR" >> "$parallel_results"

    # Sort and display results - path-based sort for proper tree structure
    LC_ALL=C sort "$parallel_results" | cut -d: -f2- | sed 's/^://'
    rm -f "$parallel_results"
  else
    local temp_output progress_file running_file temp_files job_count

    # Manual parallelization with progress bar
    echo "Using $max_jobs parallel jobs..."

    # Use temp files to collect results and track progress
    temp_output=$(mktemp)
    progress_file=$(mktemp)
    echo "0" > "$progress_file" # Initialize progress counter

    # To track if we're running or should stop
    running_file=$(mktemp)
    echo "1" > "$running_file" # 1 means running, 0 means stop

    # Store temp files for cleanup in trap
    temp_files=("$temp_output" "$progress_file" "$running_file")

    # Cleanup function
    cleanup()
    {
      echo -e "\nCleaning up..."
      echo "0" > "$running_file" # Signal progress bar to stop

      # Kill all background processes in our process group
      # We use SIGTERM to allow processes to clean up
      pkill -P $$ TERM 2> /dev/null || true

      # Clean up temp files
      for file in "${temp_files[@]}"; do
        [[ -f $file ]] && rm -f "$file"
      done

      # Clear the line and restore cursor
      echo -ne "\r\033[K"

      exit 1
    }

    # Set up trap to catch CTRL+C and other termination signals
    trap cleanup INT TERM HUP

    # Progress bar function
    update_progress()
    {
      local current total width percent completed elapsed rate eta

      current=$1
      total=$2
      width=50 # width of the progress bar
      percent=$((current * 100 / total))
      completed=$((width * current / total))
      elapsed=$(($(date +%s) - processing_start_time))
      rate=$((current > 0 && elapsed > 0 ? current / elapsed : 0))
      eta=$((rate > 0 ? (total - current) / rate : 0))

      # Create the progress bar
      local bar="["
      for ((i = 0; i < completed; i++)); do bar+="="; done
      if [[ $completed -lt $width ]]; then bar+=">" && ((completed++)); fi
      for ((i = completed; i < width; i++)); do bar+=" "; done
      bar+="]"

      # Print the progress bar with ETA
      printf "\r%s %d/%d (%d%%) %d repos/s ETA: %s" \
        "$bar" "$current" "$total" "$percent" "$rate" "$(format_duration $eta)"
    }

    # Process repositories with progress tracking
    job_count=0

    # Start a background process to update the progress bar
    (
      while true; do
        sleep 0.2 # Update every 0.2 seconds
        # Check if we should continue running
        if [[ -f $running_file && "$(cat "$running_file")" == "1" ]]; then
          if [[ -f $progress_file ]]; then
            local progress
            progress=$(cat "$progress_file")
            update_progress "$progress" "$total"

            # Exit when we reach the total
            if [[ $progress -ge $total ]]; then
              break
            fi
          fi
        else
          # Stop if signaled to do so
          break
        fi
      done

      # Clear the line when done
      echo -ne "\r\033[K"

      # Show completion message only if we finished normally
      if [[ -f $progress_file ]]; then
        local final_progress
        final_progress=$(cat "$progress_file")
        if [[ $final_progress -ge $total ]]; then
          echo "Processing complete!"
        fi
      fi
    ) &
    local progress_pid
    progress_pid=$!

    # Process repositories
    for repo in "${repos[@]}"; do
      # Check if we should continue
      if [[ "$(cat "$running_file")" != "1" ]]; then
        break
      fi

      (
        cd "$repo" 2> /dev/null || exit

        local status_text branch icon display_path depth indent basename_path

        # Get status and branch information
        status_text=""

        # Check for modified files
        git diff --quiet 2> /dev/null || status_text+="M"

        # Check for staged changes
        git diff --cached --quiet 2> /dev/null || status_text+="S"

        # Check for untracked files
        if [[ $GIT_DIRTY_CHECK_UNTRACKED -eq 1 ]]; then
          [[ -n $(git ls-files --others --exclude-standard 2> /dev/null | head -1) ]] && status_text+="?"
        fi

        # Check for stashed changes
        if [[ $GIT_DIRTY_CHECK_STASH -eq 1 ]]; then
          [[ -f "$(git rev-parse --git-dir 2> /dev/null)/refs/stash" ]] && status_text+="$"
        fi

        # Check for unpushed commits
        eval "git rev-list @{u}.. --count" 2> /dev/null \
          | grep -q '^0$' 2> /dev/null || status_text+="↑"

        # Get branch name
        branch=$(get_branch_name)

        # Determine icon based on status
        if [[ -z $status_text ]]; then
          icon="$GIT_CLEAN"
        else
          icon="$GIT_DIRTY"
        fi

        # Get path for display
        if [[ $GIT_DIRTY_TRUNCATE -eq 1 ]]; then
          display_path="${repo#"$BASE_DIR"/}"
        else
          display_path="$repo"
        fi

        # Calculate depth for indentation
        depth=$(echo "$display_path" | tr -cd '/' | wc -c)
        indent=$(printf "%*s" $((depth * 2)) "")

        # Get basename for display
        basename_path=$(get_basename "$display_path")

        # Generate output with branch info if enabled
        if [[ -z $status_text ]]; then
          # Clean repo
          if [[ $GIT_DIRTY_SHOW_BRANCH -eq 1 && -n $branch ]]; then
            # Only show branch if it's not a main branch
            if is_main_branch "$branch"; then
              echo "$display_path/1:$indent $icon $basename_path" >> "$temp_output"
            else
              echo "$display_path/1:$indent $icon $basename_path $(print_color "$BRANCH_COLOR" "($branch)")" >> "$temp_output"
            fi
          else
            echo "$display_path/1:$indent $icon $basename_path" >> "$temp_output"
          fi
        else
          # Dirty repo with status
          local status_display
          status_display="$(print_color "$STATUS_COLOR" "[$status_text]")"
          if [[ $GIT_DIRTY_SHOW_BRANCH -eq 1 && -n $branch ]]; then
            # Only show branch if it's not a main branch
            if is_main_branch "$branch"; then
              echo "$display_path/1:$indent $icon $basename_path $status_display" \
                >> "$temp_output"
            else
              local b
              b=$(print_color "$BRANCH_COLOR" "($branch)")
              echo "$display_path/1:$indent $icon $basename_path $status_display $b" \
                >> "$temp_output"
            fi
          else
            echo "$display_path/1:$indent $icon $basename_path $status_display" \
              >> "$temp_output"
          fi
        fi

        # Update progress atomically
        local current_progress
        current_progress=$(cat "$progress_file")
        echo $((current_progress + 1)) > "$progress_file"
      ) &

      # Control the number of parallel jobs
      ((job_count++))
      if ((job_count >= max_jobs)); then
        wait -n
        ((job_count--))
      fi
    done

    # Wait for all remaining jobs to finish
    wait

    # Signal progress bar to stop and wait for it to finish
    echo "0" > "$running_file"
    wait $progress_pid 2> /dev/null

    # Add parent directories to the output for tree structure
    for parent in "${!repo_parents[@]}"; do
      local rel_path depth indent basename_path

      rel_path="${parent#"$BASE_DIR"/}"
      depth=$(echo "$rel_path" | tr -cd '/' | wc -c)
      indent=$(printf "%*s" $((depth * 2)) "")
      basename_path=$(get_basename "$rel_path")
      echo "$rel_path/0:$indent $NOT_GIT $basename_path" >> "$temp_output"
    done

    # Add root directory (with special path to ensure it comes first)
    echo "/0:$NOT_GIT $BASE_DIR" >> "$temp_output"

    # Display the results in sorted order
    if [[ -f $temp_output ]]; then
      LC_ALL=C sort "$temp_output" | cut -d: -f2-
    fi

    # Clean up temp files
    for file in "${temp_files[@]}"; do
      [[ -f $file ]] && rm -f "$file"
    done

    # Remove the trap since we're done
    trap - INT TERM HUP
  fi

  # Calculate and show total processing time
  local total_time processing_time dur runtime

  total_time=$(($(date +%s) - START_TIME))
  processing_time=$(($(date +%s) - processing_start_time))
  dur=$(format_duration $processing_time)
  runtime=$(format_duration $total_time)

  echo -e "\nProcessed $total repositories in $dur (Total runtime: $runtime)"
}

check_directory_with_progress()
{
  local dir
  dir="$1"
  # Simple forward to git_dirty which will count as it goes
  git_dirty "$dir" 0
}

# Cleanup function to remove temporary files
cleanup_temp_files()
{
  if [[ -n $RESULTS_FILE && -f $RESULTS_FILE ]]; then
    rm -f "$RESULTS_FILE"
  fi
}

# Show help
show_help()
{
  local bin
  bin=$(basename "$0")
  cat << EOF
Usage: $bin [OPTIONS] [DIRECTORY]

Recursively check git repository status

Options:
  -h           Show this help message and exit
  -d NUM       Set maximum depth for showing non-git directories (default: $GIT_DIRTY_DEPTH)
  -p           Process directories in parallel (requires 'parallel' command)
  -v           Enable verbose output
  -a           Show all status details (stash, untracked, etc.)
  -e PATTERNS  Additional patterns to exclude (comma separated)
  -m NUM       Set maximum recursion depth (default: $GIT_DIRTY_MAXDEPTH)
  -c           Toggle colorized output (currently: $(use_color && echo "on" || echo "off"))
  -t           Toggle path truncation (currently: $([[ $GIT_DIRTY_TRUNCATE -eq 1 ]] && echo "on" || echo "off"))
  -b           Toggle branch name display (currently: $([[ $GIT_DIRTY_SHOW_BRANCH -eq 1 ]] && echo "on" || echo "off"))

Status indicators:
  M = Modified files
  S = Staged changes
  ? = Untracked files (with -a)
  $ = Stashed changes (with -a)
  ↑ = Unpushed commits

Example:
  $bin ~/Projects
  $bin -d 3 -e 'build,dist' ~/Code
EOF
}

# Main function
main()
{
  # Set up trap for error handling
  trap 'handle_error $? $LINENO' ERR
  trap 'cleanup_temp_files' EXIT

  # Parse command line options - cross-platform way
  while getopts "hvpacbd:e:m:t" opt; do
    case $opt in
      h)
        show_help
        exit 0
        ;;
      v)
        VERBOSE=1
        ;;
      p)
        PARALLEL=1
        ;;
      a)
        GIT_DIRTY_CHECK_STASH=1
        GIT_DIRTY_CHECK_UNTRACKED=1
        ;;
      c)
        # Toggle color
        [[ $GIT_DIRTY_COLOR -eq 1 ]] && GIT_DIRTY_COLOR=0 || GIT_DIRTY_COLOR=1
        ;;
      b)
        # Toggle branch display
        [[ $GIT_DIRTY_SHOW_BRANCH -eq 1 ]] && GIT_DIRTY_SHOW_BRANCH=0 || GIT_DIRTY_SHOW_BRANCH=1
        ;;
      t)
        # Toggle truncation
        [[ $GIT_DIRTY_TRUNCATE -eq 1 ]] && GIT_DIRTY_TRUNCATE=0 || GIT_DIRTY_TRUNCATE=1
        ;;
      d)
        GIT_DIRTY_DEPTH="$OPTARG"
        ;;
      m)
        GIT_DIRTY_MAXDEPTH="$OPTARG"
        ;;
      e)
        IFS=',' read -ra EXCLUDE_ARRAY <<< "$OPTARG"
        for item in "${EXCLUDE_ARRAY[@]}"; do
          GIT_DIRTY_EXCLUDE="$GIT_DIRTY_EXCLUDE $item"
        done
        ;;
      \?)
        log_error "Invalid option: -$OPTARG"
        show_help
        exit 1
        ;;
      :)
        log_error "Option -$OPTARG requires an argument."
        show_help
        exit 1
        ;;
    esac
  done

  # Remove processed options
  shift $((OPTIND - 1))

  # If user has provided folder as a first argument, use it
  if [[ -n $1 ]]; then
    GIT_DIRTY_DIR="$1"
  fi

  if [[ $GIT_DIRTY_DIR == "." ]]; then
    GIT_DIRTY_DIR="$(pwd)"
  fi

  # Expand path if it contains tilde
  GIT_DIRTY_DIR="${GIT_DIRTY_DIR/#\~/$HOME}"

  # Set base directory for path truncation
  BASE_DIR="$(cd "$GIT_DIRTY_DIR" && pwd)"

  # Verify the directory exists
  if [[ ! -d $GIT_DIRTY_DIR ]]; then
    log_error "Directory does not exist: $GIT_DIRTY_DIR"
    exit 1
  fi

  # Update color variables after possible toggle
  if use_color; then
    GIT_CLEAN=$(print_color "32" "✅")
    GIT_DIRTY=$(print_color "31" "❌")
    NOT_GIT=$(print_color "33" "⚠️")
    BRANCH_COLOR="35"
    STATUS_COLOR="33"
  else
    GIT_CLEAN="✅"
    GIT_DIRTY="❌"
    NOT_GIT="⚠️"
    BRANCH_COLOR=""
    STATUS_COLOR=""
  fi

  echo "Checking repositories in: $GIT_DIRTY_DIR"
  echo "Legend: $GIT_CLEAN Clean repo | $GIT_DIRTY Dirty repo"
  if [[ $GIT_DIRTY_SHOW_BRANCH -eq 1 ]]; then
    echo "Showing non-standard branches (hiding ${GIT_DIRTY_MAIN_BRANCHES})"
  fi
  echo "---------------------------------------------------------"

  # Use parallel processing if enabled and available
  if [[ $PARALLEL -eq 1 ]]; then
    process_in_parallel "$GIT_DIRTY_DIR"
  else
    # Use progress indicator for large directories
    check_directory_with_progress "$GIT_DIRTY_DIR"
  fi
}

# Run the main function
main "$@"
