##@cooked comments # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
## Gestion de fichiers de configuration et de répertoires de configuration
##@cooked nocomments
uprovide multiconf
urequire install

function conf_local() {
    # afficher les commandes pour définir comme locales les variables utilisées
    # par les fonctions conf_*
    # cela permet d'utiliser ces fonctions à l'intérieur d'autres fonctions sans
    # polluer l'espace de nom
    echo "local -a __CONF_DESCS __CONF_ARRAY_VARS __CONF_PATH_VARS"
}

function conf_auto() {
    # charger la configuration pour l'outil $1 avec les variables $2..@
    # conf_init n'est appelé que si des variables sont spécifiées, ce qui permet
    # d'appeler conf_init au préalable si une configuration spécifique doit être
    # faite.
    # Ainsi:
    #     conf_auto NAME VARS...
    # est équivalent à:
    #     conf_init VARS...
    #     conf_auto NAME
    # est équivalent à:
    #     conf_init VARS...
    #     conf_find_files __CONF_FILES ~/etc/default/NAME ~/etc/NAME.d/*.conf
    #     conf_upgrade ~/etc/default/NAME
    #     conf_load_files "${__CONF_FILES[@]}"
    # Pour supporter les scénarii où les fichiers de configuration sont ailleurs
    # que dans ~/etc/default, l'argument NAME peut être un chemin:
    #    conf_auto PATH/TO/NAME VARS...
    # est équivalent à:
    #     conf_init VARS...
    #     conf_find_files __CONF_FILES PATH/TO/NAME.conf PATH/TO/NAME.d/*.conf
    #     conf_upgrade PATH/TO/NAME.conf
    #     conf_load_files "${__CONF_FILES[@]}"
    local __name="$1"; shift
    [ -n "$__name" ] || return 1
    [ $# -gt 0 ] && conf_init "$@"
    local -a __CONF_FILES
    if [[ "$__name" == */* ]]; then
        conf_find_files __CONFS_FILES "$__name.conf" "$__name.d/*.conf"
        [ $# -gt 0 ] && conf_upgrade "$__name.conf"
    else
        conf_find_files __CONFS_FILES "$HOME/etc/default/$__name" "$HOME/etc/$__name.d/*.conf"
        [ $# -gt 0 ] && conf_upgrade "$HOME/etc/default/$__name"
    fi
    conf_load_files "${__CONFS_FILES[@]}"
}

function conf_init() {
    # définir les variables attendues lors du chargement des fichiers de
    # configuration par conf_load_files()
    # Si cette fonction n'a pas d'argument, le contenu du tableau CONFIG s'il
    # est existe est utilisé
    # par défaut, les variables sont en mode scalaire: la définition d'une
    # variable écrase la valeur précédente. Avec l'option -a les variables sont
    # en mode tableau: les nouvelles valeurs sont rajoutées à la fin du tableau.
    # Avec l'option -p les variables sont en mode chemin: les nouvelles valeurs
    # sont ajoutées si elles n'existe pas déjà avec le séparateur ':'
    # dans l'exemple suivant:
    #     conf_init NAME VALUE -a SRCDIRS DESTDIRS -p LIBPATH
    # NAME et VALUE sont scalaires, SRCDIRS et DESTDIRS sont des tableaux et
    # LIBPATH est de type chemin
    # Les variables scalaires et chemin sont initialisées à la valeur vide ou à
    # la valeur spécifiée e.g.:
    #     conf_init VAR=value MYPATH=a:b:c
    # Les variables tableaux sont initialisées à la valeur vide sauf si le nom
    # est suivi de '=' auquel cas la valeur actuelle est gardée, e.g.
    #     VS=(a b c); WS=(x y z)
    #     conf_init -a VS WS=
    #     echo_seta2 VS        # VS=()
    #     echo_seta2 WS        # WS=(x y z)
    # Dans le cas des variables tableaux, la valeur après '=' est toujours
    # ignorée. Pour simplifier la lecture, on peut rajouter une valeur marqueur
    # comme 'current' ou 'actual', e.g
    #     conf_init -a VS=CurrentValues
    # L'option -s permet de revenir au mode scalaire

    # Note: il est possible d'associer une description à chaque variable ainsi
    # qu'un en-tête, ce qui permet de construire le fichier de configuration ou
    # de mettre à jour un fichier existant avec conf_upgrade(). Par exemple, les
    # commandes suivantes:
    #     CONFIG=(
    #         "# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8"
    #         "# configurer l'application"
    #         -s
    #         "NAME=payet//nom de l'administrateur"
    #         "MAIL=admin@host.tld//mail de contact"
    #         -a
    #         "HOSTS//hôtes autorisés à se connecter"
    #     )
    #     conf_init
    # permettent de générer automatiquement le fichier de configuration suivant:
    #     # -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
    #     # configurer l'application
    #
    #     # nom de l'administrateur
    #     #NAME=payet
    #
    #     # mail de contact
    #     #MAIL=admin@host.tld
    #
    #     # hôtes autorisés à se connecter
    #     #HOSTS=()
    __CONF_DESCS=()
    __CONF_ARRAY_VARS=()
    __CONF_PATH_VARS=()
    local __type=scalar __initial=1 __prefix __var __desc
    [ $# -eq 0 ] && is_array CONFIG && set -- "${CONFIG[@]}"
    while [ $# -gt 0 ]; do
        if [ -n "$__initial" ]; then
            if [ "${1:0:1}" == "#" ]; then
                [ ${#__prefix} -gt 0 ] && __prefix="$__prefix"$'\n'
                __prefix="$__prefix$1"
                shift
                continue
            else
                [ -n "$__prefix" ] && array_add __CONF_DESCS "$__prefix"
                __initial=
            fi
        fi
        case "$1" in
        -a|--array) __type=array;;
        -p|--path) __type=path;;
        -s|--scalar) __type=scalar;;
        *)
            array_add __CONF_DESCS "$1"
            splitfsep "$1" // __var __desc
            case "$__type" in
            array)
                [ "${__var%%=*}" == "$__var" ] && eval "${__var%%=*}=()"
                array_addu __CONF_ARRAY_VARS "${__var%%=*}"
                array_del __CONF_PATH_VARS "${__var%%=*}"
                ;;
            path)
                setv "$__var"
                array_addu __CONF_PATH_VARS "${__var%%=*}"
                array_del __CONF_ARRAY_VARS "${__var%%=*}"
                ;;
            scalar)
                setv "$__var"
                ;;
            esac
            ;;
        esac
        shift
    done
}

function conf_load() {
    # charger les fichiers de configuration spécifiés
    #     conf_load SPECS...
    # est équivalent à:
    #     conf_find_files __CONF_FILES SPECS...
    #     conf_load_files "${__CONF_FILES[@]}"
    local -a __CONF_FILES
    conf_find_files __CONFS_FILES "$@"
    conf_load_files "${__CONFS_FILES[@]}"
}

function conf_find_files() {
    # initialiser le tableau $1 avec les fichiers de configuration correspondant
    # aux arguments $2..@
    # - si on spécifie un fichier, il est pris tel quel s'il existe
    # - si on spécifie un répertoire, tous les fichiers *.conf de ce répertoire
    #   sont pris
    # - si on spécifie un pattern e.g path/to/*.conf alors tous les fichiers
    #   correspondant au pattern sont pris
    # - sinon l'argument est ignoré
    local __dest="$1"; shift
    local -a __files
    local __spec __dir __wc
    array_new "$__dest"
    for __spec in "$@"; do
        if [ -f "$__spec" ]; then
            array_add "$__dest" "$__spec"
            continue
        elif [ -d "$__spec" ]; then
            __spec="$__spec/*.conf"
        fi
        splitwcs "$__spec" __dir __wc
        array_lsfiles __files "${__dir:-.}" "$__wc"
        array_extend "$__dest" __files
    done
}

function conf_load_files() {
    # sourcer les fichiers spécifiés en faisant ce qui est nécessaire pour que
    # les variables de __CONF_ARRAY_VARS soient correctement traitées.
    local -a __backups __values
    local __file __name __i __backup __bn __bv
    for __file in "$@"; do
        # faire une copie de sauvegarde puis supprimer les variables tableaux
        __backups=()
        for __name in "${__CONF_ARRAY_VARS[@]}" "${__CONF_PATH_VARS[@]}"; do
            __backup="$(declare -p "$__name" 2>/dev/null)"
            if [ -z "$__backup" ]; then
                __backup="$__name="
            else
                # faire une correction de l'expression parce que la commande
                # affichée par declare -p est différente entre bash 4.3 et bash
                # 4.4 pour les tableaux. soit le tableau array=(a b)
                # - bash 4.3 affiche      declare -a array='([0]="a" [1]="b")'
                # - bash 4.4 affiche      declare -a array=([0]="a" [1]="b")
                __backup="${__backup#declare }"
                __bn="${__backup%% *}"
                __bv="${__backup#* }"
                if [[ "$__bn" == -*a* ]]; then
                    __bn="${__bv%%=*}"
                    __bv="${__bv#*=}"
                    if [ "${__bv:0:2}" == "'(" -a "${__bv: -2:2}" == ")'" ]; then
                        __backup="$__bn=$(eval "echo $__bv")"
                    else
                        __backup="$__bn=$__bv"
                    fi
                else
                    __backup="$__bv"
                fi
            fi
            __backups=("${__backups[@]}" "$__backup")
            unset "$__name"
        done
        # charger le fichier
        source "$__file"
        # puis restaurer les variables ou les fusionner avec une éventuelle nouvelle valeur
        __i=0
        for __name in "${__CONF_ARRAY_VARS[@]}" "${__CONF_PATH_VARS[@]}"; do
            __backup="${__backups[$__i]}"
            if [ -n "$(declare -p "$__name" 2>/dev/null)" ]; then
                # la variable a été redéfinie, la fusionner avec la précédente valeur
                if array_contains __CONF_ARRAY_VARS "$__name"; then
                    array_copy __values "$__name"
                    eval "$__backup"
                    array_extend "$__name" __values
                elif array_contains __CONF_PATH_VARS "$__name"; then
                    __values="${!__name}"
                    eval "$__backup"
                    uaddpath "$__values" "$__name"
                fi
            else
                # la variable n'a pas été redéfinie, restaurer la précédente valeur
                eval "$__backup"
            fi
            __i=$(($__i + 1))
        done
    done
}

CONF_INSTALL_ASK_DEFAULT=
function conf_install() {
    # USAGE: conf_install DEST PREFIX SRCS...
    # installer les fichiers de SRCS dans le répertoire standardisé DEST avec le
    # préfixe PREFIX
    # ## destination
    # - si DEST est un nom sans chemin, e.g NAME, alors la destination est
    #   ~/etc/NAME.d
    # - si DEST est un nom avec chemin, alors la valeur est prise telle quelle
    #   comme destination, et le répertoire est créé le cas échéant.
    # Si un fichier existe déjà dans la destination, afficher une demande de
    # confirmation avant de l'écraser
    # ## source
    # - si SRC est un fichier, le prendre tel quel
    # - si SRC est un répertoire, prendre tous les fichiers SRC/*.conf
    # - si SRC est un pattern, prendre tous les fichiers correspondant
    # s'il n'y a qu'une seule source, la destination sera DEST/PREFIX.conf
    # sinon, la destination sera DEST/PREFIX-SRCNAME où SRCNAME est le nom du
    # fichier source
    # si PREFIX est vide, alors les fichiers sont copiés avec leur nom sans
    # modification.
    local -a tmpsrcs srcs
    local src dir wc
    local dest="$1"; shift
    local prefix="$1"; shift
    [[ "$dest" == */* ]] || dest="$HOME/etc/$dest.d"
    mkdir -p "$dest" || return 1
    for src in "$@"; do
        if [ -f "$src" ]; then
            array_add srcs "$src"
        elif [ -d "$src" ]; then
            array_lsfiles tmpsrcs "$src" "*.conf"
            array_extend srcs tmpsrcs
        else
            splitwcs "$src" dir wc
            array_lsfiles tmpsrcs "$dir" "$wc"
            array_extend srcs tmpsrcs
        fi
    done
    [ ${#srcs[*]} -gt 0 ] || return 0
    local COPY_UPDATE_ASK_DEFAULT="${CONF_INSTALL_ASK_DEFAULT:-$COPY_UPDATE_ASK_DEFAULT}"
    if [ -n "$prefix" ]; then
        if [ ${#srcs[*]} -eq 1 ]; then
            copy_update_ask -y "$src" "$dest/$prefix.conf"
        else
            for src in "${srcs[@]}"; do
                copy_update_ask -y "$src" "$dest/$prefix-$(basename -- "$srcname")"
            done
        fi
    else
        for src in "${srcs[@]}"; do
            copy_update_ask -y "$src" "$dest"
        done
    fi
}

function conf_upgrade() {
    # USAGE: conf_upgrade DEST [VARS...]
    # Si les variables VARS... sont spécifiées, on appelle au préalable
    # conf_init()
    local __dest="$1"; shift
    local __desc __namevalue __name __value

    [ $# -gt 0 ] && conf_init "$@"
    # calculer le préfixe et initialiser le fichier le cas échéant
    if [ ! -f "$__dest" ]; then
        local __prefix
        for __desc in "${__CONF_DESCS[@]}"; do
            [ "${__desc:0:1}" == "#" ] && __prefix="$__desc"
            break
        done
        [ ${#__prefix} -gt 0 ] || __prefix="# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8"
        echo "$__prefix" >"$__dest"
    fi
    # vérifier la présence de chaque variable
    for __desc in "${__CONF_DESCS[@]}"; do
        [ "${__desc:0:1}" == "#" ] && continue
        splitfsep "$__desc" // __namevalue __desc
        splitvar "$__namevalue" __name __value
        if ! grep -qE "^\s*#*(\s*export)?\s*$__name=" "$__dest"; then
            echo >>"$__dest"
            [ -n "$__desc" ] && echo "# $__desc" >>"$__dest"
            echo -n "#" >>"$__dest"
            if array_contains __CONF_ARRAY_VARS "$__name"; then
                echo_seta2 "$__name" >>"$__dest"
            else
                echo_setv "$__name" "$__value" >>"$__dest"
            fi
        fi
    done
}

function conf_update() {
    # USAGE: conf_update [-n] DEST VARS...
    # Pour chaque variable mentionnée, le fichier DEST est mis à jour avec sa
    # valeur actuelle. Le fichier doit exister, et conf_init() *doit* avoir été
    # appelé au préalable.
    # Avec l'option -n, ne pas modifier l'état activé/désactivé des variables
    # dans le fichier de configuration. En d'autres termes, si la variable était
    # commentée dans le fichier, la laisser commentée, mais mettre quand même à
    # jour la valeur
    local enable=1
    if [ "$1" == -n ]; then
        enable=
        shift
    fi
    local dest="$1"; shift
    [ -f "$dest" ] || return 1
    local from to
    ac_set_tmpfile destf
    ac_set_tmpfile destt
    cat "$dest" >"$destf"

    local name setvar
    for name in "$@"; do
        if array_contains __CONF_ARRAY_VARS "$name"; then
            setx setvar=echo_seta2 "$name"
        else
            setx setvar=echo_setv2 "$name"
        fi
        awkrun <"$destf" >"$destt" name="$name" setvar="$setvar" enable:int="$enable" '
$0 ~ "^\\s*#*(\\s*export)?\\s*" name "=" {
  match($0, "^(\\s*#*)((\\s*export)?\\s*)" name "=", vs)
  if (enable) print vs[2] setvar
  else print vs[1] vs[2] setvar
  next
}
{ print }'
        cat "$destt" >"$destf"
    done
    cat "$destf" >"$dest"
    ac_clean "$destf" "$destt"
    return 0
}