#!/bin/bash
#
# Bash library and executable that manipulate [markdown](https://en.wikipedia.org/wiki/Markdown). Today, only converts: markdown arrays <> bash arrays.
#
# Copyright © 2025 Jean-Jacques Brucker <jjbrucker@foopgp.org>
#
# SPDX-License-Identifier: LGPL-3.0-only
#

if [[ "$1" == --bash-completion ]] ; then
	BL_tmp_a=$("${BASH_SOURCE[0]}" --help | sed -nE 's:^ {2,4}([a-z0-9_]+\>).*:\1:p')
	BL_tmp_o=$(for a in $BL_tmp_a ; do
		echo "[$a]=\"$(eval echo $("$BASH_SOURCE" $a --help | sed -n 's,^  ... \(--[a-z_-]\+[^ ]*\).*,\1,p' | sed 's_<_{_ ; s_|_,_g ; s_>_}_ '))\""
	done)

	eval '_bl_md_completion()
	{
		local cur coptions="--help --version"
		local a actions="'$BL_tmp_a'"
		local -A aoptions=('$BL_tmp_o')

		COMPREPLY=()
		cur=${COMP_WORDS[COMP_CWORD]}

		case ${COMP_WORDS[COMP_CWORD-1]} in
			--help|--version)
				# should exit without executing any actions, so do not complete anything
				return 0 ;;
		esac
		case $cur in
			-*)
				if a=$(grep -o "\<\('${BL_tmp_a//$'\n'/\\|}'\)\>" <<< "${COMP_WORDS[@]}") ; then
					COMPREPLY=( $(compgen -W "$coptions ${aoptions[$a]}" -- $cur ) )
					return 0
				fi
				COMPREPLY=( $(compgen -W "$coptions" -- $cur ) )
				return 0 ;;
		esac
		# complete actions if none in ${COMP_WORDS[@]}
		grep -q "\<\('${BL_tmp_a//$'\n'/\\|}'\)\>" <<< "${COMP_WORDS[@]}" || COMPREPLY=( $(compgen -W "$actions" -- $cur ) )
		return 0
	}'
	unset BL_tmp_a BL_tmp_o
	complete -F _bl_md_completion "$(basename "$BASH_SOURCE")" "$BASH_SOURCE"
	return 0
fi

# If sourcing while _bl_md_parseoptions is already set, execute the function and return without reloading rest of file.
if [[ "$BASH_SOURCE" != "$0" ]] && [[ "$(type -t _bl_md_parseoptions)" == function ]] ; then
	_bl_md_parseoptions "$@"
	return $?
fi

### Constants ###

BL_MD_NAME="$(basename "$(readlink -f "$BASH_SOURCE")" )"
BL_MD_VERSION="0.3.5-1"
BL_MD_FUNCTIONS=( $(sed -n 's,^\(bl_[^( ]*\) *().*,\1,p' "$BASH_SOURCE") )
readonly BL_MD_NAME BL_MD_VERSION BL_MD_FUNCTIONS

[[ "$BASH_SOURCE" == "$0" ]] && declare -r BL_MD_isprogram=1 || declare -r BL_MD_isprogram=0

### Others Globals ###

if ((BL_MD_isprogram)) ; then
	TEXTDOMAIN="bashlibs"
	TEXTDOMAINDIR="$(dirname "$(readlink -f "$BASH_SOURCE")" )/../share/locale"
	# Require Bash 5.2. - https://www.kurokatta.org/grumble/2023/11/bash-translated-strings#fnref3
	shopt -s noexpand_translation
fi

BL_MD_chelpmsg="
Manipulate [markdown](https://en.wikipedia.org/wiki/Markdown). Today, only converts: markdown arrays <> bash arrays.

MAIN OPTIONS:"

### external functions ###

### internal functions ###

_bl_md_parseoptions() {
	local npp=$#
	for ((;$#;)) ; do
		case "$1" in
			-h|--help) printf "%s\n%s%s" "$BL_MD_usage" "$BL_MD_chelpmsg" "$BL_MD_shelpmsg" ; return 1 ;;
			-V|--version) printf "%s %s\n" "$BL_MD_NAME" "$BL_MD_VERSION" ; return 1 ;;
			--) shift ; break ;;
			-*) printf "%s: unrecognized option '%s'\n\nTry '%s --help' for more information.\n" "$BL_MD_NAME" "$1" "$BASH_SOURCE" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done
	BL_MD_NOPTIONS=$((npp-$#))
}

_bl_md_tonatnum() {
	(($#)) && sed 's/[,.]// ; s/^\([+-]\?\)0*/\1/' <<<"$1" || sed 's/[,.]// ; s/^\([+-]\?\)0*/\1/'
}

_bl_md_todecnum2() {
	local in etc
	(($#)) && in=$1 || read in etc
	in=$(printf "% 04d" "$in")
	sed 's/.\{2\}$/,&/' <<<"$in"
}

### public functions / program actions ###

bl_md_arraytobash() {
	local name
	((BL_MD_isprogram)) && name="$BL_MD_NAME ${FUNCNAME:6}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS] [FIELD_NAMES]..."
	local helpmsg="
Expect stdin to be a markdown array (with or whithout header), and convert each line of this array to a  \"declare -A ...\" statement to be loaded with eval.
If first line of input isn't an array header, use positional parameter to set (or overwrite) field names.
OPTIONS:
  -d, --declare VARNAME Set name in declare -A VARNAME=(... statement (default: \"entry\")
"
	local varname=entry

	for ((;$#;)) ; do
		case "$1" in
			-d|--declare)
				shift ; varname="$1"
				[[ "${varname}" =~ ^[a-zA-Z_][0-9a-zA-Z_]*$ ]] || { echo "$FUNCNAME: Error: given VARNAME is not a valid posix name ${varname::5}..." >&2  ; return 2 ; }
				;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) echo "$FUNCNAME $BL_MD_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local line
	local -a fields fieldnames
	local -A entry

	read line
	readarray -t fields < <(sed ' s,^|\?,, ; s, *|\?$,, ; s, \+|,\n,g ' <<<"$line" | sed 's,^ *,,' )
	fieldnames=("${fields[@]}")
	nbcol=${#fields[@]}

	# Overwrite any fieldnames whith those given on command line (and non-empty)
	i=0
	for field in "$@" ; do
		[[ -z "$field" ]] || fieldnames[$i]="$field"
		((++i))
	done

	# create first associative array to check fieldname uniqness, unescaping '|' characters
	for ((i=0;i<nbcol;i++)) ; do
		# [[ "${fieldnames[$i]}" ]] || fieldnames[$i]="$i"
		entry[${fieldnames[$i]}]=${fields[$i]//\\|/|}
	done

	if [[ ${#entry[@]} != $nbcol ]] ; then
		echo "$FUNCNAME: Error: expected $nbcol unique fieldnames, got ${#entry[@]}." >&2
		return 1
	fi

	if ! read line ; then
		#Only one line -> no header.
		declare -p entry | sed "0,/^declare -A entry=/{s//declare -A ${varname}=/}"
		return
	elif [[ "$line" =~ ^[|:-]+$ ]] ; then
		# second line is a separation line so first line was fieldnames
		header=true
	else
		header=false
	fi

	if ! $header ; then # Interpret 2 first lines
		declare -p entry | sed "0,/^declare -A entry=/{s//declare -A ${varname}=/}"
		readarray -t fields < <(sed ' s,^|\?,, ; s, *|\?$,, ; s, \+|,\n,g ' <<<"$line" | sed 's,^ *,,' )
		for ((i=0;i<nbcol;i++)) ; do
			entry[${fieldnames[$i]}]=${fields[$i]//\\|/|}
		done
		declare -p entry | sed "0,/^declare -A entry=/{s//declare -A ${varname}=/}"
	fi

	while read line ; do
		readarray -t fields < <(sed ' s,^|\?,, ; s, *|\?$,, ; s, \+|,\n,g ' <<<"$line" | sed 's,^ *,,' )
		for ((i=0;i<nbcol;i++)) ; do
			entry[${fieldnames[$i]}]=${fields[$i]//\\|/|}
		done
		declare -p entry | sed "0,/^declare -A entry=/{s//declare -A ${varname}=/}"
	done
}

bl_md_arrayfrombash() {
	local name
	((BL_MD_isprogram)) && name="$BL_MD_NAME ${FUNCNAME:6}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS] [FIELD_NAMES]..."
	local helpmsg="
Expect stdin to be lines of \"declare -A ...\" statement with the identical indexes/fieldnames.
Then eval and output markdown array ; using [FIELD_NAMES]... to order columns.
OPTIONS:
  -H, --header-only    Only display header line, and return.
  -F, --with-header    Also display header line and separation line at the beginning of array.
"
	local header=0 onlyheader=0

	for ((;$#;)) ; do
		case "$1" in
			-H|--header-only) onlyheader=1 header=1 ;;
			-F|--with-header) onlyheader=0 header=1 ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) echo "$FUNCNAME $BL_MD_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local -a fieldnames

	while read line ; do
		if ! [[ "$line" =~ ^declare" "-[Aa][a-zA-Z]*" "([a-zA-Z_][a-zA-Z0-9_]*)=\(.*" "\)$ ]] ; then
			printf "%s: Warning: unexpected line %s\n" "$FUNCNAME" "${line::21}..." >&2
			continue
		else
			local "${BASH_REMATCH[1]}"
			eval "$line" # dangerous as previous regexp isn't sufficient to prevent security issues
			local -n aa=${BASH_REMATCH[1]}
		fi
		# Construct fieldnames
		if [[ -z "$fieldnames" ]] ; then
			for f in "$@" ; do
				#if printf '%s\0' "${!aa[@]//|/\\|}" | grep -Fxqz -- "$f" ; then
				if printf '%s\0' "${!aa[@]}" | grep -Fxqz -- "$f" ; then
					fieldnames+=("$f")
				else
					printf "%s: Warning: no matching FIELD_NAME '%s'. Ignoring...\n" "$FUNCNAME" "$f" >&2
				fi
			done
			#for f in "${!aa[@]//|/\\|}" ; do
			for f in "${!aa[@]}" ; do
				if ! printf '%s\0' "${fieldnames[@]}" | grep -Fxqz -- "$f" ; then
					fieldnames+=("$f")
				fi
			done
		fi

		if ((header)) ; then
			printf "|"
			printf " %-16s |" "${fieldnames[@]}"
			printf "\n"
		fi
		((!onlyheader)) || return
		if ((header)) ; then
			printf "|"
			printf -- "------------------%.0s|" "${!aa[@]}"
			printf "\n"
			header=0
		fi
		printf "|"
		for f in "${fieldnames[@]}" ; do
			printf " %-16s |" "${aa[${f}]//|/\\|}"
		done
		#TODO (maybe): change size of field if its a date, or right aling if its numbers
		printf "\n"
	done
}

### Init ###

if ((BL_MD_isprogram)) ; then
	BL_MD_usage="Usage: $BASH_SOURCE [MAIN_OPTIONS]... ACTION [OPTIONS]... [ARGUMENTS]..."
	BL_MD_shelpmsg="
  -h, --help              "$"Show help and exit.""
  -V, --version           "$"Show version and exit.""

ACTIONS:
$(for f in "${BL_MD_FUNCTIONS[@]}" ; do printf "   %-19s%s\n" "${f:6}" "$($f --help | sed -n '/^$/{n;p;q}')" ; done)

All actions support a --help option, eg:
$ $BASH_SOURCE ${BL_MD_FUNCTIONS:6} --help

$BL_MD_NAME is also bash library, see:
$ source $BASH_SOURCE --help
"
else
	BL_MD_usage="Usage: source $BASH_SOURCE [MAIN_OPTIONS]..."
	BL_MD_shelpmsg="
      --bash-completion    set completion for $BASH_SOURCE program and return (without loading anything else)

Functions:
$(for f in "${BL_MD_FUNCTIONS[@]}" ; do printf "   %-19s%s\n" "$f" "$($f --help | sed -n '/^$/{n;p;q}')" ; done)

Reminder: when used as a library, all functions calls share the same environment variables, i.e. the same global options.
"
fi

# Parse Options
_BL_MD_RETVAL=0
_bl_md_parseoptions "$@" || _BL_MD_RETVAL=$?

# Do nothing else if sourced
[[ "$BASH_SOURCE" == "$0" ]] || return $_BL_MD_RETVAL

[[ "$_BL_MD_RETVAL" != "1"  ]] || exit 0
[[ "$_BL_MD_RETVAL" < 2 ]] || exit $_BL_MD_RETVAL

### Run ###
set -e
shift $BL_MD_NOPTIONS
if function=$(grep -o "\<bl_md_$1\>" <<< "${BL_MD_FUNCTIONS[@]}") ; then
	shift
	$function "$@"
	exit $?
else
	printf "$BL_MD_NAME: Error: "$"Unrecognized action"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$BASH_SOURCE" >&2
	exit 2
fi

