#!/opt/local/bin/bash


# usage info
usage() {
  cat <<EOF
  Usage: git recall [options]
  Options:
    -d, --date              Show logs for last n days.
    -a, --author            Author name.
    -b, --branch            Specify branch to display commits from.
    -p  --path              Specify path or filename to display commits from.
    -f, --fetch             fetch commits.
    -h, --help              This message.
    -v, --version           Show version.
    --                      End of options.
EOF
}

# Global variables
orig_stty=$(stty -g)   # Store original terminal settings
AUTHOR=""
FETCH=false
GIT_FORMAT=""
GIT_LOG=""
COMMITS=""
SINCE="1 days ago"     # show logs for last day by default
COMMITS_UNCOL=()       # commits without colors
SED_CMD=""             # Sed command to use according OS.
LESSKEY=false
VERSION="1.2.4"
BRANCH=""
SEARCH_PATH=""

# Set key bindings.
au="`echo -e '\e[A'`" # arrow up
ad="`echo -e '\e[B'`" # arrow down
ec="`echo -e '\e'`"   # escape
nl="`echo -e '\n'`"   # newline
au_1="k"              # arrow up
ad_1="j"              # arrow down
nl_1="e"              # expand

# Colored output.
function colored() {
    GREEN=$(tput setaf 4)
    YELLOW=$(tput setaf 3)
    NORMAL=$(tput sgr0)
    REVERSE=$(tput rev)
} 

# Uncolored output.
function uncolored() {
    GREEN=""
    YELLOW=""
    NORMAL=""
    REVERSE=""
}

# Clear screen from cursor to end of screen
function clear_screen() {
    echo -e "\E[J"
    tput cuu1
}

# Are we in a git repo?
if [[ ! -d ".git" ]] && ! git rev-parse --git-dir &>/dev/null; then
    echo "abort: not a git repository." 1>&2
    exit 1
fi

# Parse options
while [[ "$1" =~ ^- && ! "$1" == "--" ]]; do
    case $1 in
	-v | --version )
	    echo "$VERSION"
	    exit
	    ;;
	-d | --date )
	    SINCE="$2 days ago"
	    shift;
	    ;;
	-a | --author )
	    AUTHOR="$2"
	    shift
	    ;;
	-f | --fetch )
	    FETCH=true
	    ;;
	-h | --help )
	    usage
	    exit
	    ;;
	-b | --branch )
            BRANCH="$2"
            shift
            ;;
        -p | --path )
            SEARCH_PATH="$2"
            shift
            ;;
	* )
	    echo "abort: unknown argument" 1>&2
	    exit 1
    esac
    shift
done
if [[ "$1" == "--" ]]; then shift; fi

# Enable colors if supported by terminal.
if [[ -t 1 ]] && [[ -n "$TERM" ]] && which tput &>/dev/null && tput colors &>/dev/null; then
    ncolors=$(tput colors)
    if [[ -n "$ncolors" ]] && [[ "$ncolors" -ge 8 ]] ; then
	colored
    else
	uncolored
    fi
else
    uncolored
fi

# Set AUTHOR to current user if no param passed or display for all users if param equal to "all". 
if [[ ! -n $AUTHOR ]]; then
    AUTHOR=$(git config user.name 2>/dev/null)
elif [[ $AUTHOR = "all" ]]; then
    AUTHOR=".*"
fi

# Fetch changes before.
if [[ $FETCH == true ]]; then
    echo "${GREEN}Fetching changes...${NORMAL}"
    git fetch --all &> /dev/null
    tput cuu1
    clear_screen
fi

# Log template.
GIT_FORMAT="%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset"

# Log command.
GIT_LOG="git log --pretty=format:'${GIT_FORMAT}'  
           --author \"$AUTHOR\"
           --since \"$SINCE\" --abbrev-commit --no-merges $BRANCH $SEARCH_PATH"

# Change temporary the IFS to store GIT_LOG's output into an array.
IFS=$'\n'
COMMITS=($(eval ${GIT_LOG} 2>/dev/null))
unset IFS

NI=${#COMMITS[@]}                             # Total number of items.
SN=$(( `tput lines` - 1 ))                    # Screen's number of lines.
CN=$(tput cols)                               # Screen's number of columns.
TN=$(( $NI < $((SN -1)) ? $NI : $((SN -1))))  # Number of lines that we will display.
OFFSET=0                                      # Incremented by one each time a commit's length is higher than teminal width.

# If there is no items, exit.
if [[ $NI = 0 ]]; then
    if [[ $AUTHOR = ".*" ]]; then
   	echo "${YELLOW}All contributors did nothing during this period.${NORMAL}" && exit 0
    else
   	echo "${YELLOW}The contributor \"${AUTHOR}\" did nothing during this period.${NORMAL}" && exit 0
    fi
fi

# Check if lesskey is installed.
if command -v lesskey &> /dev/null; then
    LESSKEY=true
fi

# Set correct sed command according OS's type
case "$OSTYPE" in
    darwin*) SED_CMD="sed -E s/"$'\E'"\[([0-9]{1,2}(;[0-9]{1,2})*)?m//g" ;;
    *)       SED_CMD="sed -r s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g" ;;
esac

# Create array with uncolred commits (removing escape sequences using sed)
for elt in "${COMMITS[@]}"
do
    ELT="$(echo "$elt" | $SED_CMD)" # remove colors escape codes
    COMMITS_UNCOL+=("$ELT")
done

# Add +1 to OFFSET if a commit's length is bigger than the current terminal session's width. (This is to fix a redraw issue)
for C in "${COMMITS_UNCOL[@]}"
do
    if [[ ${#C} -gt $CN ]]; then
	OFFSET=$(( OFFSET + 1 ))
    fi
done

# Create a temporary lesskey file to change the keybindings so the user can use the TAB key to quit less. (more convenient)  
if [[ $LESSKEY = true ]]; then
    echo "\t quit" | lesskey -o /tmp/lsh_less_keys_tmp -- - &> /dev/null
fi

# Get commit's diff.
function get_diff() {
    ELT="$(echo "${COMMITS_UNCOL[$CP-1]}")"
    DIFF_TIP=${ELT:0:7}
    DIFF_CMD="git show $DIFF_TIP --color=always $SEARCH_PATH"
    DIFF=$(eval ${DIFF_CMD} 2>/dev/null)
    tmp_diff="$(echo "$DIFF" | $SED_CMD)"
    off=$(echo "$tmp_diff" | grep -c ".\{$CN\}") # Number of lines in the diff that are longer than terminal width.
    DIFF_LINES_NUMBER="$(echo "$DIFF" | wc -l)"
    DIFF_LINES_NUMBER=$(( DIFF_LINES_NUMBER + off ))
}

# This function will print the diff according the commit's tip. If the diff is too long, the result will be displayed using 'less'. 
function print_diff() {
    get_diff
    if [[ $(( TN + DIFF_LINES_NUMBER + OFFSET )) -ge $(( `tput lines` - 1 )) ]]; then
	trap - INT
	if [[ $LESSKEY = true ]]; then
	    echo "$DIFF" | less -r -k /tmp/lsh_less_keys_tmp
	else 
	    echo "$DIFF" | less -r
	fi
	trap cleanup_and_exit INT
	clear_screen
    else 
	stop=false
	clear_screen
	for i in `seq 1 $TN`
	do
	    echo -n "$NORMAL"
	    [[ $CP == "$i" ]] && echo -n "$REVERSE"
	    echo "${COMMITS[$i - 1]}"
	    [[ $CP == "$i" ]] && echo "$DIFF"
	done
	# Wait for user action.
	while ! $stop
	do
	    read -sn 1 key
	    case "$key" in
		"$nl" | "$nl_1")
		    stop=true
		    ;;
		"q")
		    stop=true
		    END=true
		    ;;
	    esac
	done
	[[ $END = false ]] && tput cuu $(( TN + DIFF_LINES_NUMBER + OFFSET )) && clear_screen
    fi
}

# Calculate OFFSET to avoid bad redraw.
function calculate_offset {
    tmp=1
    index=$(( SI -1 ))
    while [[ $tmp -lt $SN ]]
    do
    	el=${COMMITS_UNCOL[$index]}
    	if [[ ${#el} -gt $CN ]] && [[ $CP -lt $((SN -1)) ]]; then
    	    OFFSET_2=$(( OFFSET_2 + 1 ))
    	    tmp=$(( tmp + 1 ))
    	fi
    	tmp=$(( tmp + 1 ))
    	index=$(( index + 1 ))
    done
}

# Reset changes made by this script before exiting
function cleanup_and_exit() {
    # remove temporary less keybindings
    [[ $LESSKEY = true ]] && rm /tmp/lsh_less_keys_tmp

    tput cuu 1      # move cursor up one line. (remove extra line)
    tput cnorm      # unhide cursor
    echo "$NORMAL"  # normal colors
    stty "$orig_stty" > /dev/null 2>&1
    exit
}

# Catch a control-C interrupt and perform cleanup operations before exiting
trap cleanup_and_exit INT

{ # capture stdout to stderr

tput civis   # hide cursor.
CP=1         # current position
SI=1         # index
END=false    # end while loop
EXT=0        # Used to extend the number of lines to display.

while ! $END
do
    # When the number of item is higher than screen's number of lines, OFFSET_2 is recalculated each time we select a new item.
    # Set last index to print. (based on OFFSET)
    END_INDEX=0 # Index for the last item to display
    if [[  $TN == $NI ]]; then 
	END_INDEX=$TN
	OFFSET_2=$OFFSET
    elif [[  $TN == $(( SN - 1 )) ]]; then
	# Calculate new OFFSET.
	if [[ $OFFSET != 0 ]]; then 
   	    [[ $CP -lt $((SN -1)) ]] && OFFSET_2=0
   	    EXT=1
   	    calculate_offset
	fi
	END_INDEX=$(( TN + SI -1 + EXT - OFFSET_2 ))
    fi

    # Loop and echo commits
    for i in `seq $SI $END_INDEX`
    do
	echo -n "$NORMAL"
	[[ $CP == $i ]] && echo -n "$REVERSE"
	echo "${COMMITS[$i - 1]}"
    done

    read -sn 1 key
    [[ "$key" == "$ec" ]] &&
    {
	read -sn 2 k2
	key="$key$k2"
    }

    case "$key" in
	"$au" | "$au_1")
            CP=$(( CP - 1 ))
            [[ $CP == 0 ]] && [[ $SI == 1 ]] && [[ $TN == $(( SN - 1 )) ]] && CP=$NI && SI=$(( NI - SN + 2 + OFFSET_2 ))
            [[ $CP == 0 ]] && [[ $SI == 1 ]] && [[ $TN == $NI ]] && CP=$TN
            [[ $CP == $(( SI - 1 )) ]] && [[ $SI != 1 ]] && SI=$(( SI - 1 ))

   	    [[ $TN != $(( SN - 1 )) ]] && tput cuu $(( TN + OFFSET_2 ))
   	    [[ $TN == $(( SN - 1 )) ]] && tput cuu $(( TN + EXT ))
            [[ $SI != 1 ]] && clear_screen
            ;;
	"$ad" | "$ad_1")
   	    CP=$(( CP + 1 ))
            [[ $CP == $(( NI + 1 )) ]] && CP=1 && SI=1 
            [[ $CP == $(( SN + SI - 1 + EXT - OFFSET_2 )) ]] && [[ $TN == $(( SN - 1 )) ]] && SI=$(( SI + 1 ))

   	    [[ $TN != $(( SN - 1 )) ]] && tput cuu $(( TN + OFFSET_2 ))
   	    [[ $TN == $(( SN - 1 )) ]] && tput cuu $(( TN + EXT ))
            [[ $SI != 1 ]] && clear_screen
            [[ $SI = 1 ]] && [[ $CP = 1 ]] && clear_screen
            ;;
	"$nl" | "$nl_1")
   	    [[  $TN == $NI ]] && tput cuu $(( TN + OFFSET_2 ))
   	    [[  $TN != $NI ]] && tput cuu $(( TN + EXT ))
            print_diff 
            ;;
	"q")
            si=false
            END=true
            ;;
	* )
   	    tput cuu $(( TN + OFFSET_2 ))
    esac
done

cleanup_and_exit

} >&2 # END capture

