#!/usr/bin/bash

#shellcheck disable=SC1090
. "$(sfpath)" || exit 3

shellfu import pretty
shellfu import isa

usage() {
    mkusage "$@" \
        "[options] -- CMD [ARG].."                                      \
        "[options] -c CODE"                                             \
        "[options] -l"                                                  \
        "[options] -V|--version"                                        \
        "[options] --help"                                              \
    -c                                                                  \
        "-- CMD [ARG].. Run command CMD with any number of arguments."  \
        "-c CODE        Run Bash code CODE.  Notice that CODE is just"  \
        "               ine argument so beware of proper quoting."      \
        "-l             List supported EMULATORs and exit."             \
        "-V             Show version number and exit."                  \
    -o                                                                  \
        "-B             Don't emit BEL character after the command;"    \
        "               default is to emit only on non-zero status"     \
        "-L DELAY       Add DELAY before starting commands, eg. '1.2'," \
        "               '3' or '.4' are OK."                            \
        "-P             Don't pause (ask for Enter) after the command;" \
        "               default is to pause only on non-zero status"    \
        "-T DELAY       If pausing after the command, give up the pause"\
        "               after DELAY, eg. '1.2', '3' or '4'.  See also"  \
        "               -p and -P."                                     \
        "-W             Don't touch window class name (see -w)."        \
        "-b             Emit BEL character after the command;"          \
        "               default is to emit only on non-zero status"     \
        "-d             Enable debugging output."                       \
        "-e EMULATOR    Use emulator EMULATOR"                          \
        "-n             Don't run emulator, just print CODE that would" \
        "               be passed and exit;"                            \
        "-o [no]FLAG    Set option flag FLAG.  Flags are binary, and"   \
        "               have opposite meaning if prefixed with 'no'."   \
        "               Repeat multiple times to set more flags."       \
        "-p             Pause (ask for Enter) after the command;"       \
        "               default is to pause only on non-zero status"    \
        "-t TITLE       Try to set window title to TITLE.  See below"   \
        "               for details on title setting."                  \
        "-w CLASSNAME   try to set window class name (AKA 'instance')"  \
        "               to CLASSNAME.  By default, the class name is"   \
        "               set to last element of CMD (command name),"     \
        "               or left untouched if CODE syntax was used."     \
        "-v             Enable verbose output."                         \
        ""                                                              \
        "Window title setting is on a best-effort basis; there are few" \
        "methods to do it but but none work 100% reliably.  'emulwt'"   \
        "method will try to use emulator's arguments, but not all"      \
        "emulators support this.  'ttywt' method will inject a code"    \
        "snippet that will set the title using standard TTY escape"     \
        "sequences.  Beware that programs ran as part of CMD or CODE"   \
        "might set title to something else; twinner cannot do anything."\
        "to prevent that.  Both methods are enabled by default; to"     \
        "disable them, set flag, eg. '-o noemulwt' to disable 'emulwt'" \
        "method."                                                       \
        ""                                                              \
        "CLASSNAME setting is also not reliable.  Currently it depends" \
        "on the new emulator window being active as determined by"      \
        "xdotool, which might depend on behavior of the window emulator"\
        "and might be affected by race conditions, eg. if multiple"     \
        "windows are spawned in quick succession."                      \
        ""                                                              \
        "DELAY for -T and -L option must be a simple dot-separated"     \
        "decimal number.  For example '12,3', '4,567.8' are invalid,"   \
        "but '1.2', '3,' or '.4' are all valid values."                 \
        ""                                                              \
        "Note that the purpose of the BEL character is to bring user's" \
        "attention to the given window.  Depending on your system"      \
        "configuration, this might mean auditory signal (eg. a rather"  \
        "annoying beep) or visual signal (eg. coloring or blinking the" \
        "window title) or nothing."
}

TWINNER__EMULATOR=${TWINNER__EMULATOR:-urxvt}
TWINNER__LOGFILE=${TWINNER__LOGFILE:-"$HOME/.local/share/twinner/exec.log"}

_dry_run() {
    local i=0
    for arg in "$@"; do
        #shellcheck disable=SC2001
        sed "s/^/$i=/" <<< "$arg"
        ((i++))
    done
}

__twinner__exec() {
    #
    # Execute emulator $Emulator with command $Command and $Title
    #
    local args=()
    local do_emulwt=true
    test -n "$Title" || do_emulwt=false
    flag emulwt || do_emulwt=false

    case $Emulator in
        alacritty)
            args=(alacritty)
            $do_emulwt && args+=(-T "$Title")
            args+=(-e bash -c "$Code")
            ;;
        ghostty)
            args=(ghostty)
            $do_emulwt && args+=(--title="$Title")
            args+=(--command="$Code")
            ;;
        urxvt)
            args=(urxvt)
            $do_emulwt && args+=(-title "$Title")
            args+=(-e bash -c "$Code")
            ;;
        gnome-terminal)
            args=(gnome-terminal)
            args+=(-- bash -c "$Code")
            ;;
        xfce4-terminal)
            args=(xfce4-terminal)
            $do_emulwt && args+=(--title="$Title")
            args+=(-x bash -c "$Code")
            ;;
        terminator)
            args=(terminator)
            $do_emulwt && args+=(--title="$Title")
            args+=(-e "$Code")
            ;;
        konsole)
            args=(konsole)
            $do_emulwt && args+=(-p LocalTabTitleFormat="$Title")
            args+=(-e "$Code")
            ;;
        lilyterm)
            args=(lilyterm)
            $do_emulwt && args+=(--title="$Title")
            args+=(-e bash -c "$Code")
            ;;
        termit)
            args=(termit)
            args+=(-e "$Code")
            ;;
        *)
            die "unsupported terminal emulator: $Emulator"
            ;;
    esac
    command -v "$Emulator" >/dev/null || die "emulator not installed: $Emulator"
    debug -v args
    "$DryRun" && _dry_run "${args[@]}" && return 0
    mkdir -p "$(dirname "$LogFile")"
    "${args[@]}" |& ts "(%F %T) | " >>"$LogFile"
}


lsemuls() {
    #
    # List supported emulators
    #
    echo alacritty
    echo ghostty
    echo gnome-terminal
    echo konsole
    echo lilyterm
    echo terminator
    echo termit
    echo urxvt
    echo xfce4-terminal
}

print_ver() {
    #
    # Print version
    #
    echo 0.0.10
}

shtitle() {
    #
    # Print prefixable Bash code to set title
    #
    { test -n "$Title" && flag ttywt; } || return 0
    printf '%s\n' "printf '\033]0;%s\007' $(printf '%q' "$Title"); "
}

flag() {
    #
    # True if flag $1 set in $Flags
    #
    # Usage:
    #    flag [no]FLAG
    #
    # Exit with 0 if flag was found in current list of $Flags.
    # Exit with 1 if flag was not found, or a negative flag,
    # ie. 'no$1' was found.
    #
    local qflag=$1  # queried flag
    local lflag     # each effective flag
    test -n "$qflag" || warn "invalid call of flag(); no FLAG"
    for lflag in $(grep -owE '\w+' <<<"$Flags" | tac); do
        test "$lflag" == "$qflag"   && return 0
        test "$lflag" == "no$qflag" && return 1
    done
    return 1
}

default_flags() {
    #
    # Set default flags
    #
    {
        echo emulwt
        echo ttywt
    } | paste -sd','
}

bell_posthook() {
    #
    # Print Bash code to ring the bell if needed
    #
    local bell_chr=$'\a'
    local code=""
    case $Bell in
        always)     code="echo '$bell_chr'" ;;
        never)      code="" ;;
        onerr)      code="test \$_TWINNER_CODE_ES -gt 0 && echo '$bell_chr'"
    esac
    echo "$code    # Bell=$Bell"
}

mkpause() {
    #
    # Print pause code
    #
    local timeout_args=""
    local es_line="exit status was \$_TWINNER_CODE_ES"
    local close_line="press Enter to close the window"
    test -n "$PauseTimeout" && {
        timeout_args="-t $PauseTimeout"
        close_line="press Enter or wait $PauseTimeout seconds to close the window"
    }
    printf 'read -r -p "... twinner: %s\n=== twinner: %s ===" %s _TWINNER_CODE_TMP;' \
        "$es_line" \
        "$close_line" \
        "$timeout_args"
}

pause_posthook() {
    #
    # Print pause code if needed
    #
    local bell_chr=$'\a'
    local code=""
    case $Pause in
        always)     code="$(mkpause)" ;;
        never)      code="" ;;
        onerr)      code="test \$_TWINNER_CODE_ES -gt 0 && $(mkpause)" ;;
    esac
    echo "$code    # Pause=$Pause PauseTimeout=$PauseTimeout"
}

delay_prehook() {
    #
    # Print delay code if needed
    #
    test -n "$Delay" || return 0
    isa__posnum "$Delay" || {
        warn "ignoring invalid delay value: $Delay not a positive number"
        return 1
    }
    echo "sleep $Delay;"
}

guess_classname() {
    local command_head="${Command[0]}"
    test "${#Command[*]}" -gt 0 || return 1
    test -n "$command_head" || return 1
    echo "${command_head##*/}"
}

classname_prehook() {
    #
    # Print WM_CLASS setting code if needed
    #
    local classname
    case "$Classname" in
        never) return 0 ;;
        auto)   classname="$(guess_classname)" || return 0 ;;
        user=*) classname="${Classname#user=}" ;;
        *)  die "invalid \$Classname syntax: '$Classname'" ;;
    esac
    printf "xdotool getactivewindow set_window --classname %q;" $classname
    # TODO: try to determine and use --pid (which might be bad in case of
    # single-PID terminal apps)
}

posthooks() {
    #
    # Append hooks to CODE
    #
    echo
    echo '_TWINNER_CODE_ES=$?'
    bell_posthook
    pause_posthook
}

prehooks() {
    #
    # Pre-pend hooks to CODE
    #
    delay_prehook
    classname_prehook
    echo
}

main() {
    local DryRun=false
    local Bell=onerr
    local Command=()
    local Emulator=$TWINNER__EMULATOR
    local Flags
    local Delay=0
    local LogFile=$TWINNER__LOGFILE
    local Pause=onerr
    local PauseTimeout
    local Title
    local Classname=auto
    local Code
    Flags=$(default_flags)
    while true; do case $1 in
        -B) Bell=never;     shift ;;
        -L) Delay="$2";     shift 2 || usage -w "missing value to: $1" ;;
        -P) Pause=never;    shift ;;
        -T) PauseTimeout="$2"; shift 2 || usage -w "missing value to: $1" ;;
        -W) Classname=never; shift ;;
        -V) print_ver; exit 0 ;;
        -b) Bell=always;    shift ;;
        -c) Code="$2";      shift 2 || usage -w "missing value to: $1"; break ;;
        -d) PRETTY_DEBUG=true ; shift ;;
        -e) Emulator="$2";  shift 2 || usage -w "missing value to: $1" ;;
        -l) lsemuls; exit ;;
        -n) DryRun=true;    shift ;;
        -o) Flags+=",$2";   shift 2 || usage -w "missing value to: $1" ;;
        -p) Pause=always;   shift ;;
        -t) Title="$2";     shift 2 || usage -w "missing value to: $1" ;;
        -v) PRETTY_VERBOSE=true; shift  ;;
        -w) Classname=user="$2" shift 2 || usage -w "missing value to: $1" ;;
        --) shift;  break ;;
        --help)     usage -k ;;
        --version)  print_ver; exit 0 ;;
        -*) usage -w "unknown argument: $1" ;;
        *)  break ;;
    esac done
    Command=("$@")
    test -z "$Code" \
     && test -z "${Command[*]}" \
     && usage -w "neither CMD nor CODE seen"
    test -n "$Code" \
     && test -n "${Command[*]}" \
     && usage -w "both CMD and CODE seen"
    test -n "$PauseTimeout" && {
        isa__posnum "$PauseTimeout" || usage -w "invalid DELAY"
    }
    test -n "$Code" || {
        Code=$(printf '%q ' "${Command[@]}")
        Code=${Code% }
        Code="$(shtitle)$Code"
    }
    Code="$(prehooks)$Code"
    Code="$Code$(posthooks)"
    debug -v Code Command Flags
    __twinner__exec
}

true "$PRETTY_DEBUG" "$PRETTY_VERBOSE"  # make friends with ShellCheck

main "$@"
