#!/bin/bash

###########################################################
## Copyright (c) 2002-2023 Alexey Kuryakin daqgroup@mail.ru
###########################################################

###########################################################
## utility to emulate tooltip-notifier.cmd/fpquisend.exe
## call notify-send with with modified parameters
###########################################################

###########################################################
source $(crwkit which crwlib_base.sh); # Use base library #
###########################################################

####################################
# Print version, copyright, help etc
####################################
function print_version(){ echo "$scriptname version 2.1 built 20250513"; };
function print_copyright(){ echo "Copyright (c) 2002-2025 Alexey Kuryakin daqgroup@mail.ru"; };
function print_help(){
 print_version;
 print_copyright;
 echo "";
 echo "Help on $scriptname:";
 echo "*************************";
 echo "Show tooltip popup window to notify user on any event(s).";
 echo "It'is wrapper for notify-send tool, see 'man notify-send'.";
 echo "";
 echo "Usage: unix $scriptname [arguments]";
 echo "";
 echo "arguments are list of 'name value' pairs:";
 echo "";
 echo " NAME          VALUE(example) - COMMENT.";
 echo " preset        xxx            - Preset predefined parameters named 'xxx', see below:";
 echo "               xxx            = stdOk,stdNo,stdHelp,stdStop,stdDeny,stdAbort,stdError,stdFails,stdSiren,";
 echo "                              = stdAlarm,stdAlert,stdBreak,stdCancel,stdNotify,stdTooltip,stdSuccess,";
 echo "                              = stdWarning,stdQuestion,stdException,stdAttention,stdInformation,";
 echo "                              = stdExclamation,stdFpQui";
 echo "                              NB: preset xxx should be first in the list of parameters";
 echo " verbose       1              - Verbose mode  (default = 0 is silent execution)";
 echo " noDouble      1              - noDouble mode (default = 0 is enable double text)";
 echo " head          'Greeting'     - Head, i.e. Title (caption) also named Summary.";
 echo " text          'Hello world'  - Text to display";
 echo " delay         15000          - Close after 15000 ms";
 echo " audio         notify.wav     - Play sound (*.wav file)";
 echo " wav           notify.wav     - Synonym of audio";
 echo " ico           notify.ico     - Show icon file (*.ico)";
 echo " avi           alert.avi      - Show avi 32x32 icon file (*.avi)";
 echo " bkColor       red            - Background color: blue,red,green etc";
 echo " textColor     0xFF0000       - Text color, RGB, Hex, 0xRRGGBB";
 echo " trans         255            - Transparency 0..255";
 echo " font          'PT Mono'      - Font family name";
 echo " bold          1              - Bold font style";
 echo " fontSize      16             - Font size, pt";
 echo " onClick       'cmd arg'      - On click run command 'cmd arg'";
 echo " cmd0          'cmd arg'      - Symonym of onClick";
 echo " untilClickAny 1              - Close on any click";
 echo " btn1..btn9    'Button label' - Button 1..9 label text";
 echo " cmd1..cmd9    'cmd arg'      - Button 1..9 command";
 echo " guid          '{..}'         - Assign target window GUID which uses";
 echo "                              - to refresh updateable windows content";
 echo " sure          0/1            - Synonym of createIfNotVisible flag";
 echo "                              - Uses to be sure that GUID window appears";
 echo " createIfNotVisible 0/1       - 0/1=not/create window if one not visible";
 echo "                              - Uses only with guid option; default=1";
 echo " delete        'guid'         - delete message window with given GUID";
 echo " progress      p              - Show progress bar, p=0..100 percent";
 echo "                              - Uses with GUID to update progress";
 echo " run           'cmd arg'      - Run command on message received";
 echo "                              - Command runs only if window visible";
 echo " xml           '<x>..</x>'    - Append xml expression to message";
 echo "Special parameters for Linux version:";
 echo " hbsep         ': '           - Separator between head and body if head is not specified";
 echo " --demo                       - Show demo for all preset notification types.";
 echo " --test                       - Run in test mode: print command line instead of execute.";
 echo " --kill                       - Kill (terminate) notifications server process.";
 echo " --pid                        - Print notifier server process PID."
 echo " --status                     - Print notifier server process status info and exit.";
 echo " --monitor                    - Synonym of --monitor1.";
 echo " --monitor1                   - Start DBUS monitor to observe Notifications events.";
 echo " --monitor2                   - Start another DBUS monitor on Notifications events.";
 echo " --monitor1s                  - Start DBUS --monitor1 with option --system.";
 echo " --monitor2s                  - Start DBUS --monitor2 with option --system.";
 echo " --list-avail                 - List available (registered in DBUS) notifiers.";
 echo " --ini \"sound-on 1\"           - set INI file parameter (sound-on = 1) & exit";
 echo "";
 echo " Any argument (except text) can be omited.";
 echo "";
 echo "Examples:";
 echo "";
 echo " unix $scriptname head \"Demo Summary\" text \"This is test message.\" preset stdInformation delay 7000";
 echo " unix $scriptname text \"Help: This is standard Help message.\" preset stdHelp delay 15000";
 echo " unix $scriptname text \"Error: This is standard Error message.\" preset stdError delay 15000";
 echo " unix $scriptname text \"Notify: This is standard Notify message.\" preset stdNotify delay 15000";
 echo " unix $scriptname text \"Warning: This is standard Warning message.\" preset stdWarning delay 15000";
 echo " unix $scriptname text \"Question: This is standard Question message.\" preset stdQuestion delay 15000";
 echo " unix $scriptname text \"Information: This is standard Information message.\" preset stdInformation delay 15000";
 echo " unix $scriptname text \"This is demo, no double.\" delay 15000 noDouble 1 onClick \"msg * Demo.\"";
 echo " unix $scriptname text \"This is demo with icons.\" delay 15000 bkColor red ico warning.ico";
 echo " unix $scriptname text \"This is demo with sound.\" delay 15000 bkColor yellow audio notify.wav";
 echo " unix $scriptname text \"This is demo with fonts.\" delay 15000 bkColor blue textColor 0xFF0000 fontSize 32 font \"PT Mono\"";
 echo " unix $scriptname text \"Help on cmd.\" preset stdHelp delay 15000 btn1 \"Show\" cmd1 \"/opt/demo/help/manual.chm\"";
 echo " unix $scriptname guid \"{8A7260EB-79ED-4ABE-AC9C-1C4F2AE2F4AC}\" sure 1 text \"Progress 10%.\" progress 10 preset stdTooltip";
 echo " unix $scriptname guid \"{8A7260EB-79ED-4ABE-AC9C-1C4F2AE2F4AC}\" sure 0 text \"Progress 90%.\" progress 90 preset stdTooltip delay 60000";
 echo " unix $scriptname delete \"{8A7260EB-79ED-4ABE-AC9C-1C4F2AE2F4AC}\"";
 echo "";
 echo "Notes:";
 echo "";
 echo "1) Available library ico:";
 echo "-------------------------";
 if pushd $dir_ico >/dev/null 2>&1; then ls *.ico; fi; popd >/dev/null 2>&1;
 echo "";
 echo "2) Available library wav:";
 echo "-------------------------";
 if pushd $dir_wav >/dev/null 2>&1; then ls *.wav; fi; popd >/dev/null 2>&1;
 echo "";
 echo "3) Available library avi:";
 echo "-------------------------";
 if pushd $dir_avi >/dev/null 2>&1; then ls *.avi; fi; popd >/dev/null 2>&1;
 echo "";
};

################################################
# List of all preset names without 'std' prefix.
################################################
readonly preset_list="ok no help stop deny abort error fails siren alarm alert break cancel notify tooltip success warning question exception attention information exclamation fpqui";

#####################################
# Show demo with all preset messages.
#####################################
function show_demo(){
 echo "$(langstr ru "Демонстрация работы утилиты: " en "Demonstration of tool's work: ") unix tooltip-notifier";
 echo "$(langstr ru "Примечание: вызов tooltip-notifier text 'Summary: Body' работает как tooltip-notifier head 'Summary:' text 'Body'" en "Note: call tooltip-notifier text 'Summary: Body' working as tooltip-notifier head 'Summary:' text 'Body'")";
 wait_any_key 60;
 for pre in $preset_list default; do
  local PRE="$(upper_case $pre)";
  local cmd=(unix tooltip-notifier text "$(langstr ru Сообщение en Message) $PRE: $( langstr ru "Стандартное сообщение типа" en "Standard message of type") preset std$PRE." preset std$pre delay 5000);
  print_quoted_strings "${cmd[@]}"; "${cmd[@]}";
  sleep 5;
 done;
};

#######################################################################################################
# get/set INI file parameter:
# git_ini -get inifile section key
# git_ini -set inifile section key value
# git_ini -get $HOME/.config/daqgroup/tooltip-notifier/tooltip-notifier.ini tooltip-notifier sound-on
# git_ini -set $HOME/.config/daqgroup/tooltip-notifier/tooltip-notifier.ini tooltip-notifier sound-on 1
#######################################################################################################
function git_ini(){
 local op="$1"; local ini="$2"; local sec="$3"; local key="$4"; local val="$5";
 case $op in
  -get|--get) if [[ $# -eq 4 ]] && [[ -e "$ini" ]];                then git config -f $ini $sec.$key;        else return 1; fi; ;;
  -set|--set) if [[ $# -eq 5 ]] && mkdir -p "$(dirname "$ini")"; then git config -f $ini $sec.$key "$val"; else return 1; fi; ;;
  *) return 1; ;;
 esac;
};

###################################
# Setup sound mode from/to INI file
###################################
function setup_sound_mode(){
 local ini="$HOME/.config/daqgroup/tooltip-notifier/tooltip-notifier.ini";
 local sec="tooltip-notifier"; local key="sound-on";
 if which git > /dev/null 2>&1; then
  if is_number "$1"; then
   git_ini -set $ini $sec $key $1;
  fi;
  local son="$(git_ini -get $ini $sec $key)";
  if is_number "$son"; then
   let sound_on=$son;
  else
   git_ini -set $ini $sec $key $sound_on;
  fi;
 fi;
};

####################
# --ini "sound-on 1"
####################
function handle_opt_ini(){
 if [[ $# -eq 2 ]]; then
  case $1 in
   sound-on) setup_sound_mode $2; ;;
   *) ;;
  esac;
 fi;
};

#######################################
# Location of resources: ico,png,wav,…
#######################################
readonly dir_ico="/opt/crwkit/add/ico";
readonly dir_png="/opt/crwkit/add/png";
readonly dir_wav="/opt/crwkit/add/wav";
readonly dir_avi="/opt/crwkit/add/avi";

####################
# Log file location.
####################
readonly datenum="$(date +%Y%m%d)";
readonly log_dir="$TMPDIR/daqgroup-$USER/$scriptbase";
readonly log_file="$log_dir/$datenum-tooltips.log";

################################
# Cleanup log files in $log_dir.
# Delete logfiles older $1 days.
# Delete *desktop older $2 days.
################################
function log_clean(){
 local n="$1"; # delete *.log files older n days
 if is_number "$n" && [[ $n -gt 0 ]] && [[ -d "$log_dir" ]]; then
  find "$log_dir" -name "*.log" -type f -mtime +$n -delete;
 fi;
 local m="$2"; # delete *.desktop files older m days
 if is_number "$m" && [[ $m -gt 0 ]] && [[ -d "$log_dir" ]]; then
  find "$log_dir" -name "*.desktop" -type f -mtime +$m -delete;
 fi;
 return; # skip obsolete version assumed filename yyyymmdd*.log.
 if is_number "$1" && [[ $1 -gt 0 ]] && [[ -d "$log_dir" ]]; then
  local list="$(ls $log_dir | grep '.log' | sort | head -n -$1)";
  for item in $list; do rm -f "$log_dir/$item"; done;
 fi;
};

##########################
# Write event to log file.
##########################
function log_event(){
 if [[ ! -d "$log_dir" ]]; then
  mkdir -p $log_dir;
  chmod 755 $log_dir;
 fi;
 if [[ -e "$log_file" ]]; then
  if [[ -z "$*" ]]; then
   echo "" >> $log_file;
  else
   echo -n "$(date +%Y.%m.%d-%H:%M:%S): " >> $log_file;
   print_quoted_strings "$@" >> $log_file;
  fi;
 else
  log_clean 30 1;
  touch $log_file;
  chmod 644 $log_file;
 fi;
};

##########################
# Get MD5 hash by args $*.
# May use md5sum .. cksum.
##########################
function hash_by_args(){
 for hasher in md5sum shasum sha1sum sha224sum sha256sum sha384sum sha512sum cksum; do
  if which $hasher >/dev/null 2>&1; then
   echo "$*" | xargs | $hasher | cut -f 1 -d ' ';
   break;
  fi;
 done;
};

####################################
# Get temporary *.desktop by action.
# Action is btn($1)+cmd(rest of $*).
####################################
function temp_desktop_by_action(){
 local dsk="$(echo "$log_dir/action_$(hash_by_args "$*").desktop")";
 local Name="$1"; shift;
 echo "[Desktop Entry]"  >  $dsk;
 echo "Name=$Name"       >> $dsk;
 echo "Exec=$*"          >> $dsk;
 echo "Type=Application" >> $dsk;
 echo "NoDisplay=true"   >> $dsk;
 echo "$dsk";
};

####################################
# Get reference (href=..) by action.
# Action is btn($1)+cmd(rest of $*).
# Returns original file or temporary
# desktop file to execute a command.
####################################
function href_by_action(){
 local btn="$1"; shift; local cmd="$1";
 local num="$(echo "$cmd" | wc -w)";
 if [[ $num -eq 1 ]]; then
  case $cmd in
   *.desktop)                   echo "$cmd"; ;;
   *.html|*.htm)                temp_desktop_by_action "$btn" "unix lister $cmd"; ;;  # HTML family
   *.pdf|*.djv|*.djvu|*.ps)     temp_desktop_by_action "$btn" "unix lister $cmd"; ;;  # PDF family
   *.bmp|*.png|*.gif|*.ico)     temp_desktop_by_action "$btn" "unix lister $cmd"; ;;  # Pictures
   *.jpg|*.jpeg|*.tiff|*.pcx)   temp_desktop_by_action "$btn" "unix lister $cmd"; ;;  # Pictures
   *.txt|*.text|*.ascii)        temp_desktop_by_action "$btn" "unix lister $cmd"; ;;  # Text
   *.ini|*.cfg|*.crc|*.conf)    temp_desktop_by_action "$btn" "unix lister $cmd"; ;;  # Config
   *.pas|*.inc|*.lpr|*.dpr)     temp_desktop_by_action "$btn" "unix lister $cmd"; ;;  # Pascal
   *.lpi|*.lps|*.lpk|*.pp)      temp_desktop_by_action "$btn" "unix lister $cmd"; ;;  # Pascal
   *.sml|*.fsm|*.sobj)          temp_desktop_by_action "$btn" "unix lister $cmd"; ;;  # FSM
   *.md|*.markdown)             temp_desktop_by_action "$btn" "unix lister $cmd"; ;;  # Markdown
   *.lst|*.list)                temp_desktop_by_action "$btn" "unix lister $cmd"; ;;  # Lists
   *.xml)                       temp_desktop_by_action "$btn" "unix lister $cmd"; ;;  # XML
   *.log)                       temp_desktop_by_action "$btn" "unix wintail $cmd"; ;; # Log
   *)                           echo "$cmd"; ;;
  esac;
 else
  temp_desktop_by_action "$btn" "$@";
 fi;
};

#################################
# Internal state/option variables
#################################
declare -a cmdline="";          # Command line to run (to be filled).
declare -i verb=0;              # Verbose mode (0/1=Off/On) to debug.
declare -i urgency=0;           # Urgency level 0/1/2=low/normal/critical.
declare -i delay=86400000;      # Delay (expire-time) before close, ms.
declare head="";                # Text of head.
declare body="";                # Text of body.
declare preset="default";       # Preset parameters.
declare ico="default.ico";      # Icon file  or stock image name.
declare wav="default.wav";      # Sound file or stock sound name.
declare avi="";                 # Movie file or stock movie name.
declare font="PT Mono";         # Font family.
declare -i size=10;             # Font size, pt.
declare -i bold=0;              # Font is bold (0/1).
declare -i trans=255;           # Transparency in range [0..255].
declare bcol="0xBC8BDA";        # Background color: RGB or name.
declare fcol="0x000000";        # Foreground color of text font.
declare -i sure=1;              # createIfNotVisible flag.
declare guid="";                # Notification unique identifier GUID.
declare -i clany=1;             # untilClickAny flag.
declare xml="";                 # XML text.
declare hbsep=": ";             # Separator between head and body if head is not set.
declare -i nodouble=0;          # noDouble flag.
declare -i progress=-1;         # Progress indicator if in range [0..100].
declare delete="";              # guid to delete.
declare run="";                 # Command to run on click.
declare button="";              # Button  to run a command.
declare server_caps="";         # Notification server capabilities.
declare actual_caps="";         # Notification server capabilities with extension.
declare -t testmode=0;          # In testmode=1 do not run command line but print.
declare -i hex_rgb=1;           # Interpret HEX as 0:0xBBGGRR, 1:0:0xRRGGBB
declare -i sound_on=1;          # 0/1=not/play sound
declare btn0=""; declare btn1=""; declare btn2=""; declare btn3=""; declare btn4="";
declare btn5=""; declare btn6=""; declare btn7=""; declare btn8=""; declare btn9="";
declare cmd0=""; declare cmd1=""; declare cmd2=""; declare cmd3=""; declare cmd4="";
declare cmd5=""; declare cmd6=""; declare cmd7=""; declare cmd8=""; declare cmd9="";

readonly fgqui_violet="0xBC8BDA"; # FP-QUI violet color.

#####################################
# Helper functions to assign options.
#####################################
function assign_body(){ body="$1"; };
function assign_head(){ head="$1"; };
function assign_font(){ font="$1"; };
function assign_bold(){ let "bold=$1"; }
function assign_size(){ let "size=$1"; };
function assign_verb(){ let "verb=$1"; };
function assign_ico(){ ico="$(find_res $1)"; }
function assign_avi(){ avi="$(find_res $1)"; }
function assign_wav(){ wav="$(find_res $1)"; }
function assign_xml(){ xml="$1"; };
function assign_delay(){ let "delay=$1"; };
function assign_trans(){ trans="$1"; };
function assign_clany(){ clany="$1"; };
function assign_preset(){ preset="$1"; };
function set_urgency(){ urgency="$1"; };
function assign_bcol(){ bcol="${1,,}"; };
function assign_fcol(){ fcol="${1,,}"; };
function assign_sure(){ let "sure=$1"; }
function assign_guid(){ guid="$1"; };
function assign_btn0(){ btn0="$1"; }
function assign_btn1(){ btn1="$1"; }
function assign_btn2(){ btn2="$1"; }
function assign_btn3(){ btn3="$1"; }
function assign_btn4(){ btn4="$1"; }
function assign_btn5(){ btn5="$1"; }
function assign_btn6(){ btn6="$1"; }
function assign_btn7(){ btn7="$1"; }
function assign_btn8(){ btn8="$1"; }
function assign_btn9(){ btn9="$1"; }
function assign_cmd0(){ cmd0="$1"; }
function assign_cmd1(){ cmd1="$1"; }
function assign_cmd2(){ cmd2="$1"; }
function assign_cmd3(){ cmd3="$1"; }
function assign_cmd4(){ cmd4="$1"; }
function assign_cmd5(){ cmd5="$1"; }
function assign_cmd6(){ cmd6="$1"; }
function assign_cmd7(){ cmd7="$1"; }
function assign_cmd8(){ cmd8="$1"; }
function assign_cmd9(){ cmd9="$1"; }
function assign_hbsep(){ hbsep="$1"; };
function set_progress(){ let "progress=$1"; };
function set_nodouble(){ let "nodouble=$1"; };
function assign_run(){ run="$1"; };
function set_button(){ button="$1"; };
function set_delete(){ delete="$1"; };
function bold_head(){ if [[ $bold -gt 0 ]]; then true; else false; fi; }
function bold_body(){ if [[ $bold -gt 0 ]]; then true; else false; fi; }

#################
# Perform preset.
#################
function do_preset(){
 set_urgency  "$1"; shift;
 assign_delay "$1"; shift;
 assign_font  "$1"; shift;
 assign_bold  "$1"; shift;
 assign_size  "$1"; shift;
 assign_fcol  "$1"; shift;
 assign_bcol  "$1"; shift;
 assign_ico   "$1"; shift;
 assign_wav   "$1"; shift;
 assign_avi   "$1"; shift;
 set_nodouble "$1"; shift;
};

########################
# Apply selected preset.
########################
function make_preset(){
 local p="$(lower_case "$1")";
 if [[ $verb -gt 0 ]]; then echo "Make preset: $p"; fi;
 case $p in
  #preset predefined params urgency delay     font     bold size textColor   bkColor       ico              wav             avi             noDouble
  stdok)          do_preset 1       60000    "PT Mono" 0    14   darkgreen   palegreen     ok.png           ok.wav          ok.avi          1; ;;
  stdno)          do_preset 1       60000    "PT Mono" 0    14   brown       mistyrose     no.png           no.wav          no.avi          1; ;;
  stdhelp)        do_preset 0       60000    "PT Mono" 0    14   navy        aliceblue     help.png         help.wav        help.avi        1; ;;
  stdstop)        do_preset 1       3600000  "PT Mono" 0    14   maroon      mistyrose     stop.png         stop.wav        stop.avi        1; ;;
  stddeny)        do_preset 2       86400000 "PT Mono" 1    14   maroon      mistyrose     deny.png         deny.wav        deny.avi        1; ;;
  stdabort)       do_preset 2       86400000 "PT Mono" 1    14   maroon      mistyrose     abort.png        abort.wav       abort.avi       1; ;;
  stderror)       do_preset 2       86400000 "PT Mono" 1    14   maroon      mistyrose     error.png        error.wav       error.avi       1; ;;
  stdfails)       do_preset 2       86400000 "PT Mono" 1    14   maroon      mistyrose     fails.png        fails.wav       fails.avi       1; ;;
  stdsiren)       do_preset 2       86400000 "PT Mono" 1    14   maroon      mistyrose     siren.png        siren.wav       siren.avi       1; ;;
  stdalarm)       do_preset 2       86400000 "PT Mono" 1    14   maroon      mistyrose     alarm.png        alarm.wav       alarm.avi       1; ;;
  stdalert)       do_preset 2       86400000 "PT Mono" 1    14   maroon      mistyrose     alert.png        alert.wav       alert.avi       1; ;;
  stdbreak)       do_preset 1       3600000  "PT Mono" 0    14   maroon      mistyrose     break.png        break.wav       break.avi       1; ;;
  stdcancel)      do_preset 1       3600000  "PT Mono" 0    14   maroon      mistyrose     cancel.png       cancel.wav      cancel.avi      1; ;;
  stdnotify)      do_preset 0       15000    "PT Mono" 0    14   black       pink          notify.png       notify.wav      notify.avi      1; ;;
  stdtooltip)     do_preset 0       15000    "PT Mono" 0    14   black       pink          tooltip.png      tooltip.wav     tooltip.avi     1; ;;
  stdsuccess)     do_preset 1       60000    "PT Mono" 0    14   darkgreen   palegreen     success.png      success.wav     success.avi     1; ;;
  stdwarning)     do_preset 1       3600000  "PT Mono" 0    14   brown       lightyellow   warning.png      warning.wav     warning.avi     1; ;;
  stdquestion)    do_preset 1       3600000  "PT Mono" 0    14   navy        aliceblue     question.png     question.wav    question.avi    1; ;;
  stdexception)   do_preset 2       86400000 "PT Mono" 1    14   brown       lightyellow   exception.png    exception.wav   exception.avi   1; ;;
  stdattention)   do_preset 1       3600000  "PT Mono" 0    14   brown       lightyellow   attention.png    attention.wav   attention.avi   1; ;;
  stdinformation) do_preset 0       60000    "PT Mono" 0    14   navy        aliceblue     information.png  information.wav information.avi 1; ;;
  stdexclamation) do_preset 1       3600000  "PT Mono" 0    14   brown       lightyellow   exclamation.png  exclamation.wav exclamation.avi 1; ;;
  stdfpqui)       do_preset 0       15000    "PT_Mono" 0    14   black       $fgqui_violet tooltip.png      tooltip.wav     tooltip.avi     1; ;;
  default|*)      do_preset 0       5000     "PT Mono" 0    12   black       aliceblue     information.png  information.wav information.avi 1; ;;
 esac;
 assign_preset $p;
};

########################
# Header by preset name.
########################
function preset_head(){
 case $1 in
  stdok)          echo "$(langstr ru "Подтверждено"     en "Confirmed"    )"; ;;
  stdno)          echo "$(langstr ru "Отказано"         en "Refused"      )"; ;;
  stdhelp)        echo "$(langstr ru "Справка"          en "Help"         )"; ;;
  stdstop)        echo "$(langstr ru "Остановка"        en "Stop"         )"; ;;
  stddeny)        echo "$(langstr ru "Отказ"            en "Deny"         )"; ;;
  stdabort)       echo "$(langstr ru "Сбой"             en "Abort"        )"; ;;
  stderror)       echo "$(langstr ru "Ошибка"           en "Error"        )"; ;;
  stdfails)       echo "$(langstr ru "Критический Сбой" en "Failure"      )"; ;;
  stdsiren)       echo "$(langstr ru "Тревога"          en "Failure"      )"; ;;
  stdalarm)       echo "$(langstr ru "Тревога"          en "Alarm"        )"; ;;
  stdalert)       echo "$(langstr ru "Тревога"          en "Alert"        )"; ;;
  stdbreak)       echo "$(langstr ru "Разрыв"           en "Break"        )"; ;;
  stdcancel)      echo "$(langstr ru "Отмена"           en "Cancel"       )"; ;;
  stdnotify)      echo "$(langstr ru "Уведомление"      en "Notification" )"; ;;
  stdtooltip)     echo "$(langstr ru "Уведомление"      en "Notification" )"; ;;
  stdsuccess)     echo "$(langstr ru "Сообщение"        en "Message"      )"; ;;
  stdwarning)     echo "$(langstr ru "Предупреждение"   en "Warning"      )"; ;;
  stdquestion)    echo "$(langstr ru "Сообщение"        en "Message"      )"; ;;
  stdexception)   echo "$(langstr ru "Критический Сбой" en "Failure"      )"; ;;
  stdattention)   echo "$(langstr ru "Внимание"         en "Attention"    )"; ;;
  stdinformation) echo "$(langstr ru "Информация"       en "Information"  )"; ;;
  stdexclamation) echo "$(langstr ru "Восклицание"      en "Exclamation"  )"; ;;
  stdfpqui)       echo "$(langstr ru "Сообщение"        en "Message"      )"; ;;
  default|*)      echo "$(langstr ru "Сообщение"        en "Message"      )"; ;;
 esac;
};

#######################
# Apply default preset.
#######################
function preset_default(){
 if [[ "$preset" = "default" ]]; then
  make_preset "$preset";
 fi;
};

#####################
# Find resource file.
#####################
function find_res(){
 local res="$1";
 if [[ -n "$res" ]]; then
  if [[ "${res:0:1}" != "/" ]]; then
   case $1 in
    *.avi) local res=$dir_avi/$1; if [[ -e "$res" ]]; then echo "$res"; fi; ;;
    *.ico) local res=$dir_ico/$1; if [[ -e "$res" ]]; then echo "$res"; fi; ;;
    *.png) local res=$dir_png/$1; if [[ -e "$res" ]]; then echo "$res"; fi; ;;
    *.wav) local res=$dir_wav/$1; if [[ -e "$res" ]]; then echo "$res"; fi; ;;
    [a-z_A-Z\-\.][a-z_A-Z\-\.]*) echo "$res"; ;; # looks like stock resource
    *) ;;
   esac;
  else
   echo "$res";
  fi;
 fi;
};

#####################################################################################################################################
# Get notifier server Capabilities:
# 1) qdbus org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.Notifications.GetCapabilities
#  > body body-markup body-hyperlinks body-images actions icon-static image/svg+xml private-synchronous append
# 2) busctl --user call org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.Notifications GetCapabilities
#  > as 9 "body" "body-markup" "body-hyperlinks" "body-images" "actions" "icon-static" "image/svg+xml" "private-synchronous" "append"
#####################################################################################################################################
function qdbus_get_caps(){
 qdbus $1 org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.Notifications.GetCapabilities;
};
function busctl_get_caps(){
 busctl $1 call org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.Notifications GetCapabilities;
};
function parse_busctl_out(){ shift; shift; echo $*; };

#############################################
# Print list of notifier server capabilities.
#############################################
function list_server_capabilities(){
 local ans="";
 if which qdbus >/dev/null 2>&1; then
  for bus in user system; do
   ans="$(qdbus_get_caps --$bus | xargs)";
   if [[ -n "$ans" ]]; then echo "$ans"; return 0; fi;
  done;
 fi;
 if which busctl >/dev/null 2>&1; then
  for bus in user system; do
   ans="$(busctl_get_caps --$bus | xargs)";
   if [[ -n "$ans" ]]; then echo "$(parse_busctl_out $ans)"; return 0; fi;
  done;
 fi;
 return 1;
};

#############################
# Detect server capabilities.
#############################
function detecting_caps(){
 server_caps="$(list_server_capabilities | xargs)";
 actual_caps="$server_caps";
};

############################################
# Check if server has support of feature $1.
############################################
function has_support(){
 for item in $actual_caps; do
  if [[ "$1" = "$item" ]]; then
   return 0;
  fi;
 done;
 return 1;
};
function has_support_any(){
 for arg in $*; do
  if has_support "$arg"; then
   return 0;
  fi;
 done;
 return 1;
};
function has_support_all(){
 local found=0;
 for arg in $*; do
  if has_support "$arg"; then
   let "found++";
  else
   return 1;
  fi;
 done;
 if [[ $found -gt 0 ]]; then return 0; fi;
 return 1;
};

##############################################
# check if any notification daemon is running.
##############################################
function is_notifier_daemon_running(){
 local s="";
 if [[ -n "$server_caps" ]]; then
  return 0; # server detected
 fi;
 for exe in qdbus busctl; do
  if which $exe >/dev/null 2>&1; then
   for bus in user system; do
    s="$($exe --$bus | grep 'org.freedesktop.Notifications')";
    if [[ -n "$s" ]]; then return 0; fi;
   done;
   return 1;
  fi;
 done;
 return 1;
};

####################################
# Print notifier server process PID.
####################################
function print_notifier_pid(){
 local pid="";
 for bus in user system; do
  # the method 'busctl status' much faster then 'busctl list'
  #pid="$(busctl --$bus list | grep 'org.freedesktop.Notifications' | awk '{print $2}')";
  pid="$(busctl --$bus status 'org.freedesktop.Notifications' 2>/dev/null | grep '^PID=')";
  if [[ -n "$pid" ]]; then echo "${pid/PID=/}"; break; fi;
 done;
};

###################################################
# Print notifier server process status information.
# Example: print_notifier_ps pid comm command;
###################################################
function print_notifier_ps(){
 local id="pid command";
 if [[ -n "$1" ]]; then id="$*"; fi;
 local pid="$(print_notifier_pid)";
 if [[ -n "$pid" ]] && is_number $pid; then
  ps --pid $pid --format "$id" --no-headers | sed 's/^\s*//';
 fi;
};

###############################
# print notifier server status.
###############################
function print_server_status(){
 if is_notifier_daemon_running; then
  local addon="${actual_caps/$server_caps/}";
  if [[ -n "$addon" ]]; then addon=" + $(echo "$addon" | xargs)"; fi;
  echo "notifier-process-info: $(print_notifier_ps pid command)";
  echo "notifier-capabilities: $server_caps$addon";
  exit 0;
 else
  fatal 1 "Server of Notifications is NOT running.";
 fi;
};

############################################
# kill (terminate) notifier serrver process.
############################################
function kill_server(){
 local pid="$(print_notifier_pid)";
 if [[ -n "$pid" ]]; then
  if kill $pid; then
   echo "PID $pid stopped";
  else
   echo_to_stderr "Could not stop PID $pid";
  fi;
 fi;
};

#################################################
# extract executable base name from command line.
#################################################
function exctaract_exe(){
 local exe="$(basename "$1")";
 if [[ -n "$exe" ]]; then echo "$exe"; fi;
};

###################################################
# calculate actual capabilities, for known servers.
# it's server_caps plus special (fake) caps depends
# on current (running) server.
# summary-markup - notifier can use summary markup.
###################################################
function do_actual_caps(){
 if [[ -n "$actual_caps" ]]; then
  local cmd="$(print_notifier_ps command)";
  local exe="$(exctaract_exe $cmd)";
  case $exe in
   qtnotifydaemon) actual_caps="$actual_caps summary-markup"; ;;
   dunst)          action_caps="$actual_caps dunst"; ;;
   *) ;;
  esac;
 fi;
};

readonly xdg_data_dir="/usr/share";
readonly dbus_services_dir="$xdg_data_dir/dbus-1/services";

#############################################
# print list of notifiers registered in DBUS.
#############################################
function list_dbus_notifiers(){
 for fn in $(ls -1 $dbus_services_dir); do
  local file="$dbus_services_dir/$fn";
  local name="$(cat $file | grep 'Name=org.freedesktop.Notifications')";
  if [[ -n "$name" ]]; then
   local exec="$(cat $file | grep 'Exec=' | cut -d '=' -f 2)";
   if [[ -n "$exec" ]]; then echo "$exec"; fi;
  fi;
 done;
};

############################################################
# start DBUS event monitor on org.freedesktop.Notifications.
############################################################
function start_dbus_monitor(){
 local where="";
 local monitor="";
 case $1 in
  2) monitor="busctl"; where="--user"; ;;
  *) monitor="dbus-monitor"; where="--session"; ;;
 esac;
 if [[ "$where" = "s" ]]; then where="--system"; fi;
 case $monitor in
  busctl) if which busctl >/dev/null 2>&1; then busctl --user --match="type='signal',interface='org.freedesktop.Notifications'" monitor; exit $?; fi; ;;
  dbus-monitor) if which dbus-monitor >/dev/null 2>&1; then dbus-monitor  --monitor "type=signal,interface=org.freedesktop.Notifications"; exit $?; fi; ;;
  *) fatal 1 "Error: invalid monitor $monitor"; ;;
 esac;
};

######################
# Command line parser.
######################
function parse_args(){
 if [[ -z "$*" ]]; then
  fatal 1 "$scriptname: no arguments. Call $scriptname --help";
 fi;
 while [[ -n "$1" ]]; do
  case $(lower_case $1) in
   preset)                  shift; make_preset  "$1"; ;;
   verbose|verb)            shift; assign_verb  "$1"; ;;
   summary|brief)           shift; assign_head  "$1"; ;;
   head|title|caption)      shift; assign_head  "$1"; ;;
   text|body)               shift; assign_body  "$1"; ;;
   font)                    shift; assign_font  "$1"; ;;
   bold)                    shift; assign_bold  "$1"; ;;
   fontsize|size)           shift; assign_size  "$1"; ;;
   ico|icon)                shift; assign_ico   "$1"; ;;
   avi)                     shift; assign_avi   "$1"; ;;
   wav|audio)               shift; assign_wav   "$1"; ;;
   size|fontsize)           shift; assign_size  "$1"; ;;
   delay|timeout)           shift; assign_delay "$1"; ;;
   bkcolor|bgcolor)         shift; assign_bcol  "$1"; ;;
   textcolor|fgcolor)       shift; assign_fcol  "$1"; ;;
   sure|createifnotvisible) shift; assign_sure  "$1"; ;;
   trans)                   shift; assign_trans "$1"; ;;
   xml)                     shift; assign_xml   "$1"; ;;
   run)                     shift; assign_run   "$1"; ;;
   button)                  shift; set_button   "$1"; ;;
   untilclickany)           shift; assign_clany "$1"; ;;
   guid)                    shift; assign_guid  "$1"; ;;
   btn0)                    shift; assign_btn0  "$1"; ;;
   btn1)                    shift; assign_btn1  "$1"; ;;
   btn2)                    shift; assign_btn2  "$1"; ;;
   btn3)                    shift; assign_btn3  "$1"; ;;
   btn4)                    shift; assign_btn4  "$1"; ;;
   btn5)                    shift; assign_btn5  "$1"; ;;
   btn6)                    shift; assign_btn6  "$1"; ;;
   btn7)                    shift; assign_btn7  "$1"; ;;
   btn8)                    shift; assign_btn8  "$1"; ;;
   btn9)                    shift; assign_btn9  "$1"; ;;
   cmd0|onclick)            shift; assign_cmd0  "$1"; ;;
   cmd1)                    shift; assign_cmd1  "$1"; ;;
   cmd2)                    shift; assign_cmd2  "$1"; ;;
   cmd3)                    shift; assign_cmd3  "$1"; ;;
   cmd4)                    shift; assign_cmd4  "$1"; ;;
   cmd5)                    shift; assign_cmd5  "$1"; ;;
   cmd6)                    shift; assign_cmd6  "$1"; ;;
   cmd7)                    shift; assign_cmd7  "$1"; ;;
   cmd8)                    shift; assign_cmd8  "$1"; ;;
   cmd9)                    shift; assign_cmd9  "$1"; ;;
   hbsep)                   shift; assign_hbsep "$1"; ;;
   progress)                shift; set_progress "$1"; ;;
   nodouble)                shift; set_nodouble "$1"; ;;
   delete)                  shift; set_delete   "$1"; ;;
   -test|--test)            testmode=1; ;;
   -demo|--demo)            show_demo; exit 0; ;;
   -kill|--kill)            kill_server; exit $?; ;;
   -pid|--pid)              print_notifier_pid; exit $?; ;;
   -status|--status)        print_server_status; exit $?; ;;
   -h|-help|--help)         print_help; exit 0; ;;
   --version)               print_version; exit 0; ;;
   -list-avail|--list-avail) list_dbus_notifiers; exit 0; ;;
   -monitor|--monitor)      start_dbus_monitor 1 u; exit 0; ;;
   -monitor1|--monitor1)    start_dbus_monitor 1 u; exit 0; ;;
   -monitor2|--monitor2)    start_dbus_monitor 2 u; exit 0; ;;
   -monitor1s|--monitor1s)  start_dbus_monitor 1 s; exit 0; ;;
   -monitor2s|--monitor2s)  start_dbus_monitor 2 s; exit 0; ;;
   -ini|--ini)              shift; handle_opt_ini $1; exit 0; ;;
   -*)                      fatal 1 "$scriptname: invalid option: $1"; ;;
   *)                       fatal 1 "$scriptname: invalid clause: $1"; ;;
  esac;
  shift;
 done;
};

#######################
# check $1 is web color
# format: #rrggbb
#######################
function is_web_color(){
 local cc="$1";
 if [[ ${#cc} -eq 7 ]] && [[ "${cc:0:1}" = "#" ]]; then return 0; else return 1; fi;
};

#############################
# convert color to web format
#############################
function color_to_web(){
 local cc="$1";
 if [[ "${cc:0:1}" != "#" ]]; then
  case $cc in
   0x*)        if [[ $hex_rgb -eq 0 ]]; then cc="#${cc:6:2}${cc:4:2}${cc:2:2}"; else cc="#${cc:2:6}"; fi; ;;
   [a-z0-9_]*) cc="$(colorcode -web $cc | xargs)"; ;;
   *) ;;
  esac;
 fi;
 echo "$cc";
};

###################
# play wav sound $1
###################
function play_sound(){
 if [[ -n "$1" ]] && [[ -e "$1" ]] && [[ $sound_on -ne 0 ]]; then
  grun play -q "$wav";
 fi;
};

###############################
# Print params on Verbose mode.
###############################
function print_params(){
 if [[ -n "$1" ]] && [[ $1 -gt 0 ]]; then
  local vars="body head verb preset delay font size bold bcol fcol";
  vars="$vars xml avi ico wav trans progress guid delete createifnotvisible nodouble run button";
  vars="$vars btn0 cmd0 btn1 cmd1 btn2 cmd2 btn3 cmd3 btn4 cmd4 btn5 cmd5 btn6 cmd6 btn7 cmd7 btn8 cmd8 btn9 cmd9";
  for id in $vars; do printf "%-20s \"%s\"\n" "$id:" "${!id}"; done;
  printf "%-20s " "cmdline:"; echo "${cmdline[@]}";
 fi;
};

###############################
# Copy button/run to btn0/cmd0.
###############################
function check_run_button(){
 if [[ -z "$cmd0" ]] && [[ -n "$run" ]]; then assign_cmd0 "$run"; fi;
 if [[ -z "$btn0" ]] && [[ -n "$button" ]]; then assign_btn0 "$button"; fi;
};

####################################
# Append arguments to cmdline array.
####################################
function append_cmdline(){
 #cmdline=("${cmdline[@]}" "$@");
 cmdline+=("$@");
};

################################
# Append button/command to body.
################################
function append_num_btn_cmd(){
 local num="$1"; local btn="$2"; local cmd="$3";
 if [[ -n "$num" ]] && [[ -n "$btn" ]] && [[ -n "$cmd" ]]; then
  local ref="$(href_by_action "$btn" "$cmd")";
  if [[ $num -eq 0 ]]; then
   if [[ -z "$head" ]]; then head="$btn"; fi;
   head="<a href=\"$ref\">$head</a>";
  else
   body="$body <a href=\"$ref\">$btn</a>";
  fi;
 fi;
};

###########################################################
# Compose target cmdline by income command line parameters.
###########################################################
function compose_cmdline(){
 cmdline=(notify-send);
 ###############
 # apply urgency
 ###############
 case $urgency in
  0) append_cmdline --urgency=low; ;;
  1) append_cmdline --urgency=normal; ;;
  2) append_cmdline --urgency=critical; ;;
  *) ;;
 esac;
 #############
 # apply delay
 #############
 append_cmdline --expire-time=$delay;
 #####################
 # apply ico, wav, avi
 #####################
 ico="$(find_res $ico)";
 if [[ -n "$ico" ]]; then
  if has_support_any icon-static icon-multi icon-name x-dunst-stack-tag; then
   append_cmdline --icon=$ico;
  fi;
 fi;
 wav="$(find_res $wav)";
 if [[ -n "$wav" ]]; then
  setup_sound_mode;
  if [[ $sound_on -ne 0 ]]; then
   if has_support_any sound sound-file sound-name; then
    append_cmdline --hint=string:sound-file:$wav;
   else
    play_sound "$wav";
   fi;
  fi;
 fi;
 ########################
 # apply colors for dunst
 ########################
 if has_support_any dunst x-dunst-stack-tag; then
  local bc="$(color_to_web "$bcol")"; local fc="$(color_to_web "$fcol")";
  if is_web_color "$bc"; then append_cmdline "--hint=string:bgcolor:$bc"; fi;
  if is_web_color "$fc"; then append_cmdline "--hint=string:fgcolor:$fc"; fi;
 fi;
 ##########################
 # apply guid (synchronous)
 ##########################
 if has_support_any x-dunst-stack-tag x-canonical-private-synchronous; then
  if [[ -n "$guid" ]]; then
   local hid="x-canonical-private-synchronous";
   if has_support_any x-dunst-stack-tag; then hid="x-dunst-stack-tag"; fi;
   local gm5="$(echo -n "$guid" | md5sum | cut -d' ' -f1)";
   append_cmdline "--hint=string:$hid:guid_$gm5";
  fi;
 fi;
 ############################
 # apply xml if one not empty
 ############################
 if [[ -n "$xml" ]]; then
  if [[ -z "$body" ]]; then
   body="$xml";
  else
   body="$body $xml";
  fi;
 fi;
 ############################
 # validate head if one empty
 ############################
 if [[ -z "$head" ]] && [[ -n "$hbsep" ]] && [[ -n "$body" ]]; then
  local rest="${body#*$hbsep}";
  if [[ -n "$rest" ]]; then
   local rlen="${#rest}";
   local blen="${#body}";
   local slen="${#hbsep}";
   local p=0; let "p=$blen-$slen-$rlen+1";
   if [[ $p -gt 0 ]]; then
    local hlen=0; let "hlen=$blen-$rlen";
     head="${body:0:$hlen}"; body="$rest";
     head="$(echo "$head" | sed -e 's/^\s*//' -e 's/\s*$//')";
   fi;
  fi;
 fi;
 ###############
 # apply buttons
 ###############
 check_run_button;
 if has_support_all body-markup body-hyperlinks; then
  append_num_btn_cmd 0 "$btn0" "$cmd0";
  append_num_btn_cmd 1 "$btn1" "$cmd1";
  append_num_btn_cmd 2 "$btn2" "$cmd2";
  append_num_btn_cmd 3 "$btn3" "$cmd3";
  append_num_btn_cmd 4 "$btn4" "$cmd4";
  append_num_btn_cmd 5 "$btn5" "$cmd5";
  append_num_btn_cmd 6 "$btn6" "$cmd6";
  append_num_btn_cmd 7 "$btn7" "$cmd7";
  append_num_btn_cmd 8 "$btn8" "$cmd8";
  append_num_btn_cmd 9 "$btn9" "$cmd9";
 fi;
 ############################
 # validate head if one empty
 ############################
 local date="$(date +%Y.%m.%d-%H:%M:%S)";
 if [[ -z "$head" ]]; then head="$date: $(preset_head $preset)"; fi;
 ########################
 # apply font, size, bold
 ########################
 if has_support body-markup; then
  if [[ -n "$font" ]]; then body="<font color='${fcol//0x/#}' face='$font'>$body</font>"; fi;
  if [[ $size -lt 10 ]]; then body="<small>$body</small>"; fi;
  if [[ $size -gt 12 ]]; then body="<big>$body</big>"; fi;
  if bold_body; then body="<b>$body</b>"; fi;
 fi;
 if has_support summary-markup; then
  #if [[ -n "$font" ]]; then head="<font color='${fcol//0x/#}' face='$font'>$head</font>"; fi;
  if [[ $size -lt 10 ]]; then head="<small>$head</small>"; fi;
  if [[ $size -gt 12 ]]; then head="<big>$head</big>"; fi;
  if bold_head; then head="<b>$head</b>"; fi;
 fi;
 ##################
 # apply head, body
 ##################
 append_cmdline "$head" "$body";
};

#########################################
# Action  on delete notification by GUID.
#########################################
function do_delete(){
 if [[ -n "$1" ]]; then
  true;
 fi;
};

####################
# Check dependecies.
####################
function check_depends(){
 local checklist="$*";
 for item in $checklist; do
  if which $item >/dev/null 2>&1; then
   if [[ $verb -gt 0 ]]; then
    echo "Found $item: $($item --version)";
   fi;
  else
   fatal 1 "Error: could not find $item";
  fi;
 done;
};

########################
# Actions to do on exit.
########################
function do_on_exit(){
 true;
};

##########################
# Cleanup actions on exit.
##########################
function cleanup_on_exit() {
 local rv=$?;
 do_on_exit;
 exit $rv;
}

function init_instance(){
 trap "cleanup_on_exit" EXIT;
};

################
# Main procedure
################
function main(){
 log_event;
 init_instance;
 log_event "$@";
 detecting_caps;
 do_actual_caps;
 preset_default;
 parse_args "$@";
 check_depends notify-send;
 compose_cmdline; print_params $verb;
 if [[ -n "$delete" ]]; then
  do_delete $delete;
  return $?;
 fi;
 log_event "${cmdline[@]}";
 if [[ $verb -gt 0 ]]; then print_quoted_strings "${cmdline[@]}"; fi;
 if [[ $testmode = 1 ]]; then print_quoted_strings "${cmdline[@]}"; return; fi;
 exec "${cmdline[@]}";
};

main "$@";

##############
## END OF FILE
##############
