#!/bin/bash

# Internal helper design to allow helpers to use getopts to manage their arguments
#
# [internal]
#
# example: function my_helper()
# {
#     local -A args_array=( [a]=arg1= [b]=arg2= [c]=arg3 )
#     local arg1
#     local arg2
#     local arg3
#     ynh_handle_getopts_args "$@"
#
#     [...]
# }
# my_helper --arg1 "val1" -b val2 -c
#
# usage: ynh_handle_getopts_args "$@"
# | arg: $@    - Simply "$@" to tranfert all the positionnal arguments to the function
#
# This helper need an array, named "args_array" with all the arguments used by the helper
# 	that want to use ynh_handle_getopts_args
# Be carreful, this array has to be an associative array, as the following example:
# local -A args_array=( [a]=arg1 [b]=arg2= [c]=arg3 )
# Let's explain this array:
# a, b and c are short options, -a, -b and -c
# arg1, arg2 and arg3 are the long options associated to the previous short ones. --arg1, --arg2 and --arg3
# For each option, a short and long version has to be defined.
# Let's see something more significant
# local -A args_array=( [u]=user [f]=finalpath= [d]=database )
#
# NB: Because we're using 'declare' without -g, the array will be declared as a local variable.
#
# Please keep in mind that the long option will be used as a variable to store the values for this option.
# For the previous example, that means that $finalpath will be fill with the value given as argument for this option.
#
# Also, in the previous example, finalpath has a '=' at the end. That means this option need a value.
# So, the helper has to be call with --finalpath /final/path, --finalpath=/final/path or -f /final/path, the variable $finalpath will get the value /final/path
# If there's many values for an option, -f /final /path, the value will be separated by a ';' $finalpath=/final;/path
# For an option without value, like --user in the example, the helper can be called only with --user or -u. $user will then get the value 1.
#
# To keep a retrocompatibility, a package can still call a helper, using getopts, with positional arguments.
# The "legacy mode" will manage the positional arguments and fill the variable in the same order than they are given in $args_array.
# e.g. for `my_helper "val1" val2`, arg1 will be filled with val1, and arg2 with val2.
#
# Requires YunoHost version 3.2.2 or higher.
ynh_handle_getopts_args() {
    # Manage arguments only if there's some provided
    set +o xtrace # set +x
    if [ $# -eq 0 ]; then
        set -o xtrace # set -x
        return
    fi

    # Store arguments in an array to keep each argument separated
    local arguments=("$@")

    # For each option in the array, reduce to short options for getopts (e.g. for [u]=user, --user will be -u)
    # And built parameters string for getopts
    # ${!args_array[@]} is the list of all option_flags in the array (An option_flag is 'u' in [u]=user, user is a value)
    local getopts_parameters=""
    local option_flag=""
    for option_flag in "${!args_array[@]}"; do
        # Concatenate each option_flags of the array to build the string of arguments for getopts
        # Will looks like 'abcd' for -a -b -c -d
        # If the value of an option_flag finish by =, it's an option with additionnal values. (e.g. --user bob or -u bob)
        # Check the last character of the value associate to the option_flag
        if [ "${args_array[$option_flag]: -1}" = "=" ]; then
            # For an option with additionnal values, add a ':' after the letter for getopts.
            getopts_parameters="${getopts_parameters}${option_flag}:"
        else
            getopts_parameters="${getopts_parameters}${option_flag}"
        fi
        # Check each argument given to the function
        local arg=""
        # ${#arguments[@]} is the size of the array
        for arg in $(seq 0 $((${#arguments[@]} - 1))); do
            # Escape options' values starting with -. Otherwise the - will be considered as another option.
            arguments[arg]="${arguments[arg]//--${args_array[$option_flag]}-/--${args_array[$option_flag]}\\TOBEREMOVED\\-}"
            # And replace long option (value of the option_flag) by the short option, the option_flag itself
            # (e.g. for [u]=user, --user will be -u)
            # Replace long option with = (match the beginning of the argument)
            arguments[arg]="$(printf '%s\n' "${arguments[arg]}" | sed "s/^--${args_array[$option_flag]}/-${option_flag} /")"
            # And long option without = (match the whole line)
            arguments[arg]="$(printf '%s\n' "${arguments[arg]}" | sed "s/^--${args_array[$option_flag]%=}$/-${option_flag} /")"
        done
    done

    # Read and parse all the arguments
    # Use a function here, to use standart arguments $@ and be able to use shift.
    parse_arg() {
        # Read all arguments, until no arguments are left
        while [ $# -ne 0 ]; do
            # Initialize the index of getopts
            OPTIND=1
            # Parse with getopts only if the argument begin by -, that means the argument is an option
            # getopts will fill $parameter with the letter of the option it has read.
            local parameter=""
            getopts ":$getopts_parameters" parameter || true

            if [ "$parameter" = "?" ]; then
                ynh_die "Invalid argument: -${OPTARG:-}"
            elif [ "$parameter" = ":" ]; then
                ynh_die "-$OPTARG parameter requires an argument."
            else
                local shift_value=1
                # Use the long option, corresponding to the short option read by getopts, as a variable
                # (e.g. for [u]=user, 'user' will be used as a variable)
                # Also, remove '=' at the end of the long option
                # The variable name will be stored in 'option_var'
                local option_var="${args_array[$parameter]%=}"
                # If this option doesn't take values
                # if there's a '=' at the end of the long option name, this option takes values
                if [ "${args_array[$parameter]: -1}" != "=" ]; then
                    # 'eval ${option_var}' will use the content of 'option_var'
                    eval ${option_var}=1
                else
                    # Read all other arguments to find multiple value for this option.
                    # Load args in a array
                    local all_args=("$@")

                    # If the first argument is longer than 2 characters,
                    # There's a value attached to the option, in the same array cell
                    if [ ${#all_args[0]} -gt 2 ]; then
                        # Remove the option and the space, so keep only the value itself.
                        all_args[0]="${all_args[0]#-${parameter} }"

                        # At this point, if all_args[0] start with "-", then the argument is not well formed
                        if [ "${all_args[0]:0:1}" == "-" ]; then
                            ynh_die "Argument \"${all_args[0]}\" not valid! Did you use a single \"-\" instead of two?"
                        fi
                        # Reduce the value of shift, because the option has been removed manually
                        shift_value=$((shift_value - 1))
                    fi

                    # Declare the content of option_var as a variable.
                    eval ${option_var}=""
                    # Then read the array value per value
                    local i
                    for i in $(seq 0 $((${#all_args[@]} - 1))); do
                        # If this argument is an option, end here.
                        if [ "${all_args[$i]:0:1}" == "-" ]; then
                            # Ignore the first value of the array, which is the option itself
                            if [ "$i" -ne 0 ]; then
                                break
                            fi
                        else
                            # Ignore empty parameters
                            if [ -n "${all_args[$i]}" ]; then
                                # Else, add this value to this option
                                # Each value will be separated by ';'
                                if [ -n "${!option_var}" ]; then
                                    # If there's already another value for this option, add a ; before adding the new value
                                    eval ${option_var}+="\;"
                                fi

                                # Remove the \ that escape - at beginning of values.
                                all_args[i]="${all_args[i]//\\TOBEREMOVED\\/}"

                                # For the record.
                                # We're using eval here to get the content of the variable stored itself as simple text in $option_var...
                                # Other ways to get that content would be to use either ${!option_var} or declare -g ${option_var}
                                # But... ${!option_var} can't be used as left part of an assignation.
                                # declare -g ${option_var} will create a local variable (despite -g !) and will not be available for the helper itself.
                                # So... Stop fucking arguing each time that eval is evil... Go find an other working solution if you can find one!

                                eval ${option_var}+='"${all_args[$i]}"'
                            fi
                            shift_value=$((shift_value + 1))
                        fi
                    done
                fi
            fi

            # Shift the parameter and its argument(s)
            shift $shift_value
        done
    }

    # Call parse_arg and pass the modified list of args as an array of arguments.
    parse_arg "${arguments[@]}"
    
    set -o xtrace # set -x
}
