<template>
    <div class="versions">
        <h3>Versions</h3>
        <version
            v-for="version in versions"
            :key="version.id"
            :version="version"
            :open="containsUsedVersion(version)"
            @view-changes="viewChanges"
            @export-version="exportVersion"
            @create-version="openCreateVersionModal"
            @delete-version="openDeleteVersionModal"
            @use-version="openUseVersionModal">
        </version>

        <changesView ref="changesView" />

        <formModal class="form-hidden" ref="useVersionModal" :title="titleUseModal">
            <template>
                <div class="thesaurus-use-bar-wrap">
                    <div class="thesaurus-use-bar-label">{{ messages.get('thesaurus_version', 'use_progress') }}</div>
                    <div class="thesaurus-use-bar-track">
                        <div class="thesaurus-use-bar" :style="{width: useBarWidth + '%'}"></div>
                    </div>
                </div>
                <div :class="['thesaurus-use-msg', {'thesaurus-use-msg--error': useForm.error}]">
                    {{ useStatusMessage }}
                </div>
            </template>
            <template #footer>
                <button type="button" class="bouton" :disabled="!useForm.done && !useForm.error" @click="closeUseModal">
                    {{ messages.get('common', 'close') }}
                </button>
            </template>
        </formModal>

        <formModal class="form-hidden" ref="createVersionModal" :title="titleCreateVersionModal" @close="closeCreateVersionModal" @submit="submitCreateVersionModal">
            <template>
                <div class="row">
                    <label class="etiquette" for="new_version_name">
                        {{ messages.get('thesaurus_version', 'version_name') }}
                    </label>
                </div>
                <div class="row">
                    <input
                        type="text"
                        id="new_version_name"
                        class="saisie-30em"
                        v-model.trim="createVersionForm.name"
                        required
                        aria-required="true"
                        autofocus
                    />
                </div>
            </template>
            <template #footer>
                <button type="submit" class="bouton">
                    {{ createVersionForm.version && createVersionForm.version.isRoot ? messages.get('thesaurus_version', 'create_main_version') : messages.get('thesaurus_version', 'create_version') }}
                </button>
            </template>
        </formModal>

        <formModal class="form-hidden" ref="deleteVersionModal" :title="titleDeleteVersionModal" @close="closeDeleteVersionModal" @submit="confirmDeleteVersionModal">
            <template>
                <p>{{ messages.get('thesaurus_version', 'delete_version_confirm') }}</p>
            </template>
            <template #footer>
                <button type="submit" class="bouton btnDelete">
                    {{ messages.get('common', 'remove') }}
                </button>
            </template>
        </formModal>

        <formModal class="form-hidden" ref="exportVersionModal" :title="titleExportModal" @close="closeExportModal" @submit="submitExportModal">
            <template>
                <fieldset>
                    <legend>{{ messages.get('thesaurus_version', 'export_choice_label') }}</legend>
                    <div class="row">
                        <input type="radio" name="export_action" value="1" id="export_thesaurus" required aria-required="true" v-model.number="exportForm.action" />
                        <label class="etiquette" for="export_thesaurus">{{ messages.get('thesaurus_version', 'version_export_thesaurus') }}</label>
                    </div>
                    <div class="row">
                        <input type="radio" name="export_action" value="2" id="export_update" required aria-required="true" v-model.number="exportForm.action" :disabled="exportForm.version ? exportForm.version.isRoot : false" />
                        <label class="etiquette" for="export_update">{{ messages.get('thesaurus_version', 'version_export_update') }}</label>
                    </div>
                    <div class="row">
                        <input type="radio" name="export_action" value="3" id="export_update_txt" required aria-required="true" v-model.number="exportForm.action" :disabled="exportForm.version ? exportForm.version.isRoot : false" />
                        <label class="etiquette" for="export_update_txt">{{ messages.get('thesaurus_version', 'version_export_update_txt') }}</label>
                    </div>
                </fieldset>

                <div aria-live="polite">
                    <template v-if="(exportForm.action === 2 || exportForm.action === 3) && exportForm.ancestors.length > 0">
                        <div class="row">
                            <label class="etiquette" for="export_version_from">
                                {{ messages.get('thesaurus_version', 'version_export_version_from') }}
                            </label>
                        </div>
                        <div class="row">
                            <select id="export_version_from" v-model="exportForm.versionFrom" required aria-required="true">
                                <option v-for="ancestor in exportForm.ancestors" :key="ancestor.id" :value="ancestor.id">
                                    {{ ancestor.isRoot ? ancestor.name : ancestor.revision + ' ' + ancestor.name }}
                                </option>
                            </select>
                        </div>
                    </template>
                </div>

            </template>
            <template #footer>
                <button type="submit" class="bouton">
                    {{ messages.get('common', 'export') }}
                </button>
            </template>
        </formModal>
    </div>
</template>

<script>
import version from './version.vue'
import formModal from '@/common/components/FormModal.vue'
import changesView from './changesView.vue'

/**
 * @module versions
 * @description Composant conteneur qui affiche l'arbre complet des versions du thesaurus
 *              et gere toutes les modales : journal des modifications, creation de version,
 *              suppression de version et export.
 */
export default {
    name: 'versions',
    props: {
        /**
         * Tableau des noeuds de version racine a afficher.
         * Chaque noeud peut contenir un tableau subVersions pour le rendu recursif.
         * @type {Array<Object>}
         */
        versions: {
            type: Array,
            required: false,
            default: () => []
        }
    },
    data() {
        return {
            /**
             * Etat du formulaire de la modale d'export.
             * @type {{ version: Object|null, action: number, versionFrom: number|null, ancestors: Array<Object> }}
             */
            exportForm: {
                version: null,
                action: 1,
                versionFrom: null,
                ancestors: [],
            },
            /**
             * Etat du formulaire de la modale de creation de version.
             * @type {{ version: Object|null, name: string }}
             */
            createVersionForm: {
                version: null,
                name: '',
            },
            /**
             * Etat du formulaire de la modale de confirmation de suppression.
             * @type {{ version: Object|null }}
             */
            deleteVersionForm: {
                version: null,
            },
            /**
             * Etat de la modale de progression de restauration.
             * @type {{
             *   version: Object|null,
             *   thesaurusId: number,
             *   phase: string,
             *   versionIds: number[],
             *   total: number,
             *   processed: number,
             *   done: boolean,
             *   error: boolean
             * }}
             */
            useForm: {
                version: null,
                thesaurusId: 0,
                phase: '',
                versionIds: [],
                total: 0,
                processed: 0,
                done: false,
                error: false,
            }
        }
    },
    components: { version, formModal, changesView },
    computed: {
        /**
         * Titre de la modale de creation de version, adapte selon qu'il s'agit
         * d'une version principale (racine) ou d'une sous-version.
         * @returns {string}
         */
        titleCreateVersionModal() {
            if (!this.createVersionForm.version) {
                return '';
            }
            if (this.createVersionForm.version.isRoot) {
                return this.messages.get('thesaurus_version', 'create_main_version_title');
            }
            return this.messages.get('thesaurus_version', 'create_version_title') + ` ${this.createVersionForm.version.revision} ${this.createVersionForm.version.name}`;
        },
        /**
         * Titre de la modale de confirmation de suppression.
         * @returns {string}
         */
        titleDeleteVersionModal() {
            if (!this.deleteVersionForm.version) {
                return '';
            }
            const v = this.deleteVersionForm.version;
            const prefix = v.isRoot ? '' : `${v.revision} `;
            return this.messages.get('thesaurus_version', 'delete_version_title') + prefix + v.name;
        },
        /**
         * Titre de la modale d'export.
         * @returns {string}
         */
        titleExportModal() {
            if (!this.exportForm.version) {
                return '';
            }
            const v = this.exportForm.version;
            const prefix = v.isRoot ? '' : `${v.revision} `;
            return this.messages.get('thesaurus_version', 'version_export_title') + prefix + v.name;
        },
        /**
         * Titre de la modale de progression de restauration.
         * @returns {string}
         */
        titleUseModal() {
            if (!this.useForm.version) {
                return '';
            }
            const v = this.useForm.version;
            const prefix = v.isRoot ? '' : `${v.revision} `;
            return this.messages.get('thesaurus_version', 'use_title') + ' - ' + prefix + v.name;
        },
        /**
         * Largeur de la barre de progression de restauration en pourcentage.
         * @returns {number}
         */
        useBarWidth() {
            if (!this.useForm.total) {
                return this.useForm.done ? 100 : 0;
            }
            return Math.min(100, Math.round((this.useForm.processed / this.useForm.total) * 100));
        },
        /**
         * Message de statut de la restauration.
         * @returns {string}
         */
        useStatusMessage() {
            if (this.useForm.error) {
                return this.messages.get('thesaurus_version', 'use_error');
            }
            if (this.useForm.done) {
                return this.messages.get('thesaurus_version', 'use_done');
            }
            return this.messages.get('thesaurus_version', 'use_progress');
        }
    },
    methods: {
        /**
         * Ouvre la modale du journal pour la version donnee.
         * Sert les donnees depuis changelogCache si disponibles ; sinon charge depuis l'API.
         * @async
         * @param {Object} version - La version dont afficher le journal.
         * @returns {Promise<void>}
         */
        /**
         * Indique si un noeud de version ou l'un de ses descendants est la version utilisee.
         * Utilise pour ouvrir automatiquement l'accordeon jusqu'a la version active.
         * @param {Object} version - Le noeud a tester.
         * @returns {boolean}
         */
        containsUsedVersion(version) {
            if (version.used) {
                return true;
            }
            if (version.subVersions && version.subVersions.length) {
                return version.subVersions.some(sub => this.containsUsedVersion(sub));
            }
            return false;
        },
        /**
         * Delegue l'affichage du journal des modifications au composant changesView.
         * @param {Object} version - La version dont afficher le journal.
         * @returns {void}
         */
        viewChanges(version) {
            this.$refs.changesView.viewChanges(version);
        },
        /**
         * Ouvre la modale de creation de version pre-remplie pour la version parente donnee.
         * Le nom est pre-rempli avec "Version X.Y" calcule depuis le nombre d'enfants existants.
         * @param {Object} version - La version parente sous laquelle creer un enfant.
         * @returns {void}
         */
        openCreateVersionModal(version) {
            const childCount = version.subVersions ? version.subVersions.length : 0;
            let nextRevision;
            if (version.isRoot) {
                nextRevision = (childCount + 1) + '.0';
            } else {
                nextRevision = version.revision.replace(/^V/, '') + '.' + (childCount + 1);
            }
            this.createVersionForm.version = version;
            this.createVersionForm.name = this.messages.get('thesaurus_version', 'version_default_name') + ' ' + nextRevision;
            this.$refs.createVersionModal.show();
        },
        /**
         * Soumet le formulaire de creation, appelle l'API et insere la nouvelle version
         * dans la liste subVersions du parent en verrouillant tous les freres precedents.
         * @async
         * @returns {Promise<void>}
         */
        async submitCreateVersionModal() {
            const version = this.createVersionForm.version;
            const result = await this.ws.post('versions', version.id + '/create', { name: this.createVersionForm.name });
            if (result && !result.error) {
                version.subVersions.forEach(v => { v.locked = true; });
                version.subVersions.push(result);
                this.$refs.createVersionModal.close();
                this.notif.info(this.messages.get('common', 'success_save'));
            } else {
                this.notif.error(this.messages.get('common', 'failed_save'));
            }
        },
        /**
         * Reinitialise l'etat du formulaire de creation apres l'animation de fermeture (500 ms).
         * @returns {void}
         */
        closeCreateVersionModal() {
            setTimeout(() => {
                this.createVersionForm.version = null;
                this.createVersionForm.name = '';
            }, 500);
        },
        /**
         * Ouvre la modale de confirmation de suppression pour la version donnee.
         * @param {Object} version - La version a supprimer.
         * @returns {void}
         */
        openDeleteVersionModal(version) {
            this.deleteVersionForm.version = version;
            this.$refs.deleteVersionModal.show();
        },
        /**
         * Confirme la suppression : appelle l'API, retire le noeud de l'arbre
         * et restaure l'etat de verrouillage correct sur tous les groupes de freres.
         * @async
         * @returns {Promise<void>}
         */
        async confirmDeleteVersionModal() {
            const version = this.deleteVersionForm.version;
            const result = await this.ws.post('versions', version.id + '/delete', {});
            if (result && !result.error) {
                this.removeVersionFromTree(this.versions, version.id);
                this.unlockLatestSiblings(this.versions);
                this.$refs.deleteVersionModal.close();
                this.notif.info(this.messages.get('common', 'success_delete'));
            } else {
                this.notif.error(this.messages.get('common', 'failed_delete'));
            }
        },
        /**
         * Reinitialise l'etat du formulaire de suppression apres l'animation de fermeture (500 ms).
         * @returns {void}
         */
        closeDeleteVersionModal() {
            setTimeout(() => {
                this.deleteVersionForm.version = null;
            }, 500);
        },
        /**
         * Parcourt recursivement l'arbre et s'assure que seul le dernier frere de chaque
         * niveau est deverrouille. Appele apres une suppression pour recalculer les verrous.
         * @param {Array<Object>} versions - Le tableau de versions a parcourir.
         * @returns {void}
         */
        unlockLatestSiblings(versions) {
            if (versions.length > 0) {
                versions.forEach(v => { v.locked = true; });
                versions[versions.length - 1].locked = false;
            }
            versions.forEach(v => {
                if (v.subVersions && v.subVersions.length) {
                    this.unlockLatestSiblings(v.subVersions);
                }
            });
        },
        /**
         * Retire recursivement un noeud de version de l'arbre par son identifiant.
         * @param {Array<Object>} versions  - Le tableau de versions dans lequel chercher.
         * @param {number}        idToRemove - L'identifiant de la version a retirer.
         * @returns {boolean} Vrai si la version a ete trouvee et retiree, faux sinon.
         */
        removeVersionFromTree(versions, idToRemove) {
            for (let i = 0; i < versions.length; i++) {
                if (versions[i].id === idToRemove) {
                    versions.splice(i, 1);
                    return true;
                }
                if (versions[i].subVersions && this.removeVersionFromTree(versions[i].subVersions, idToRemove)) {
                    return true;
                }
            }
            return false;
        },
        /**
         * Ouvre la modale de progression et lance la restauration asynchrone en deux phases.
         *
         * Phase 1 (undo)   : annule les changelogs des versions entre la version courante et le LCA.
         * Phase 2 (replay) : rejoue les changelogs des versions entre le LCA et la version cible.
         * Aucune entree de changelog n'est supprimee ; les deux directions sont prises en charge.
         *
         * @param {Object} version - La version cible.
         * @returns {Promise<void>}
         */
        async openUseVersionModal(version) {
            this.useForm.version     = version;
            this.useForm.thesaurusId = 0;
            this.useForm.phase       = '';
            this.useForm.versionIds  = [];
            this.useForm.processed   = 0;
            this.useForm.total       = 0;
            this.useForm.done        = false;
            this.useForm.error       = false;
            this.$refs.useVersionModal.show();

            try {
                const start = await this.ws.post('versions', version.id + '/use', { step: 'start' });
                if (!start || start.error) {
                    this.useForm.error = true;
                    return;
                }

                this.useForm.thesaurusId = start.thesaurusId;
                this.useForm.phase       = start.phase;
                this.useForm.versionIds  = start.versionIds;
                this.useForm.total       = start.total;

                if (start.versionIds.length > 0) {
                    let lastProcessedId = 0;
                    while (true) {
                        const batch = await this.ws.post('versions', version.id + '/use', {
                            step:            'batch',
                            phase:           this.useForm.phase,
                            versionIds:      this.useForm.versionIds,
                            lastProcessedId: lastProcessedId,
                        });
                        if (!batch || batch.error) {
                            this.useForm.error = true;
                            return;
                        }
                        this.useForm.processed += batch.processed;
                        lastProcessedId = batch.lastProcessedId;
                        if (!batch.hasMore) {
                            break;
                        }
                    }
                }

                const finish = await this.ws.post('versions', version.id + '/use', { step: 'finish' });
                if (!finish || finish.error) {
                    this.useForm.error = true;
                    return;
                }

                this.useForm.done = true;
                setTimeout(() => { window.location.reload(); }, 1200);
            } catch (e) {
                this.useForm.error = true;
            }
        },
        /**
         * Ferme la modale de restauration.
         * @returns {void}
         */
        closeUseModal() {
            this.$refs.useVersionModal.close();
        },

        /**
         * Parcourt recursivement l'arbre et retourne la liste des ancetres de la version ciblee,
         * ordonnes du noeud racine vers le parent direct.
         * @param {Array<Object>} versions - Niveau courant de l'arbre.
         * @param {number}        targetId - Identifiant de la version dont chercher les ancetres.
         * @param {Array<Object>} path     - Accumulateur des ancetres traverses.
         * @returns {Array<Object>|null} Liste des ancetres, ou null si non trouve.
         */
        findAncestors(versions, targetId, path = []) {
            for (const v of versions) {
                if (v.id === targetId) {
                    return path;
                }
                if (v.subVersions && v.subVersions.length) {
                    const result = this.findAncestors(v.subVersions, targetId, [...path, v]);
                    if (result !== null) {
                        return result;
                    }
                }
            }
            return null;
        },
        /**
         * Ouvre la modale d'export pour la version donnee.
         * Calcule la liste des ancetres pour alimenter le select "Version de depart".
         * Pour la version principale (isRoot), aucun ancetre n'existe, le select est masque.
         * @param {Object} version - La version a exporter.
         * @returns {void}
         */
        exportVersion(version) {
            const ancestors = this.findAncestors(this.versions, version.id) || [];
            this.exportForm.version = version;
            this.exportForm.action = 1;
            this.exportForm.ancestors = ancestors;
            this.exportForm.versionFrom = ancestors.length > 0 ? ancestors[ancestors.length - 1].id : null;
            this.$refs.exportVersionModal.show();
        },
        /**
         * Reinitialise l'etat du formulaire d'export apres l'animation de fermeture (500 ms).
         * @returns {void}
         */
        closeExportModal() {
            setTimeout(() => {
                this.exportForm.action = 1;
                this.exportForm.version = null;
                this.exportForm.versionFrom = null;
                this.exportForm.ancestors = [];
            }, 500);
        },
        /**
         * Construit l'URL d'export avec les parametres du formulaire et ouvre un nouvel onglet
         * pour declencher le telechargement du fichier.
         * @returns {void}
         */
        submitExportModal() {
            const version = this.exportForm.version;
            const params = new URLSearchParams({ action: this.exportForm.action });
            if ((this.exportForm.action === 2 || this.exportForm.action === 3) && this.exportForm.ancestors.length > 0) {
                params.set('version_from', this.exportForm.versionFrom);
            }
            const url = this.ws.url + 'versions/' + version.id + '/export?' + params.toString();
            window.open(url, '_blank');
            this.$refs.exportVersionModal.close();
        },
    }
}
</script>
