#!/bin/sh
# -*- indent-tabs-mode: nil; -*-
#:
#: A simple library to emulate simple array (one-dimensional) in a POSIX shell.
#:
#: :Author:    Franz Glasner
#: :Copyright: (c) 2024 Franz Glasner.
#:             All rights reserved.
#: :License:   BSD 3-Clause "New" or "Revised" License.
#:             See LICENSE for details.
#:             If you cannot find LICENSE see
#:             <https://opensource.org/licenses/BSD-3-Clause>
#: :ID:        @(#)@@SIMPLEVERSIONTAG@@
#:
#: This implementation is inspired by
#: https://unix.stackexchange.com/questions/137566/arrays-in-unix-bourne-shell
#:
#: Is implements one-dimensional array with one-base indexing.
#:
#: Hints and rules:
#:
#: - Every array has a NAME
#: - One-based indexing is used
#: - Array elements are stored in a global variable named
#:   ``_farr_<NAME>_<index-number>``
#: - The number of elements in the array ist stored in the global
#:   variable ``_farr_<NAME>__``
#: - An array name must conform to shell variable naming conventions
#: - Currently the number of of elements of an array must be >= 0
#: - An unset global variable ``_farr_<NAME>__`` variable is a severe
#:   error normally and forces an immediate error ``exit``.
#:   Exceptions to this rule are documented.
#:


_farr_global_prefix="_farr_"


#:
#: Internal error for fatal errors
#:
#: Args:
#:   $1 (str): The error message
#:
_array_fatal() {
    echo "ERROR: ${1}" 1>&2
    exit 70    # EX_SOFTWARE
}


#:
#: Create a new array
#:
#: Args:
#:   $1 (str): The name of the array.
#:             Must conform to shell variable naming conventions
#:   $2... (optional): Optional initialization values
#:
#: It is assumed that the array does not exist already.
#:
array_create() {
    local _name

    local _gvrname _el _l

    [ $# -lt 1 ] && _array_fatal "missing array name"
    _name=$1
    _gvrname=${_farr_global_prefix}$1
    shift

    # Check whether the variable already exists
    eval _l=\${${_gvrname}__:-__UNSET__}

    [ "${_l}" != "__UNSET__" ] && _array_fatal "array \`${_name}' already exists"
    # Really create
    eval ${_gvrname}__=0

    for _el in "$@"; do
	array_append ${_name} "${_el}"
    done
}


#:
#: Create or re-create a new array.
#:
#: Args:
#:   $1 (str): The name of the array.
#:             Must conform to shell variable naming conventions
#:   $2... (optional): Optional initialization values
#:
#: If the array exists already it will be reinitialized completely.
#: If the array does not exist already it is just like `array_create`.
#: It is just a convenience function for calling `array_destroy`, ignoring
#: errors eventually, and calling `array_create`.
#:
array_new() {
    array_destroy $1 || true
    array_create "$@"
}


#:
#: Get the length of an array
#:
#: Args:
#:   $1 (str): The name of the array.
#:
#: Output (stdout):
#:   The number of elements of the array.
#:   If the array does not exist the output is -1.
#:
array_length() {
    local _name

    local _gvrname _l

    [ $# -lt 1 ] && _array_fatal "missing array name"
    _name=$1
    _gvrname=${_farr_global_prefix}$1

    # Check whether the variable already exists
    eval _l=\${${_gvrname}__:-__UNSET__}

    if [ "${_l}" = "__UNSET__" ]; then
	printf "%s" "-1"
    else
	printf "%s" "${_l}"
    fi
}


#:
#: Append a value to an existing array
#:
#: Args:
#:   $1 (str): The name of the existing array
#:   $2 (optional): The value to append. If the value is not given the null
#:                  will be appended.
#:
array_append() {
    local _name _value

    local _gvrname _l _l1

    [ $# -lt 1 ] && _array_fatal "missing array name"
    _name=$1
    _gvrname=${_farr_global_prefix}$1
    _value="${2-}"

    # Check whether the variable already exists
    eval _l=\${${_gvrname}__:-__UNSET__}
    if [ "${_l}" = "__UNSET__" ]; then
	_array_fatal "array \`${_name}' does not exist"
    fi

    _l1=$((${_l} + 1))
    # Set value
    eval ${_gvrname}_${_l1}="\"${_value}\""
    # Set new array length
    eval ${_gvrname}__=${_l1}
}


#:
#: Get an array value from a given index
#:
#: Args:
#:   $1 (str): The name of the existing array
#:   $2 (int): The index
#:
#: Output (stdout):
#:   The value at index $2.
#:
array_get() {
    local _name _index

    local _gvrname _l _value

    [ $# -lt 1 ] && _array_fatal "missing array name"
    _name=$1
    _gvrname=${_farr_global_prefix}$1
    [ $# -lt 2 ] && _array_fatal "missing array index"
    _index=$2

    # Check whether the variable already exists
    eval _l=\${${_gvrname}__:-__UNSET__}
    if [ "${_l}" = "__UNSET__" ]; then
	_array_fatal "array \`${_name}' does not exist"
    fi

    # check index range
    if [ \( "${_index}" -lt 1 \) -o \( "${_index}" -gt ${_l} \) ]; then
	_array_fatal "array index out of bounds"
    fi

    eval _value=\"\${${_gvrname}_${_index}}\"
    printf "%s" "${_value}"
}


#:
#: Empty an existing array
#:
#: Args:
#:   $1 (str): The name of the existing array
#:
array_clear() {
    local _name

    local _gvrname _l _idx

    [ $# -lt 1 ] && _array_fatal "missing array name"
    _name=$1
    _gvrname=${_farr_global_prefix}$1

    # Check whether the variable already exists
    eval _l=\${${_gvrname}__:-__UNSET__}
    if [ "${_l}" = "__UNSET__" ]; then
	_array_fatal "array \`${_name}' does not exist"
    fi

    _idx=1
    while [ ${_idx} -le ${_l} ]; do
	eval unset ${_gvrname}_${_idx}
	_idx=$((${_idx} + 1))
    done

    # Length is now zero
    eval ${_gvrname}__=0
}


#:
#: Destroy and unset an array and all its elements
#:
#: Args:
#:   $1 (str): The name of an array. The array may exist or not.
#:
#: Returns:
#:   - A truthy value if the array existed and has been deleted
#:   - A falsy value if the array does not exist
#:
array_destroy() {
    local _name

    local _gvrname _l _idx

    [ $# -lt 1 ] && _array_fatal "missing array name"
    _name=$1
    _gvrname=${_farr_global_prefix}$1

    # Handle non-existing array names
    eval _l=\${${_gvrname}__:-__UNSET__}
    if [ "${_l}" = "__UNSET__" ]; then
	return 1
    fi
    _idx=1
    while [ ${_idx} -le ${_l} ]; do
	eval unset ${_gvrname}_${_idx}
	_idx=$((${_idx} + 1))
    done

    # Remove
    eval unset ${_gvrname}__
}


#:
#: Call a function for every element in an array starting at the first index
#:
#: The function to be called must accept three arguments:
#: - the array name
#: - the current index
#: - the element value at the current index
#:
#: The iteration stops if the called function returns a falsy value.
#:
#: Args:
#:   $1 (str): The name of an existing array.
#:   $2 (str): The name of a function to be called with three arguments.
#:
#: Warning:
#:   If the number of elements changes while being in `array_for_each` then
#:   the behaviour is undefined.
#:   The current implementation determines the length of the array once
#:   at the start of execution.
#:
array_for_each() {
    local _name _cb

    local _gvrname _l _idx _value _rv

    [ $# -lt 1 ] && _array_fatal "missing array name"
    _name=$1
    _gvrname=${_farr_global_prefix}$1
    [ $# -lt 2 ] && _array_fatal "missing callback function name"
    _cb="$2"

    # Check whether the variable already exists
    eval _l=\${${_gvrname}__:-__UNSET__}
    if [ "${_l}" = "__UNSET__" ]; then
	_array_fatal "array \`${_name}' does not exist"
    fi

    _idx=1
    while [ ${_idx} -le ${_l} ]; do
	eval "${_cb} ${_name} ${_idx} \"\${${_gvrname}_${_idx}}\""
	_rv=$?
	[ ${_rv} -ne 0 ] && return ${_rv}
	_idx=$((${_idx} + 1))
    done
    return 0
}


#:
#: Like `array_for_each`, but the function is called in reversed order --
#: beginning with the last index
#:
array_reversed_for_each() {
    local _name _cb

    local _gvrname _l _idx _value _rv

    [ $# -lt 1 ] && _array_fatal "missing array name"
    _name=$1
    _gvrname=${_farr_global_prefix}$1
    [ $# -lt 2 ] && _array_fatal "missing callback function name"
    _cb="$2"

    # Check whether the variable already exists
    eval _l=\${${_gvrname}__:-__UNSET__}
    if [ "${_l}" = "__UNSET__" ]; then
	_array_fatal "array \`${_name}' does not exist"
    fi

    _idx=${_l}
    while [ ${_idx} -gt 0 ]; do
	eval "${_cb} ${_name} ${_idx} \"\${${_gvrname}_${_idx}}\""
	_rv=$?
	[ ${_rv} -ne 0 ] && return ${_rv}
	_idx=$((${_idx} - 1))
    done
    return 0
}
