# shellcheck shell=bash
#
# functional_bash.sh - Bash functions for a "functional style"
#
# Source this file at the top of your Bash script.
#
# Usage:   source "/path/to/functional_bash.sh"
# Example: source "/usr/local/lib/functional_bash.sh"
#
# Author:  Wolfgang <w6g@wu6ch.de>
# License: MIT (https://wu6ch.de/bash/functional_bash/LICENSE)
# Home:    https://wu6ch.de/bash/functional_bash/
# GitLab:  https://gitlab.com/w6g/functional_bash


set +o errexit -o nounset -o pipefail


get_unset() {
  echo "fnVuc2V0fgo="   # base64 --wrap=0 <<< "~unset~" ; echo
}


get_set() {
  echo "fn5zZXR+fgo="   # base64 --wrap=0 <<< "~~set~~" ; echo
}


is_unset() {
  [ "$1" == "$(get_unset)" ]
}


is_set() {
  ! is_unset "$1"
}


get_minint() {
  local value
  (( value = -(2 ** 63) ))
  (( value == 0 )) && (( value = -(2 ** 31) ))
  echo "$value"
}


get_maxint() {
  local value
  (( value = 2 ** 63 - 1 ))
  (( value == -1 )) && (( value = 2 ** 31 - 1 ))
  echo "$value"
}


get_min() {
  (( $1 < $2 )) ; put_cond "$2" "$1" $?
}


get_max() {
  (( $1 > $2 )) ; put_cond "$2" "$1" $?
}


strtrim() {
  sed -e 's/^  *//' -e 's/  *$//' <<< "$1"
}


list() {
  local i ; for i in "$@" ; do echo "$i" ; done
}


llength() {
  wc -l
}


lnull() {
  (( $(llength) == 0 ))
}


ltake() {
  head -n "${1}"
}


ldrop() {
  tail -n +$(($1 + 1))
}


lhead() {
  ltake 1
}


ltail() {
  ldrop 1
}


llast() {
  tail -n 1
}


linit() {
  head -n -1
}


lcons() {
  echo "$1"
  cat -
}


lprepend() {
  list "$@"
  cat -
}


lappend() {
  cat -
  list "$@"
}


lreverse() {
  tac -
}


lminval() {
  # shellcheck disable=SC2317
  _minval() {
    (( $1 < $2 )) ; put_cond "$2" "$1" $?
  }

  lfoldl "$(get_maxint)" _minval
}


lmaxval() {
  # shellcheck disable=SC2317
  _maxval() {
    (( $1 > $2 )) ; put_cond "$2" "$1" $?
  }

  lfoldl 0 _maxval
}


lminlen() {
  # shellcheck disable=SC2317
  _minlen() {
    local len="${#2}"
    (( $1 < len )) ; put_cond "$len" "$1" $?
  }

  lfoldl "$(get_maxint)" _minlen
}


lmaxlen() {
  # shellcheck disable=SC2317
  _maxlen() {
    local len="${#2}"
    (( $1 > len )) ; put_cond "$len" "$1" $?
  }

  lfoldl 0 _maxlen
}


list_to_array() {
  local -n _array="$1"
  # shellcheck disable=SC2034
  mapfile -t _array
}


to_array() {
  # shellcheck disable=SC2034
  local -n __array="$1" ; shift
  list_to_array __array < <(list "$@")
}


lmap() {
  local ret_val=0 rval element

  while read -r element
  do
    "$@" "$element" ; rval=$?
    ((ret_val == 0)) && ret_val=$rval
  done

  return $ret_val
}


lapply() {
  # shellcheck disable=SC2317
  _func_map_func() {
    local array=()
    list_to_array array < <(tuple_to_list "$2")
    "${array[@]}" "$1"
  }

  # shellcheck disable=SC2317
  _list_map_func() {
    local -n _funcs="$1" ; shift
    list "${_funcs[@]}" | lmap _func_map_func "$@"
  }

  lmap _list_map_func "$@"
}


lbind() {
  lmap "$@"
}


lfilter() {
  local element

  while read -r element
  do
    "$@" "$element" && echo "$element"
  done

  true
}


lfilter_nonhash() {
  tr -d '\015' | sed -e 's/^  *//' -e 's/ *#.*$//' -e '/^$/d'
}


ljoin_backslash_terminated() {
  sed -e :a -e '/\\$/N ; s/\\\n// ; ta'
}


lfoldl() {
  local acc="$1" ; shift
  local ret_val=0 rval element

  while read -r element
  do
    acc="$("$@" "$acc" "$element")" ; rval=$?
    ((ret_val == 0)) && ret_val=$rval
  done

  echo -e "$acc"
  return $ret_val
}


lscanl() {
  local acc="$1" ; shift
  local ret_val=0 rval element

  while read -r element
  do
    acc="$("$@" "$acc" "$element")" ; rval=$?
    ((ret_val == 0)) && { ret_val=$rval; echo "$acc"; }
  done

  echo -e "$acc"
  return $ret_val
}


lfoldm() {
  local acc="$1" ; shift
  local ret_val=0 rval element
  local pair out

  while read -r element
  do
    pair="$("$@" "$acc" "$element")" ; rval=$?
    ((ret_val == 0)) && ret_val=$rval
    acc="$(tfst "$pair")" ; out="$(tsnd "$pair")"
    ((ret_val == 0)) && is_set "$out" && echo "$out"
  done

  echo -e "$acc"
}


lunfoldr() {
  local seed="$1" ; shift
  local pair

  while pair="$("$@" "$seed")"
  do
    tfst "$pair"
    seed="$(tsnd "$pair")"
  done

  true
}


show_list() {
  # shellcheck disable=SC2317
  _show_list_func() {
    [ -n "$1" ] && echo -n "${1},"
    echo "$2"
  }

  echo "[$(lfoldl "" _show_list_func)]"
}


tencode() {
  base64 --wrap=0 <<< "$1"
  echo
}


tdecode() {
  base64 -d <<< "$1"
}


to_pair() {
  echo "$(tencode "$1"),$(tencode "$2")"
}


list_to_tuple() {
  # shellcheck disable=SC2317
  _list_to_tuple_func() {
    [ -n "$1" ] && echo -n "${1},"
    echo "$2"
  }

  lmap tencode | lfoldl "" _list_to_tuple_func
}


to_tuple() {
  list "$@" | list_to_tuple
}


split_at() {
  local s ; s="$(sed -n "s/^\(.*\)$1\(.*\)\$/\\1\\t\\2/p" <<< "$2")"
  to_pair  "$(cut -f 1 <<< "$s")" "$(cut -f 2 <<< "$s")"
}


tnth() {
  (( $1 > 0 )) \
    && (( $(tr -dc ',' <<< "$2" | wc -m) >= $(($1 - 1)) )) \
    && tdecode "$(cut -d "," -f "$1" <<< "$2")"
}


tfst() {
  tnth 1 "$1"
}


tsnd() {
  tnth 2 "$1"
}


tthd() {
  tnth 3 "$1"
}


swap_pair() {
  to_pair "$(tsnd "$1")" "$(tfst "$1")"
}


tmap() {
  local tuple="$1" ; shift
  tuple_to_list "$tuple" | lmap "$@" | list_to_tuple
}


tuple_to_list() {
  local i=1

  while tnth $i "$1"
  do
    i=$((i + 1))
  done
}


show_tuple() {
  # shellcheck disable=SC2317
  _show_tuple_func() {
    [ -n "$1" ] && echo -n "${1},"
    echo "$2"
  }

  echo "($(tuple_to_list "$1" | lfoldl "" _show_tuple_func))"
}


is_subset() {
  # shellcheck disable=SC2317
  _search_in_array() {
    local -n _searched_array="$1"
    list "${_searched_array[@]}" | grep -q "^${2}\$"
  }

  lmap _search_in_array "$1"
}


intersection() {
  local -n __searched_array="$1"
  local element

  while read -r element
  do
    list "${__searched_array[@]}" | grep -q "^$element\$" && echo "$element"
  done

  true
}


split_to_array() {
  local IFS="$2"
  local -n _split_array="$3"

  # shellcheck disable=SC2034
  read -ra _split_array <<< "$1"
}


amap() {
  local -n _assoc="$1" ; shift
  local ret_val=0 rval key

  for key in "${!_assoc[@]}"
  do
    "$@" "$key" "${_assoc["$key"]}" ; rval=$?
    ((ret_val == 0)) && ret_val=$rval
  done

  return $ret_val
}


zipwith() {
  local -n _left="$1"  ; shift
  local -n _right="$1" ; shift

  local num_left="${#_left[@]}"
  local num_right="${#_right[@]}"
  local num=$num_left ; (( num_left > num_right )) && num=$num_right

  local ret_val=0 i

  for (( i = 0; i < num; ++i ))
  do
    "$@" "${_left[i]}" "${_right[i]}" ; rval=$?
    ((ret_val == 0)) && ret_val=$rval
  done

  return $ret_val
}


get_script_name() {
  echo "${script_name-$(basename -- "$0")}"
}


put_cond() {
  if (( $3 == 0 )) ; then echo "$2" ; else echo "$1" ; fi
}


put_err() {
  echo -e "$(get_script_name): ${1}!" >&2
  false
}


put_msg() {
  echo -e "$(get_script_name): ${1}"
}


put_value() {
  if [ -n "$1" ] ; then echo "$1" ; else put_err "$2" ; fi
}


align_with() {
  local filler=${1:0:1}
  local str_len="${#3}"
  local num_fillers=$(($2 - str_len))
  local i

  for (( i = 0; i < num_fillers; ++i )) ; do echo -n "$filler" ; done
}


put_aligned() {
  echo "$3 $(align_with "$1" "$2" "$3") $4"
}


put_if_different() {
  local new ; new="$(cat -)"

  { ! is_set "${2-$(get_unset)}" && cmp "$1" <<< "$new" > /dev/null 2>&1; } || {
    [ -n "${3-}" ] && echo "$3"
    echo "$new"
    echo "$new" > "$1"
  }
}


is_installed() {
  type -p "$1" > /dev/null || put_err "$1 is not installed"
}


are_installed() {
  list "$@" | lmap is_installed
}


is_readable() {
  [ "$1" == "-" ] || [ -r "$1" ] || put_err "cannot read '$1'"
}


read_file() {
  is_readable "$1" && cat "$1"
}


get_options() {
  local optstring=":${1#:}"     ; shift
  local -n _options="$1"        ; shift
  local -n _remaining_args="$1" ; shift
  local help="$1"               ; shift
  local ret_val=0 opt

  OPTIND=1

  while getopts "$optstring" "opt"
  do
    # shellcheck disable=SC2034
    _options["$opt"]="${OPTARG-$(get_set)}"
    [ "$opt" == "?" ] && ret_val=1
  done

  shift "$((OPTIND - 1))"
  # shellcheck disable=SC2034
  _remaining_args=( "$@" )

  [ $ret_val == 0 ] || "$help" "$(get_script_name)"
  return $ret_val
}


get_arg() {
  local -n __options="$1"
  local arg="${3-$(get_unset)}"

  [[ -v __options["$2"] ]] && {
    arg="${__options["$2"]}"
    [ -z "$arg" ] && arg="$(get_set)"
  }

  echo "$arg"
}


get_conf() {
  local -n _conf="$1"
  local element pair key value

  while read -r element
  do
    pair="$(split_at "=" "$element")"
    key="$(  strtrim "$(tfst "$pair")" | sed -e 's/"//g' -e 's/[- ]/_/g')"
    value="$(strtrim "$(tsnd "$pair")" | sed -e 's/"//g')"
    # shellcheck disable=SC2034
    _conf["$key"]="$value"
  done < <(lfilter_nonhash | ljoin_backslash_terminated)
}


read_conf_file() {
  ! [ -e "$1" ] || get_conf "$2" < <(read_file "$1")
}


set_conf_env() {
  # shellcheck disable=SC2034
  local -n __conf="$1"

  # shellcheck disable=SC2317
  _set_var() { eval "$1=\"$2\""; }

  amap __conf _set_var
}


# EOF
