#!/bin/sh
###########################################################
#
#	NameSpace Independent Shell Functions
#
# This is main supplementary functions file
# it is not intended to be called directly
# see functions digest below
#
# Any global variable or function name defined here MUST start with "_"
# to avoid namespace clash (all user-defined names must NOT start with "_")
# Names started with two _'s are internals and should not be used outside
#
#==========================================================

## TODO support for sh -efu running

# Variables:

export _Me=`basename "$0"`			# script name
export _GMe="${_GMe:-$_Me}"			# global script name (for set of scripts)
export _FMe=`realpath "$0"`			# script fullpath
export _MeDir=`dirname "$_FMe"`			# script directory
export _MePID="$$"				# PID of current shell
export _Home="${_Home:-$HOME}"			# base directory
export _AMe=`echo "$_Me" | tr -cd "[:alnum:]"`	# script name alphanumeric part (for generating filenames)
__RC_SUFFIX="${__RC_SUFFIX:-rc}"		# config file suffix
export _RC="$_Home/.$_GMe$__RC_SUFFIX"		# config file
export _SYSRC="/etc/$_GMe$__RC_SUFFIX"		# system-wide config file
export _RCDIR="$_Home/.config/$_GMe"		# new style config storage
export _RCFILE="$_RCDIR/__init__.$__RC_SUFFIX"	# new style config file
export _RC_HOST="$_RCDIR/`hostname`.$__RC_SUFFIX"	# host specific config file
export _Verb=0					# verbosity level
export _Options=""				# getopts options (generated!)
export _Usage=""				# short help string
export _Help=""					# long help string
export _TmpFiles=""				# temporary files
export _TmpDirs=""				# temporary directories
export _LC_COLLATE=C				# LC_COLLATE submissions
export __Version=2.04				# this functions file version
export _Version=0.0				# software version
__CR="
"						# see functions.ascii for more
__IsXFlag=""					# used while temprarily turnin sh -x off
__Unsafe=""					# set this to non-empty to enable unsafe actions
						#(like non-/tmp/-directory deleting etc.)
export _OS="`uname -o`"				# OS identifier

# LC_COLLATE wrappers (e. g. for [a-z] regexps work properly etc.)
# sed grep sort type
sed() { env LC_COLLATE="$_LC_COLLATE" sed "$@"; }
grep() { env LC_COLLATE="$_LC_COLLATE" grep "$@"; }
sort() { env LC_COLLATE="$_LC_COLLATE" sort "$@"; }

## GNU or BSD sed? (NOTE: -r key is added to BSD sed already)
sed --version 2>/dev/null 1>/dev/null &&
_Esed() { sed -r "$@"; } || _Esed() { sed -E "$@"; }
sed --version 2>/dev/null 1>/dev/null &&
_EsedNL="\n" || _EsedNL="\
"

#==========================================================
# Message displaying and error system

## Internal: display a message
##	One may redefine this (e.g. with XDialog) for another displaying method
__Message() { # message [title]
  { test -n "$2" && echo "$2: $1" || echo "$1"; } >&2
}

# Display an error to stderr
#   Errorlevels and exit:
#   0		- INFO
#   1..255	- ERROR (exit)
#   Negatives are treated as bit scales
#   01		- WARNING
#   10		- ERROR
#   11		- no message type
#   0100	- exit status 0/1
#   1000	- always exit
_Error() { # errormsg [errorlevel]
  local ER ret ex
  case "${2:-1}" in
    [1-9]*) ER="ERROR"; ret="${2:-1}"; ex=1 ;;
    0) ER="INFO"; ret=0; ex=0 ;;
    q) ER=""; ret=0; ex=0 ;;
    -*) ex="${2#-}"
      case "$(($ex&3))" in
	0) ER=INFO;;
        1) ER=WARNING;;
        2) ER=ERROR;;
        3) ER="";;
      esac
      ret="$((($ex&4)==4))"
      ex="$((($ex&8)==8))"
  esac
  __Message "$1" "$ER"
  test "$ex" = "1" && exit "$ret" || :
}

# Display a message to stderr
_Msg() { # [message]
  _Error "${*:-<STUB>}" -3
}

# Display warning message if _Verb>=0
_Warning() { # message
  test $_Verb -ge 0 && _Error "$*" -1 || :
}

# Display a message to stderr and exit
_Die() { # [message]
  case "$*" in
    "") test $_Verb -gt 0 && _Error "Exit" -8 || exit 0;;
    *) _Error "$*" -11 ;;
  esac
}

# Display message in verbose mode only
_Debug() { # [[verbosity] message]
  local _Debug_v
  case "$#$1" in
    2[0-9]) _Debug_v="$1"; shift;;
    *) _Debug_v=0;;
  esac
  if [ $_Verb -gt "$_Debug_v" ]; then _Msg "$@"; fi
}

#==========================================================
# Temporary files and exit handler

# Remove all temporary files and directory
_clean_tmp() {
  local F
  test -z "$_TmpFiles" || echo "$_TmpFiles" | while read F
  do test -z "$F" || rm -f "$F"; done

  test -z "$_TmpDirs" || echo "$_TmpDirs" | while read F 
  do
    case "$F" in
      */tmp/?*) # it seems safe to delete this
      		rm -rf "$F";;
      "") ;;
      *) _Warning "It seems unsafe to delete '$F' directory" ;;
    esac
  done
  _TmpDirs=""; _TmpFiles=""
}

__cleanup_handler() { # [exit status]
  trap - EXIT
  _clean_tmp
  exit "$1"
}

# Perform actions on exit
_exit_handler() {
  __cleanup_handler "$?"
}

# Perform actions on deadly signal and exit 
_signal_handler() {
  __cleanup_handler 143	# SIGTERM+128
}

trap _exit_handler EXIT
trap _signal_handler HUP INT QUIT PIPE TERM

# Create a temporary file and store its name to _TmpFiles
# second parameter means type: [-]d* -- directory, [-]p* -- pipe, other -- file
# (type is also used as "prefix" if later is not set)
# assign filename to "variable"
_TmpFile() { # variable [type [prefix [suffix]]]  
  local _Type="$2" _Suff="$4" _F _P _Z _N="file" _X="XXXXXXXXXXXX"
  case "$1" in
    *[!_a-zA-Z0-9]*) _Error "Cannot store any value to '$1' variable, please use alphanumeric name" ;;
    "") _Error "No variable name provided to _TmpFile function" ;;
  esac
  case "$_Type" in
    .*) _Suff="$_Type"; _Type="";;
    *p|p*) _P=""; _N="pipe";;
    *d|d*) _P="-d"; _N="directory";;
  esac
  test -z "$3" || _N="${3##*/}"
  _N="$_N.$_X$_Suff"
  _F=`mktemp -t $_P "$_AMe.$_N"` || _Error "Cannot create '$_F' temporary $_N"
  case "$_Type" in
    *p|p*) # XXX look for race conditions here
        _Z="`mktemp XXXXXXXXXXXX`"
    	mv "$_F" "$_Z"
	mkfifo "$_F"
	rm -f "$_Z"
	;;
  esac
  case "$_Type" in 
    *d|d*) _TmpDirs="$_TmpDirs
$_F" ;;
    *) _TmpFiles="$_TmpFiles
$_F" ;;
  esac
  eval "export $1='$_F'"
}

# Create a temporary directory and store its' name to _TmpDirs
# assign filename to "variable"
_TmpDir() { # variable
  _TmpFile "$1" directory
}

#==========================================================
# Help system
## TODO long options

# WARNING: this function strongly depends on "case" operator programming style:
#	key) # help string
# if help string contains capitalized word, it will be used as parameter name
# E. g.:
#	o) # redirect output to FILE
# will produce this help string:
#	-o FILE		redirect output to FILE	
# By default, help strings are searched
# from the first occurance of " getopt[s] " string to the first " esac$" pattern
# so keep a commentary after additional "esac" if it emerges inside
#
# Generted varsiables:
# _Options -- option string in getopts format

# Store a help information
_MakeHelp() { # description [tail_text [additional_help [start_pattern end_pattern]]]
  local descr
  local selfname
  case "$1" in
    *--*) selfname="${1%% -- *}"; descr="${1##* -- }" ;;
    ?*) selfname="$_Me"; descr="$1" ;;
    "") _Error "Insufficient parameters in _MakeHelp" ;;
  esac
  local st="${4:-" getopts? .*_Options"}"
  local en="${5:-"[	 ]esac[ 	]*$"}"
  local sedfilter="/$st/,/$en/"
  local sedFpattern='^[ 	]*([^)]*)\)[ 	]*#[ 	]*(.*)'
  local sedUpattern='^[ 	]*([^)]*)\)[ 	]*#[ 	]*n![ 	]*(.*)'
  local sedPpattern='^[ 	]*([^)]*)\)[ 	]*#[ 	]*([^!]*[^!A-Z]([A-Z]{3,}).*)'
  local genhelp="$(_Esed -n "$sedfilter{
  /$sedPpattern/s/$sedPpattern/ -\1 <\3>	\2/p
  s/([ 	])!([A-Z])/\1\2/g
  /$sedUpattern/s/$sedUpattern/ -\1		(TODO) \2/p
  /$sedFpattern/s/$sedFpattern/ -\1		\2/p
  }" "$_FMe")"
  _Options=$(_Esed -n "$sedfilter{
  /$sedPpattern/s/$sedPpattern/\1:/p
  /$sedUpattern/n
  /$sedFpattern/s/$sedFpattern/\1/p
  }" "$_FMe" | sort | tr -d '\n')
  _Usage="$selfname v$_Version -- $descr
Usage: $_Me [-$_Options]${2:+" "}$2"
  _Help="
$genhelp${3:+$__CR}$3"
}

# Print error message and usage or usage and help (if no optarg), then exit
_ExitHelp() { # opt optarg [ret]
  local msg
  local ret="${3:-1}"
  case "$1" in
    "?") msg="Unknown option";;
    ":") msg="Option needs an argument";;
    *) _Msg "$_Usage$_Help"; exit 0;;
  esac
 _Error "$msg: -$2
 $_Usage
use -h key for long help
" $ret
}

# Grep digest from NISH file
_Digest() { # [Name_pattern [filename]]
  #egrep "^(#([^#!]|\$|##)|(export +)?_[^_]$1[_a-zA-Z0-9-]+(\(\)|.*# ))" "${2:-$0}" |
  local name="${1:-[^_]}"
  _Esed -n 's/^#[ 	*]*$/#/p
  /^#([^!#]|.#)/p
  s/^(export +)?(_('"$name"')[_a-zA-Z0-9]+)=[^=]*#[ 	]*(.*)/\2	# \4/p
  s/^(_('"$name"')[_a-zA-Z0-9]+)[ 	]*\(\)[ 	]*\{([	 ]*#[ 	]*(.*))?/\1() \4\
/p
' "${2:-0}"
}

#==========================================================
# Date/time arithmetic

## Convert date(1) format date to seconds from 01.01.1970
__DateSec() { # date
  LC_ALL=C date -d "$1" "+%s"
}

# Append Shift to Date
#  e. g.: _DateAdd "12 feb 2010 10:12 pm" "2 days 1 hour"
_DateAdd() { # Date Shift
  local d1="`__DateSec "$1"`"
  local d0="`__DateSec "+0 sec"`"
  local d2="`__DateSec "$2"`"
  case "-$3" in
    -) LC_ALL=C date -d "@$(($d1+$d2-$d0))" "+%d  %m %Y" ;;
    -+*) date -d "@$(($d1+$d2-$d0))" "$3" ;;
    -[Ee][Jj]*) date -d "@$(($d1+$d2-$d0))" "+%Y/%m/%d %H:%M:00" ;;
  esac
}

#==========================================================
# Configuration file editing

# Set "Variable=Value" inside Context of File or append it at the start of the Context
_CfgEqSet() { # File Variable Value [Context=0,$]
  local Context="${4:-1,\$}"
  local N=$(sed -n "$Context{/^[ 	]*$2[ 	]*=/=}" "$1")
  local n
  # if Variable=Value exists
  test -n "$N" &&
  for n in $N
  do # Replace
    sed -i "$n""s/^\([ 	]*$2[ 	]*=[ 	]*\).*/\1$(_Quote "$3")/" "$1"
  done || { # Append
    N=$(sed -n "$Context{=;q}" "$1")
    test -n "$N" &&
    sed -i "$N""a\
    $2=$3
    " "$1" ||
    _Warning "No '$Context' context found in $1"
  }
}

__CfGet() { # File Context Variable Div Value Tail
  test -z "$(sed -n "$2{=}" "$1")" &&
  _Warning "No '$2' context found in $1" || {
  local ret=$(sed -n "$2{/$3$4$5$6/{s/$3$4\($5\)$6/\1/p;q}}" "$1")
  echo -n $ret
  test -n "$ret"
  }
}

# Print Value from "Variable=Value" inside Context of File
_CfgEqGet() { # File Variable [Context=1,$]
  __CfGet "$1" "${3:-1,\$}" "^[ 	]*$2" "[ 	]*=[ 	]*" ".*" ""
}

# Print Value from "Variable Value" inside Context of File
_CfgBlGet() { # File Variable [Context=1,$]
  __CfGet "$1" "${3:-1,\$}" "^[ 	]*$2" "  *" ".*" ""
}

# Delete all "Variable=" entries inside Context of File
_CfgEqDel() { # File Variable [Context=1,$]
  local Context="${3:-1,\$}"
  test -z "$(sed -n "$Context{=}" "$1")" &&
  _Warning "No '$Context' context found in $1" ||
  sed -i "$Context{/^[ 	]*$2[ 	]*=/d}" "$1"
}

# Comment all "Variable=" entries inside Context of File
_CfgEqComm() { # File Variable [Context=1,$]
  local Context="${3:-1,\$}"
  test -z "$(sed -n "$Context{=}" "$1")" &&
  _Warning "No '$Context' context found in $1" ||
  sed -i "$Context{s/^\([ 	]*$2[ 	]*=.*\)/#\1/}" "$1"
}

# Uncomment all "#Variable=" entries inside Context of File
_CfgEqUnComm() { # File Variable [Context=1,$]
  local Context="${3:-1,\$}"
  test -z "$(sed -n "$Context{=}" "$1")" &&
  _Warning "No '$Context' context found in $1" ||
  sed -i "$Context{s/^[ 	]*#\([ 	]*$2[ 	]*=.*\)/\1/}" "$1"
}

#==========================================================
# Parallel execution functions
 
# To run a number of tasks in parallel:
# first call _II_init <max_number_of_tasks>
# next call _II_run <task> [<parameters...>] for each task
# and last call _II_wait to wait for last task to be done

# Detect number of cores
_II_cores() { #
  #N="`ls -d /proc/sys/kernel/sched_domain/cpu* 2>/dev/null | wc -l`" ||
  N=`grep vendor_id /proc/cpuinfo 2>/dev/null | wc -l`
  test $N != 0 ||
  N="`sysctl -n kern.smp.cpus 2>/dev/null`"
  test -z "$N" && _Msg "Warning: cannot determine number of CPUs"
  echo "$(($N+0))"
}

# Initialize parallel environment for running maxproc tasks at once
_II_init() {	# [maxproc]
  _TmpFile __II_count		# number of processes
  _TmpFile __II_sync pipe	# pipe for end-of-proc catching
  __II_maxproc="${1:-$(_II_cores)}"	# parallel slots number
  __II_current=0		# tasks already executed
}

__II_done() { wc -l < $__II_count; }

__II_exit() {
  trap - EXIT
  echo $__II_err >> $__II_count
  echo $__II_err > $__II_sync
  exit $__II_err
}

__II_next() {
  __II_err=0
  trap __II_exit EXIT
  "$@"
  __II_err="$?"
}

__II_pause() { { read __II_err < $__II_sync; } 2>/dev/null || :; }

# Wait for empty slot becomes available, then execute a task
_II_run() { # task [args ...]
  test $(($__II_current - $(__II_done) )) -lt $__II_maxproc || __II_pause
  (__II_next "$@") &
  __II_current=$(($__II_current + 1))
}

# Wait for all tasks to be done
_II_wait() {
  while test $(($__II_current - $(__II_done) )) -ne 0
  do __II_pause; done
}

#==========================================================
# Misc functions

## TODO: "A_b_c" "D_e" -> "A b c" "D e" call

# Print N-th string character (or N-th substring of cell_size length)
_Left() { # String N [cell_size]
  # TODO [cell_size]
  if [ "$2" = "0" ]; then expr "$1" : "\(.\).*"
  else  expr "$1" : ".\{$2\}\(.\).*"
  fi
}

# Print newest/-oldest of version numbers given (via sort -V)
_Newer() { # [-] VersionNumber1 [VersionNumber2 ...]
  local keys="-V"
  test "$1" != "-" || { keys="-V -r"; shift; }
  echo "$@" | tr " " "\n" | sort $keys | tail -1
}

# Print pseudo-random 0..Max (32767 if Max undefined)
_Random() { # [Max]
  echo $((`dd if=/dev/urandom count=2 bs=1 2>/dev/null | od -An -t u4`%${1:-32768}))
}

# Parse "ipcalc Parameters" and get Field value
_IpCalc() { # Parameters Field
  local ipc="ipcalc"	# dependency avoidance
  $ipc $1 | sed -n "/$2/s/$2[ 	][ 	]*\([^ 	/]*\).*/\1/p"
}

# Print string that is *not* in Text (first look for Defauilt string)
_NotIn() { # Text [Default=@@@]
  local Default=${2:-@@@}
  while echo "$1" | grep -l "$Default"; do Default="@$Default"; done
  echo "$Default"
}

# Print "prefix""N*str""suffix", if any
_NStr() { # N str=" " [prefix [suffix]]
  case "$1" in
    [1-9]*) printf "$3%$1s$4" '' | sed "s ${2:- }g";;
    *) ;;
  esac
}

# 

# Decode utf-8 output if not utf-8 locale is used
_DeU8() { # [locale]
  local l="${1:-$LANG}"
  case "$l" in
    *[uU][Tt][fF]*8*) cat ;;
    *) iconv -f utf-8 -t "${l##*.}" -r. ;;
  esac
}

# Remove arg from argument_string with fields separated by "separator"
_DelArg() { # argument_string arg [separator=' ']
  local sep="${3:- }"
  local a="$sep$1$sep"
  a="${a%$sep$2$sep*}$sep${a#*$sep$2$sep}"
  a="${a%$sep}"; a="${a#$sep}"
  echo "$a"
}

# Translate string of format "3-5,12,15" to "3 4 5 12 15"
# and print it using printf formatstring if specified (default is %d)
_ExpandEnum() { # [formatstring] number_list [divider [diapazone_divider]]
  case "$1" in
    %*) fmt="$1\n"; shift;;
    *)  fmt="%d\n";;
  esac
  local lst="$1"
  local div="${2:-,}"
  local dia="${3:--}"
  local d n
  for d in $(echo "$lst" | tr "$div" " "); do
    case "$d" in
      *-*) for n in $(seq $(echo "$d" | tr "-" " ") ); do printf "$fmt" "$n"; done ;;
      *) printf "$fmt" "$d" ;;
    esac
  done
}

# Quote Chars_regexp in Str (e. g. /home/dir -> \/home\/dir)
_Quote() { # Str [Chars_regexp=."[/]"]
  # Thanks Teemu Likonen for the pattern
  local chars="${2:-[]/[\^$.*]}"
  printf '%s' "$1" | sed "s$chars\\\&g"
}

# If input is file, cat it, and then echo args
_CatEcho() { # [text]
  # TODO: prog name or something
  tty -s || cat
  echo " $@"
}

# Pick file list of <prefix><version>-<timestamp><suffix>
# and generate <prefix><version-=1>-<current_timestamp><suffix>
# VERSION is hexadecimal; use "-" for empty suffix or prefix
_RevFileNames() { # [prefix [suffix [max_version [stamp_format [newprefix]]]]]
  local prefix="${1:-file.}"; test "$prefix" != "-" || prefix=""
  local suffix="${2:-.log}"; test "$suffix" != "-" || suffix=""
  local vers="${3:-FFFF}"
  local stamp="$(date "${4:-+%Y-%m.%d-%H.%M}")"
  local newprefix="${5:-$prefix}"

  local last="$(ls -d "$prefix"*[-.:,_]*"$suffix" 2> /dev/null | sort | head -1)"
  test -n "$last" || last="$prefix$vers-$stamp$suffix"
  local repf="$(_Quote "$prefix")"
  local resf="$(_Quote "$suffix")"
  local new="$(echo "$last" | _Esed "s/${repf}([[:xdigit:]]+).*/\1/")"
  local div="$(echo "$last" | _Esed "s/${repf}[[:xdigit:]]+([^[:digit:]]*).*/\1/")"
  echo "$newprefix$(printf "%0${#new}X" "$((0x$new-1))")$div$stamp$suffix"
}

# Calculate longest common path of file list on stdin or arguments
_LCP() { # [path1 [path2 ...]]
  local n
  case "$#" in
    0) cat;;
    *) for n; do echo "$n"; done;;
  esac | sed -e 's,$,/,;1{h;d;}' -e 'G;s,\(.*/\).*\n\1.*,\1,;h;$!d;s,/$,,'
}

case "$_GMe" in
  functions|*.functions) # directly called
    case "$*" in
      ?*) "$@" ;;
      *)
	echo "$__Version" >&2
	tty -s && PG=less || PG="cat -s"
	_Digest "[^_]" "$_FMe" | $PG
	;;
    esac
  ;;
esac
