#!/usr/bin/bash
#
# augmented by barbuk: https://github.com/BarbUk/snippy
#  . restore current clipboard
#  . {clipboard} placeholder to use current clipboard in snippet
#  . {cursor} placeholder to place the cursor
#    . go left to the correct position for cli and gui paste
#    . go up for block snippet for gui paste
#  . ##noparse header in snippet to not parse
#  . ##tmpfile header in snippet to replace $tmpfile by the temp filename used in the script
#  . ##richsnippet header in snippet to use paste buffer in rich mode (To copy html content in gui)
#  . execute command begining by $
#  . execute bash script in $snippets_directory/scripts
#  . copy script content when selection is selected by CTRL+Return
#  . icons ! Icon name is set from first dir name. If you have the following snippets, the terminal icon will be displayed in rofi
#    terminal/
#    ├── other
#    │   └── date
#    └── script
#        └── test
#
# augmented by "opennomad": https://gist.github.com/opennomad/15c4d624dce99066a82d
# originally written by "mhwombat": https://bbs.archlinux.org/viewtopic.php?id=71938&p=2
# Based on "snippy" by "sessy"
# (https://bbs.archlinux.org/viewtopic.php?id=71938)
#
# You will also need "rofi", "xsel", "xclip" and "xdotool". Get them from your linux
# distro in the usual way.
#
# To use:
# 1. Create the directory ~/.config/snippy
#
# 2. Create a file in that directory for each snippet that you want.
#    The filename will be used as a menu item, so you might want to
#    omit the file extension when you name the file.
#
#    TIP: If you have a lot of snippets, you can organise them into
#    subdirectories under ~/.config/snippy.
#
#    TIP: The name of the subdirectory will be passed to rofi as an icon name
#
#    TIP: The contents of the file will be pasted asis, so if you
#    don't want a newline at the end when the text is pasted, don't
#    put one in the file.
#
# 3. Bind a convenient key combination to this script.
#
#    TIP: If you're using XMonad, add something like this to xmonad.hs
#      ((mod4Mask, xK_s), spawn "/path/to/snippy")
#
set -o errexit -o pipefail -o nounset

readonly snippets_directory=${XDG_CONFIG_HOME:-$HOME/.config}/snippy
readonly rofi_args=(-no-lazy-grab -dmenu -i -sort -sorting-method fzf -async-pre-read 20 -theme-str 'element-icon { size: 2.35ch;}' -kb-accept-custom "" -kb-custom-1 "Ctrl+Return")
readonly fzf_args=(--select-1 --reverse --inline-info --multi --preview '( bat --style auto --color always --language bash {} 2> /dev/null || highlight --force -O ansi -l {} 2> /dev/null ) | head -200' -1)
readonly focus_wait=0.15

# Placeholders
readonly placeholder_cursor="{cursor}"
readonly placeholder_clipboard="{clipboard}"
readonly placeholder_clipboard_urlencode="{clipboard_urlencode}"

tmpfile=$(mktemp)
readonly tmpfile
trap 'rm -f $tmpfile' EXIT HUP INT TRAP TERM

# colors
readonly normal="\e[0m"
readonly bold="\e[1m"
readonly underline="\e[4m"

script_content=false
action=gui
snippet=

# smarty like template engine which executes inline bash in (bashdown) strings (replaces variables with values e.g.)
# @link http://github.com/coderofsalvation/bashdown
# @dependancies: sed cat
# @example: echo 'hi $NAME it is $(date)' | bashdown
# fetches a document and interprets bashsyntax in string (bashdown) templates
# @param string - string with bash(down) syntax (usually surrounded by ' quotes instead of ")
bashdown() {
    while IFS= read -r line; do
        line="$(eval "printf -- \"$( printf "%s" "$line" | sed 's/"/\\"/g' )\"")";
        echo -e "$line"
    done
}

# Simplified version of bashdown, use echo and bash var search and replace
# Better handling of symbol char
bashdown_simple() {
  while IFS= read -r line || [[ -n "$line" ]]; do
    line="$(eval "echo \"${line//\"/\\\"}\"")"
    echo "$line"
  done
}

# Detect if focused app is a terminal or a gui
is_gui() {
    class="$(xprop -id "$(xdotool getwindowfocus)" WM_CLASS | cut -d'"' -f2 | tr '[:upper:]' '[:lower:]')"
    # Return false if the class if a term
    if [[ "$class" =~ term|tilda|kitty|alacritty ]]; then
        return 1
    fi
    return 0
}

# Detect vim
is_vim() {
    name="$(xprop -id "$(xdotool getwindowfocus)" WM_NAME | cut -d'"' -f2)"
    # vim with `set title` set the term title with:
    # document - VIM
    if [[ "${name:(-3)}" == VIM ]]; then
        return 0
    fi
    return 1
}

# Find the index of a string in a string
strindex() {
    x="${1%%$2*}"
    [[ "$x" = "$1" ]] && echo -1 || echo "${#x}"
}

is_rich_snippet() {
    file="$1"
    grep -q "##richsnippet" "$file"
}

# Move the cursor up or left
move_cursor() {
    local key=$1
    local count=$2
    local keys="End "
    if [[ $count -gt 0 ]]; then
        until [  "$count" -eq 0 ]; do
            keys+="${key} "
            ((count-=1)) || true
        done
        # shellcheck disable=SC2086
        xdotool key --delay 0.1 $keys
    fi
}

init() {
    # Check basic dependency
    local all_needed_programs_installed=true
    local needed_programs=( rofi fzf xsel xclip jq xdotool )
    for program in "${needed_programs[@]}"; do
        if ! command -v "$program" >/dev/null 2>&1; then
            all_needed_programs_installed=false
            echo -e "${bold}$program${normal} missing"
        fi
    done

    if [ "$all_needed_programs_installed" = false ] ; then
        echo -e "\nPlease install the previous dependancies"
        exit 1
    fi

    # Check snippet directory
    if [[ ! -d "$snippets_directory" ]]; then
        mkdir -p "$snippets_directory"
        echo "$snippets_directory created"
        echo "hi it is \$(date)" > "$snippets_directory/test"
    fi
}

usage() {
    echo "Usage:"
    echo -e "\t${bold}$0${normal} [OPTION] ACTION"
    echo -e "\tSnippy snippets manager"

    echo "Options"
    echo -e "\t-h, --help Show help"

    echo "Actions"
    echo -ne "\t${bold}gui${normal}"
    echo -e "\t Browse snippet and paste it in the focused window ${underline}(default)${normal}"

    echo -ne "\t${bold}cli${normal}"
    echo -e "\t list snippet in cli mode, only copy snippet in the paste buffer"

    echo -ne "\t${bold}edit${normal}"
    echo -e "\t Browse snippet and edit it"

    echo -ne "\t${bold}add${normal}"
    echo -e "\t Add a new snippet"

    echo -ne "\t${bold}list${normal}"
    echo -e "\t list snippet"

    echo -ne "\t${bold}cat${normal}"
    echo -e "\t list category"

    echo -ne "\t${bold}completion${normal}"
    echo -e "\t bash completion"

    exit
}

parse_options() {

  while (( "$#" )); do
    case "$1" in
      -h|--help)
        usage
        exit
        ;;
      --) # end argument parsing
        shift
        break
        ;;
      *)
        action="$1"
        shift
        return
        ;;
    esac
  done

}

error() {
  local message="$1"
  echo "$message" >&2
}

cli() {
    snippet=$( list | fzf "${fzf_args[@]}")
}

list() {
    local type="${1:-f}"

    find -L . -type "$type" \
        | grep -vE '^\.$|\.git|\.gitconfig|\.gitkeep|\.gitignore' \
        | sed -e 's!\.\/!!' \
        | sort
}

add() {
    local snippet="$*"
    if [ -e "${snippet}" ]; then
        error "snippet ${snippet} already exists"
        return 1
    fi

    if [ -z "$EDITOR" ]; then
        EDITOR=vim
    fi

    $EDITOR "$snippet"
}

edit() {
    [ -z "${snippet}" ] && return 1

    if [ -z "$EDITOR" ]; then
        EDITOR=vim
    fi

    $EDITOR "$snippet"
}

gui() {
    # Use the filenames in the snippy directory as menu entries.
    # Get the menu selection from the user.
    # shellcheck disable=SC2086
    set +o errexit

    snippet=$( list | sed -re 's_^([^/]*)/(.*)_&\x0icon\x1f\1_' | rofi "${rofi_args[@]}" -p '❯ ')

    if [ $? -eq 10 ]; then
        script_content=true
    fi

    set -o errexit
}

run() {
    local paste="${1:-true}"
    local current_clipboard cursor_line cursor_line_position cursor_line cursor_position cursor_line_lenght

    # just return if nothing was selected
    [ -z "${snippet}" ] && return 1

    if [ -f "${snippets_directory}/${snippet}" ]; then

        # Put the contents of the selected file into the paste buffer.
        # If file is empty, the content is the basename of the file
        if [ ! -s "${snippets_directory}/${snippet}" ]; then
            content="$(basename "${snippet}")"

        # Custom selection, copy the script content without parsing
        elif [ "$script_content" = true ]; then
            content="$( cat "${snippets_directory}/${snippet}" )"

        # don't parse file with the ##noparse header
        elif grep -qE "^##noparse" "${snippets_directory}/${snippet}"; then
            content="$( tail -n +2 "${snippets_directory}/${snippet}" )"

        # replace tmpfile for snippets with ##tmpfile header
        elif grep -qE "^##tmpfile" "${snippets_directory}/${snippet}"; then
            content="$( bashdown_simple <<< "$(tail -n +2 "${snippets_directory}/${snippet}" | sed "s%\$tmpfile%$tmpfile%g" )" )"

        # execute bash script in scripts dir
        elif [[ $(dirname "${snippet}") == 'scripts' ]] && grep -qE "^#!/bin/bash" "${snippets_directory}/${snippet}"; then
            content="$( bash "${snippets_directory}/${snippet}" )"

        # default action
        else
            content="$( bashdown_simple < "${snippets_directory}/${snippet}" )"
        fi

        if [ -n "$content" ]; then
            printf "%s" "$content" > "$tmpfile"
        fi

    else [[ ${snippet} =~ ^$ ]]
        ${snippet##*$} 2>/dev/null > "$tmpfile" # execute as bashcommand
    fi

    # if tmpfile is empty at this step, nothing to do.
    if [ ! -s "$tmpfile" ]; then
        return
    fi

    # save current clipboard
    current_clipboard=$(xsel --clipboard)
    # clear clipboard
    xsel --clipboard --clear
    # replace {clipboard} by the clipboard content
    # use awk to handle correctly multiline clipboard
    if grep -q "$placeholder_clipboard" "$tmpfile"; then
        awk -i inplace \
            -v clipboard="$current_clipboard" \
            -v placeholder="$placeholder_clipboard" \
            '{ gsub(placeholder, clipboard); print }' "$tmpfile"

        # remove last EOL
        perl -pi -e 'chomp if eof' "$tmpfile"
    fi

    if grep -q "$placeholder_clipboard_urlencode" "$tmpfile"; then
        awk -i inplace \
            -v clipboard="$( echo "$current_clipboard" | jq -sRr @uri)" \
            -v placeholder="$placeholder_clipboard_urlencode" \
            '{ gsub(placeholder, clipboard); print }' "$tmpfile"

        # remove last EOL
        perl -pi -e 'chomp if eof' "$tmpfile"
    fi

    # define cursor position and line at 0, we don't need to go up or left if there is no {cursor} placeholder
    cursor_line_position=0
    cursor_position=0

    # Check if there is a {cursor} placeholder
    if grep -qF $placeholder_cursor "$tmpfile"; then
        # retrieve the line number of the cursor placeholder
        cursor_line_position=$(grep -n "$placeholder_cursor" "$tmpfile" | cut -d: -f1)
        # retrieve the line
        cursor_line=$(grep $placeholder_cursor "$tmpfile")
        # calculate snippet total lines
        file_lines=$(wc -l < "$tmpfile")
        # determine the number of line to go up
        cursor_line_position=$(( file_lines - cursor_line_position + 1 ))
        # Extract cursor position
        cursor_position=$(strindex "$cursor_line" $placeholder_cursor)
        # total cursor line lenght
        cursor_line_lenght=${#cursor_line}
        # Compute the final cursor position ( 8 is the lenght of the placeholder {cursor} )
        cursor_position=$(( cursor_line_lenght - cursor_position - 8 ))
        # remove the placeholder
        sed -i -e "s/$placeholder_cursor//g" "$tmpfile"
    fi

    # Copy snippet in clipboard
    if is_rich_snippet "${snippets_directory}/${snippet}"; then
        xclip -target text/html -selection clipboard -in -loops 1 < "$tmpfile"
    else
        xsel --clipboard --input < "$tmpfile"
    fi

    if [ "$paste" = true ] ; then
        # Paste into the current application.
        if is_gui; then
            # We need a little pause to handle the time to focus on the window
            sleep "$focus_wait"
            # Paste
            xdotool key ctrl+v sleep "$focus_wait"
            move_cursor "Up" $cursor_line_position
        else
            xdotool key ctrl+shift+v sleep "$focus_wait"
            if is_vim; then
                move_cursor "Up" $cursor_line_position
            fi
        fi

        move_cursor "Left" $cursor_position

        # We need a little pause to handle the time to have the content of tmpfile pasted if is_gui
        sleep "$focus_wait"

        # Restore current clipboard
        echo -ne "$current_clipboard" | xsel --clipboard --input
    fi
}

main() {
    parse_options "$@"

    cd "${snippets_directory}" || exit

    case "$action" in
        'gui' )
            gui
            run
        ;;
        'cli' )
            cli
            run false
        ;;
        'list' )
            list
        ;;
        'cat' )
            list d
        ;;
        'add' )
            shift
            add "$@"
        ;;
        'edit' )
            cli
            edit
        ;;
        *)
            error "Action $action does not exists"
            exit 1
        ;;
    esac
}

init && main "$@"