#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname "$0")/lib/ulib/ulib" || exit 1
urequire DEFAULTS multiconf

function display_help() {
    uecho "$scriptname: classer des fichiers selon certains règles

Les règles sont spécifiées dans le fichier ~/etc/default/ufile

Dans ce fichier, deux tableaux contiennent les règles applicables:
* le tableau RULES contient des règles de la forme pattern:destdir[:renamef] et
  permet de classer des fichiers correspondant à des patterns
* le tableau NRULES contient des règles de fle forme name:destdir[:renamef] et
  permet de classer des fichiers quelconques en spécifiant la règle à utiliser

Les champs sont:
* name est un nom quelconque, utilisé avec l'option --name
* pattern est au format glob et identifie les fichiers auxquels s'applique la
  règle, sauf si la chaine commence par / auquel cas il s'agit d'une expression
  régulière reconnue par awk. Par exemple, les deux patterns suivants sont
  équivalents:
    [ab]cd*.pdf
    /[ab]cd.*\\\\.pdf\$
* destdir est le répertoire de destination dans lequel classer le fichier
* renamef est une fonction qui permet de supporter le renommage d'un fichier
  lors de son classement. Sa signature est 'renamef filename pf destdir' où
  filename est le nom du fichier source, pf son chemin complet et destdir la
  valeur de destdir mentionnée dans la règle.
  La fonction doit définir la variable dest qui est le nouveau nom. Si la
  fonction retourne un code d'erreur autre que zéro, la règle est ignorée.
  Si le nouveau nom contient un chemin, destdir est ignoré et le fichier est
  déplacé dans le répertoire spécifié avec le nom spécifié. Si dest est un
  tableau avec plusieurs destinations, alors le fichier est copié en plusieurs
  fois.
  Si dest est de la forme [user@]host:path alors le fichier est copié par scp
  sur l'hôte spécifié vers la destination spécifiée, sauf si l'hôte courant est
  déjà celui mentionné dans la valeur, auquel cas la copie est faite directement
  dans le répertoire spécifié.
  Si le user et l'hôte courant sont déjà à la valeur spécifiée, alors la copie
  est faite en local sans utiliser scp sauf si l'option --force-scp est utilisée
  Le chemin spécifié, en local ou distant, est toujours le chemin complet vers
  le fichier destination. Si on veut copier le fichier sans le renommer vers un
  répertoire, il faut mettre un slash e.g destdir/ ou user@host:destdir/
  variables pouvant être définies mais non documentées: interaction(=-i)

USAGE
    $scriptname [options] <files...>
    $scriptname [options] -r <files|dirs...>

ACTIONS
    --file
        Classer les fichiers spécifiés. C'est l'action par défaut
    -l, --list
        Lister les règles définies
    -e, --edit
        Lancer un éditeur sur le fichier de configuration

OPTIONS
    -c, --config CONFIG
        Utiliser le fichier de configuration spécifié au lieu de la valeur par
        défaut ~/etc/default/ufile et ~/etc/ufile.d/*.conf
    -C, --other-configs
        Charger les fichiers ~/etc/ufile.d/*.conf en plus du fichier spécifié
        avec --config. Cette option est ignorée si --config n'est pas utilisé.
    -j, --nrule NAME
        Spécifier une règle du tableau NRULES à utiliser pour classer les
        fichiers spécifiés. Cette option peut être spécifiée autant de fois que
        nécessaire.
        Par défaut, seul le tableau RULES est utilisé pour trouver la règle à
        appliquer. Avec cette option, seul le tableau NRULES est utilisé.
    -v, --var NAME=VALUE
        Définir une variable qui sera utilisée par la fonction renamef. Cette
        option peut être spécifiée autant de fois que nécessaire.
        Les noms commençant par _ sont réservés et ne peuvent pas être définis.
    --force-cp
        Spécifier le mode de classement des fichiers. Par défaut, le fichier est
        déplacé dans la destination s'il s'agit d'un classement local, ou copié
        s'il s'agit d'un classement distant. Avec --force-cp, le fichier est
        systématiquement copié dans la destination.
    -m, --local-only
        Ignorer les classements qui auraient pour conséquence de copier le
        fichier sur un hôte distant. Ne traiter que les classement locaux. Cela
        s'applique aussi aux classements distants qui désignent l'hôte courant.
    -S, --ssh SSH
        S'il faut classer sur un hôte distant avec scp, utiliser le programme
        spécifié pour la connexion par ssh
    --force-scp
        Toujours utiliser scp pour une copie distante. Par défaut s'il est
        déterminé que l'hôte distant est en réalité l'hôte courant, alors la
        copie est effectuée directement.
    -f, --force
        Si le fichier destination existe, alors l'écraser sans confirmation.
        Cette option est ignorée pour un classement distant.
    -r, --recursive
        Classer récursivement tous les fichiers d'un répertoire. Sans cette
        option, il n'est pas autorisé de fournir un répertoire comme argument.
    -n, --fake
        Afficher les opérations qui seraient faites"
}

function joinp() {
    # afficher le chemin $1/$2
    local pf="$1"
    [ -n "$2" -a "${pf%/}" == "$pf" ] && pf="$pf/"
    pf="$pf${2#/}"
    echo "$pf"
}

function __check_destdir() {
    local destdir="$1" rule="$2"
    if [ -z "$destdir" ]; then
        eerror "$rule: règle invalide: destdir est vide"
        return 1
    fi
    return 0
}
function __set_dest() {
    local dest="$1" destdir="$2" filename="$3" force_scp="$4"
    local userhost remotedir destname
    if [[ "$dest" == *:* ]]; then
        splitpair "$dest" userhost remotedir
        if [ -z "$force_scp" ] && check_userhostname "$userhost"; then
            # on est déjà avec le bon user sur le bon hôte
            if [ -n "$remotedir" ]; then
                splitpath "$remotedir" destdir destname
                setx destdir=abspath "$destdir" "$HOME"
                [ -n "$destname" ] || destname="$filename"
                setx dest=joinp "$destdir" "$destname"
            else
                setx dest=joinp "$HOME" "$filename"
            fi
        fi
    elif [[ "$dest" == */* ]]; then
        splitpath "$dest" destdir destname
        [ -n "$destname" ] || destname="$filename"
        setx dest=joinp "$destdir" "$destname"
        setx dest=abspath "$dest"
    else
        __check_destdir "$destdir" "$rule" || return 1
        setx dest=joinp "$destdir" "$dest"
    fi
    upvar dest "$dest"
    return 0
}

function define_vars() {
    local _name _value
    for _name in "$@"; do
        splitvar "$_name" _name _value
        if [[ "$_name" == _* ]]; then
            ewarn "$_name: cette variable ne peut être définie"
        else
            setv "$_name" "$_value"
        fi
    done
}

args=(%
    --help '$exit_with display_help'
    -c:,--config: config=
    -C,--other-configs other_configs=1
    --file action=file
    -j:,--nrule: _nrules
    -v:,--var: _vars
    --force-cp force_cp=1
    -m,--local-only local_only=1
    -S:,--ssh: SSH=
    --force-scp force_scp=1
    -f,--force force=1
    -r,--recursive recursive=1
    -n,--fake fake=1
    -l,--list action=list
    -e,--edit action=edit
)
parse_args "$@"; set -- "${args[@]}"

[ -n "$action" ] || action=file

## charger toutes les règles

conf_init -a RULES NRULES
if [ -n "$config" ]; then
    if [ "$action" != edit ]; then
        # le fichier doit exister, sauf en mode édition où il sera créé s'il
        # n'existe pas déjà
        [ -f "$config" ] || die "$config: fichier introuvable"
    fi
    if [ -f "$config" ]; then
        source "$config" || die "$config: erreur lors de la lecture du fichier"
    fi
    [ -n "$other_configs" ] && conf_load "$HOME/etc/ufile.d/*.conf"
else
    set_defaults ufile
    conf_load "$HOME/etc/ufile.d/*.conf"
fi

## actions particulières

if [ "$action" == list ]; then
    echo "# RULES"
    array_to_lines RULES
    echo "# NRULES"
    array_to_lines NRULES
    exit 0
elif [ "$action" == edit ]; then
    [ -n "$config" ] || setx config=get_user_defaults_file ufile
    if [ ! -f "$config" ]; then
        einfo "Le fichier $(ppath "$config") n'existe pas. Il sera créé avec un contenu par défaut"
        mkdirof "$config" || die
        cp "$scriptdir/lib/default/ufile" "$config"
    fi
    "${EDITOR:-vi}" "$config"
    exit $?
elif [ "$action" != file ]; then
    die "bug: $action: action non implémentée"
fi

## classement des fichiers

if [ -n "$fake" ]; then
    function docmd() { qvals "$@"; }
else
    function docmd() { "$@"; }
fi

if [ ${#_nrules[*]} -gt 0 ]; then
    array_fix_paths _nrules ,
    array_copy _rules NRULES
    _nrule=1
else
    array_copy _rules RULES
    _nrule=
fi

[ $# -gt 0 ] || die "Vous devez spécifier des fichiers à classer"
[ ${#_rules[*]} -gt 0 ] || die "Il faut définir des règles ${_nrule:+N}RULES dans ~/etc/default/ufile ou ~/etc/ufile.d/*.conf"

# vérifier les règles
for rule in "${_rules[@]}"; do
    splitpair "$rule" pattern r2
    splitpair "$r2" destdir r3
    splitpair "$r3" renamef r4
    if [ -z "$destdir" -o "${destdir#"~/"}" != "$destdir" ]; then
        :
    elif [ "${destdir#/}" == "$destdir" ]; then
        ewarn "$rule: règle potentiellement problématique: destdir devrait être absolu"
    fi
done

# faire la liste des fichiers
_files=()
for file in "$@"; do
    if [ -d "$file" -a -n "$recursive" ]; then
        setx file=abspath "$file"
        array_from_lines rfiles "$(find "$file" -type f)"
        array_extendu _files rfiles
    elif [ -f "$file" ]; then
        setx file=abspath "$file"
        array_addu _files "$file"
    elif [ -n "$fake" ]; then
        : # on est en mode fake, pas grave si le fichier n'est pas trouvé
    elif [ -d "$file" ]; then
        eerror "$file: est un répertoire. essayez avec -r"
    else
        eerror "$file: fichier introuvable. il sera ignoré"
    fi
done

# faire le classement effectif
r=
for file in "${_files[@]}"; do
    setx pf=abspath "$file"
    setx dir=dirname -- "$pf"
    setx filename=basename -- "$pf"
    found=
    for rule in "${_rules[@]}"; do
        splitpair "$rule" pattern r2
        splitpair "$r2" odestdir r3
        splitpair "$r3" renamef r4
        if [ "${odestdir#"~/"}" != "$odestdir" ]; then
            odestdir="$HOME/${odestdir#"~/"}"
        elif [ "$odestdir" == "~" ]; then
            odestdir="$HOME"
        fi

        if [ -n "$_nrule" ]; then
            array_contains _nrules "$pattern" || continue
        else
            if [ "${pattern#/}" != "$pattern" ]; then
                awk -v filename="$filename" -v pattern="${pattern#/}" 'BEGIN { exit(filename ~ pattern? 0: 1) }' || continue
            else
                eval "[[ \"\$filename\" == $(qwc "$pattern") ]]" || continue
            fi
        fi

        unset dest
        interaction=--DEFAULT--
        if [ -n "$renamef" ]; then
            # protéger les variables nécessaires au lancement de renamef
            _renamef="$renamef"
            _filename="$filename"
            _pf="$pf"
            _odestdir="$odestdir"
            define_vars "${_vars[@]}"
            "$_renamef" "$_filename" "$_pf" "$_odestdir" || continue
        fi
        if is_array dest; then
            array_copy tmpdests dest
            dests=()
            for dest in "${tmpdests[@]}"; do
                __set_dest "$dest" "$odestdir" "$filename" "$force_scp" || break
                array_add dests "$dest"
            done
        elif is_defined dest; then
            __set_dest "$dest" "$odestdir" "$filename" "$force_scp" || break
            dests=("$dest")
        else
            __check_destdir "$odestdir" "$rule" || break
            setx dest=joinp "$odestdir" "$filename"
            dests=("$dest")
        fi

        i=1
        mvi=${#dests[*]}
        [ -z "$force" ] && mvint=-i || mvint=
        for dest in "${dests[@]}"; do
            if [[ "$dest" == *:* ]]; then
                if [ -n "$local_only" ]; then
                    einfo "$dest: destination ignorée à cause du mode local uniquement"
                    continue
                else
                    [ "$interaction" == --DEFAULT-- ] && int= || int="$interaction"
                    estep "$filename --> $dest"
                    ask_yesno $int "Voulez-vous continuer?" O || {
                        r=1; found=x; break
                    }
                    docmd scp ${SSH:+-S "$SSH"} "$file" "$dest" || die "problème lors de la copie du fichier"
                fi

            else
                [ "$interaction" == --DEFAULT-- ] && int=-i || int="$interaction"
                estep "$filename --> $dest"
                ask_yesno $int "Voulez-vous continuer?" O || {
                    r=1; found=x; break
                }
                setx destdir=dirname -- "$dest"
                docmd mkdir -p "$destdir" || die "$destdir: impossible de créer le répertoire"
                if [ $i -eq $mvi -a -z "$force_cp" ]; then
                    mvdesc="du déplacement"
                    mvcmd=mv
                else
                    mvdesc="de la copie"
                    mvcmd=cp
                fi
                docmd "$mvcmd" $mvint "$file" "$dest" || die "problème lors $mvdesc du fichier"
            fi
            i=$(($i + 1))
        done

        [ -n "$found" ] || found=1
        break
    done

    if [ -z "$found" ]; then
        ewarn "$file: aucune correspondance n'a été trouvée"
    fi
done

[ -n "$r" ] || r=0
exit $r