#!/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 [ $# -ne 0 ]; then
        # 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]="$(echo "${arguments[arg]}" | sed "s/^--${args_array[$option_flag]}/-${option_flag} /")"
                # And long option without = (match the whole line)
                arguments[arg]="$(echo "${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 --message="Invalid argument: -${OPTARG:-}"
                elif [ "$parameter" = ":" ]; then
                    ynh_die --message="-$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 --message="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
        }

        # LEGACY MODE
        # Check if there's getopts arguments
        if [ "${arguments[0]:0:1}" != "-" ]; then
            # If not, enter in legacy mode and manage the arguments as positionnal ones..
            # Dot not echo, to prevent to go through a helper output. But print only in the log.
            set -x
            echo "! Helper used in legacy mode !" >/dev/null
            set +x
            local i
            for i in $(seq 0 $((${#arguments[@]} - 1))); do
                # Try to use legacy_args as a list of option_flag of the array args_array
                # Otherwise, fallback to getopts_parameters to get the option_flag. But an associative arrays isn't always sorted in the correct order...
                # Remove all ':' in getopts_parameters
                getopts_parameters=${legacy_args:-${getopts_parameters//:/}}
                # Get the option_flag from getopts_parameters, by using the option_flag according to the position of the argument.
                option_flag=${getopts_parameters:$i:1}
                if [ -z "$option_flag" ]; then
                    ynh_print_warn --message="Too many arguments ! \"${arguments[$i]}\" will be ignored."
                    continue
                fi
                # Use the long option, corresponding to the option_flag, 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[$option_flag]%=}"

                # Store each value given as argument in the corresponding variable
                # The values will be stored in the same order than $args_array
                eval ${option_var}+='"${arguments[$i]}"'
            done
            unset legacy_args
        else
            # END LEGACY MODE
            # Call parse_arg and pass the modified list of args as an array of arguments.
            parse_arg "${arguments[@]}"
        fi
    fi
    set -o xtrace # set -x
}
