#!/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
#
#==========================================================
# Variables:

export _Me=`basename "$0"`			# script name
test -n "$_GMe" ||
export _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
test -n "$_Home" ||
export _Home="$HOME"				# base directory
export _AMe=`echo "$_Me" | tr -cd "[:alnum:]"`	# script name alphanumeric part (for generating filenames)
test -n "$__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 _RC_HOST="$_Home/.$_GMe$__RC_SUFFIX.`hostname`"	# 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=1.02				# 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
#	check _Verb if to display message
#	exit if errorlevel >0 or "x"
#	do not display error type in "Q" (quiet) mode
_Error() { # errormsg [errorlevel|"q" [errorlevel]]
  local ER ret
  case "${2:-1}" in
    [1-9]*) ER="ERROR"; ret="${2:-1}" ;;
    [0Ii]*) ER="INFO"; ret=0 ;;
    [-WwXx]*) ER="WARNING"; ret=-1 ;;
    [Qq]*) ER=""; ret="${3:--1}";;
    *) ER="ERROR"; ret=1 ;;
  esac
  if [ "$_Verb" -ge 1 ]	# verbose
  then
    __Message "$1" "$ER"
  elif [ "$_Verb" -ge 0 ]	# normal
  then
    test "$ret" -eq 0 || __Message "$1" "$ER" >&2
  else				# quiet
    test "$ret" -le 0 || __Message "$1" "$ER" >&2
  fi
  test $ret -le 0 -a "${2:-1}" != "x" || exit $ret
}

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

# Display message in verbose mode only
_Debug() { # [message] [verbosity]
  local v="${2:-0}"
  if [ $_Verb -gt "$v" ]; then _Msg "$1"; 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";;
      "") ;;
      *) _Error "It seems unsafe to delete '$F' directory" w ;;
    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 _F _P _Z _N="file" _X="XXXXXXXXXXXX"
  case "$1" in
    *[!_a-zA-Z0-9]*) _Error "Cannot store valuse to '$1' variable, please user alphanumeric name" ;;
    "") _Error "no variable name provided to _TmpFile function" ;;
  esac
  case "$2" in
    *p|p*) _P=""; _N="pipe";;
    *d|d*) _P="-d"; _N="directory";;
  esac
  test -z "$3" || _N="${3##*/}"
  _N="$_N.$_X$4"
  _F=`mktemp -t $_P "$_AMe.$_N"` || _Error "Cannot create '$_F' temporary $_N"
  case "$2" in
    *p|p*) # XXX look for race conditions here
        _Z="`mktemp XXXXXXXXXXXX`"
    	mv "$_F" "$_Z"
	mkfifo "$_F"
	rm -f "$_Z"
	;;
  esac
  case "$2" 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
}

#==========================================================
# 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" ||
    _Error "No '$Context' context found in $1" -1
  }
}

__CfGet() { # File Context Variable Div Value Tail
  test -z "$(sed -n "$2{=}" "$1")" &&
  _Error "No '$2' context found in $1" -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")" &&
  _Error "No '$Context' context found in $1" -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")" &&
  _Error "No '$Context' context found in $1" -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")" &&
  _Error "No '$Context' context found in $1" -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 "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
_DelArg() { # argument_string arg
  local a=" $1 "
  a="${a% $2 *} ${a#* $2 }"
  a="${a% }"; a="${a# }"
  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"
}

# use this to cat the stdin and echo args instead of running filter-like program
_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
_RevFileNames() { # [prefix [suffix [max_version [stamp_format]]]]
  local prefix="${1:-file.}"
  local suffix="${2:-.log}"
  local vers="${3:-FFFF}"
  local stamp="$(date "${4:-+%Y-%m.%d-%H:%M}")"

  local last="$(ls "$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 "$prefix$(printf "%0${#new}X" "$((0x$new-1))")$div$stamp$suffix"
}

# 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}"
}

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