<template>
    <div class="ie-conv-function">
        <!-- Select + aide sur une meme ligne : l'icone reste a DROITE du selecteur. -->
        <div class="ie-conv-fn-row">
            <select class="ie-conv-fn ie-select" :value="value.fn" @change="changeFn($event.target.value)">
                <option value="" disabled>{{ messages.get('importexport', 'ie_conv_select_function') }}</option>
                <option v-for="f in sortedFunctions" :key="f.key" :value="f.key">{{ f.label }}</option>
            </select>
            <!-- Aide fonction : icone a tooltip (affichee seulement si une description existe). -->
            <i v-if="currentDef && currentDef.description"
                class="fa fa-info-circle ie-conv-help" :title="currentDef.description"
                :aria-label="currentDef.description"></i>
            <!-- Apercu : test de la fonction, porte ICI (ligne du selecteur) pour
                 rester pres des controles plutot qu'apres tout le bloc d'arguments. -->
            <div v-if="previewActive" class="ie-conv-preview">
                <button type="button" class="bouton ie-conv-preview-btn" :disabled="previewLoading"
                    :title="messages.get('importexport', 'ie_conv_preview_test')" @click="testExpression()">{{ messages.get("importexport", "ie_conv_test_button")}}</button>
                <span v-if="previewError" class="ie-conv-preview-error" :title="previewError">{{ previewError }}</span>
                <span v-else-if="previewDone" class="ie-conv-preview-result"
                    :class="{ 'ie-conv-preview-empty': previewResult === '' }"
                    :title="previewResult">{{ previewResult === "" ? messages.get("importexport", "ie_conv_preview_empty") : previewResult }}</span>
            </div>
        </div>
        <div class="ie-conv-args" v-if="currentDef && currentDef.args.length">
            <div class="ie-conv-arg" v-for="arg in currentDef.args" :key="arg.key">
                <label class="ie-conv-arg-label">
                    {{ arg.label }}
                    <!-- Aide parametre : icone a tooltip (si description definie). -->
                    <i v-if="arg.description" class="fa fa-info-circle ie-conv-help"
                        :title="arg.description" :aria-label="arg.description"></i>
                </label>
                <value-editor
                    :value="argExpr(arg)"
                    :functions-catalog="functionsCatalog"
                    :data-defs="dataDefs"
                    :source-options="sourceOptions"
                    :source-tree="sourceTree"
                    :iteration-nodes="iterationNodes"
                    :depth="depth + 1"
                    :eval-fn="evalFn"
                    :preview-active="previewActive"
                    @input="setArg(arg.key, $event)">
                </value-editor>
            </div>
        </div>
    </div>
</template>

<script>
/**
 * Editeur d'un appel de fonction (expression "calc").
 *
 * Affiche le selecteur de fonction (libelles issus du catalogue) puis, pour
 * chaque argument de la fonction choisie, un value-editor (recursion). Les
 * libelles d'arguments proviennent du catalogue (donc du fichier de messages).
 */
export default {
    name: "FunctionEditor",
    props: {
        value: { type: Object, default: () => ({ kind: "calc", fn: "", args: {} }) },
        functionsCatalog: { type: Array, default: () => [] },
        dataDefs: { type: Array, default: () => [] },
        sourceOptions: { type: Array, default: () => [] },
        /** Arbre source, propage aux value-editor des arguments. */
        sourceTree: { type: Array, default: () => [] },
        /** Noeuds d'iteration en scope, propages aux value-editor des arguments. */
        iterationNodes: { type: Array, default: () => [] },
        depth: { type: Number, default: 0 },
        /** Fonction d'evaluation injectee, propagee aux arguments. */
        evalFn: { type: Function, default: null },
        /** Un echantillon est-il charge (test a la demande des arguments) ? */
        previewActive: { type: Boolean, default: false }
    },
    data() {
        return {
            previewLoading: false,
            previewDone: false,
            previewResult: "",
            previewError: ""
        };
    },
    watch: {
        // Reinitialise l'affichage du resultat des que la fonction change (fonction
        // selectionnee ou argument edite) : un resultat affiche serait perime.
        value() {
            this.previewLoading = false;
            this.previewDone = false;
            this.previewResult = "";
            this.previewError = "";
        }
    },
    computed: {
        /**
         * Catalogue de fonctions a afficher. Le tri par libelle est fait UNE SEULE
         * fois a l'initialisation (cf. conversions.js), le catalogue etant partage
         * et immuable : on le renvoie tel quel, sans re-trier par instance.
         *
         * @returns {Array}
         */
        sortedFunctions() {
            return this.functionsCatalog;
        },
        /**
         * Descripteur de la fonction couramment selectionnee (clef, libelle,
         * arguments), ou null.
         */
        currentDef() {
            return this.functionsCatalog.find((f) => f.key === this.value.fn) || null;
        }
    },
    methods: {
        /**
         * Expression courante d'un argument. Si l'argument n'est pas encore
         * renseigne, defaut selon son TYPE declare : "table" => editeur de table
         * (kind "table") ; sinon valeur fixe vide.
         *
         * @param {{key: string, type?: string}} arg Descripteur d'argument.
         */
        argExpr(arg) {
            const existing = this.value.args && this.value.args[arg.key];
            if (existing) {
                return existing;
            }
            if (arg.type === "table") {
                return { kind: "table", entries: [] };
            }
            return { kind: "fixed", value: "" };
        },
        /**
         * Met a jour l'expression d'un argument et emet la fonction modifiee.
         */
        setArg(key, expr) {
            const args = Object.assign({}, this.value.args || {}, { [key]: expr });
            this.$emit("input", Object.assign({}, this.value, { args }));
        },
        /**
         * Change la fonction selectionnee (les arguments sont reinitialises).
         */
        changeFn(fn) {
            this.$emit("input", { kind: "calc", fn: fn, args: {} });
        },
        /**
         * Teste la fonction courante (this.value) sur l'echantillon actif via la
         * fonction d'evaluation injectee. Seule cette expression est evaluee.
         */
        async testExpression() {
            if (typeof this.evalFn !== "function") {
                return;
            }
            this.previewLoading = true;
            this.previewError = "";
            this.previewDone = false;
            try {
                const res = await this.evalFn(this.value);
                if (res && res.ok) {
                    this.previewResult = this.flattenResult(res.results);
                    this.previewDone = true;
                } else {
                    this.previewError = (res && res.error)
                        ? res.error
                        : this.messages.get("importexport", "ie_conv_preview_error");
                }
            } catch (e) {
                this.previewError = this.messages.get("importexport", "ie_conv_preview_error");
            } finally {
                this.previewLoading = false;
            }
        },
        /**
         * Aplati un resultat d'evaluation en chaine affichable (resultat iteratif
         * rendu element par element).
         */
        flattenResult(node) {
            if (node && Array.isArray(node.items)) {
                return node.items
                    .map((it) => "[" + it.position + "] " + this.flattenResult(it.result))
                    .join("   ");
            }
            return node && typeof node.display !== "undefined" ? node.display : "";
        }
    }
};
</script>
