/**
 * @file modal-dialog.js
 */
import * as Dom from './utils/dom' ;
import Component from './component' ;
import window from 'global/window' ;
import document from 'global/document' ;
import keycode from 'keycode' ;

const MODAL_CLASS_NAME = 'vjs-modal-dialog' ;

/**
 * Le `ModalDialog` s'affiche au-dessus de la vidéo et de ses contrôles, ce qui bloque l'accès à la vidéo
 * l'interaction avec le lecteur jusqu'à ce qu'il soit fermé.
 *
 * Les boîtes de dialogue modales comportent un bouton "Fermer" et se ferment lorsque ce bouton est activé
 * est activée - ou lorsque l'on appuie sur ESC n'importe où.
 *
 * @extends Component
 */
class ModalDialog extends Component {

  /**
   * Créer une instance de cette classe.
   *
   * @param {Player} player
   *        Le `Player` auquel cette classe doit être attachée.
   *
   * @param {Objet} [options]
   *        La mémoire clé/valeur des options du lecteur.
   *
   * @param {Mixed} [options.content=undefined]
   *        Fournir un contenu personnalisé pour ce modal.
   *
   * @param {string} [options.description]
   *        Description textuelle de la fenêtre modale, principalement pour des raisons d'accessibilité.
   *
   * @param {boolean} [options.fillAlways=false]
   *        Normalement, les modales ne sont remplies automatiquement que la première fois
   *        ils ouvrent. Cela indique à la fenêtre modale de rafraîchir son contenu
   *        chaque fois qu'il s'ouvre.
   *
   * @param {string} [options.label]
   *        Une étiquette de texte pour la fenêtre modale, principalement pour des raisons d'accessibilité.
   *
   * @param {boolean} [options.pauseOnOpen=true]
   *        Si `true`, la lecture sera interrompue si la lecture est interrompue lorsque
   *        la modale s'ouvre et reprend lorsqu'elle se ferme.
   *
   * @param {boolean} [options.temporary=true]
   *        Si `true`, la fenêtre modale ne peut être ouverte qu'une seule fois ; elle sera
   *        a disposé dès qu'il a été fermé.
   *
   * @param {boolean} [options.uncloseable=false]
   *        Si `true`, l'utilisateur ne pourra pas fermer la fenêtre modale
   *        par l'intermédiaire de l'interface utilisateur selon les modalités habituelles. La clôture programmatique est
   *        toujours possible.
   */
  constructor(player, options) {
    super(player, options) ;

    this.handleKeyDown_ = (e) => this.handleKeyDown(e) ;
    this.close_ = (e) => this.close(e) ;
    this.opened_ = this.hasBeenOpened_ = this.hasBeenFilled_ = false ;

    this.closeable(!this.options_.uncloseable) ;
    this.content(this.options_.content) ;

    // Assurez-vous que le contentEl est défini APRÈS l'initialisation des enfants
    // parce que nous ne voulons que le contenu de la modale dans le contentEl
    // (pas les éléments de l'interface utilisateur comme le bouton de fermeture).
    this.contentEl_ = Dom.createEl('div', {
      className : `${MODAL_CLASS_NAME}-content`
    }, {
      rôle : "document
    }) ;

    this.descEl_ = Dom.createEl('p', {
      className : `${MODAL_CLASS_NAME}-description vjs-control-text`,
      id : this.el().getAttribute('aria-describedby')
    }) ;

    Dom.textContent(this.descEl_, this.description()) ;
    this.el_.appendChild(this.descEl_) ;
    this.el_.appendChild(this.contentEl_) ;
  }

  /**
   * Créer l'élément DOM du `ModalDialog`
   *
   * @return {Element}
   *         L'élément DOM qui est créé.
   */
  createEl() {
    return super.createEl('div', {
      className : this.buildCSSClass(),
      tabIndex : -1
    }, {
      'aria-describedby' : `${this.id()}_description`,
      aria-hidden" : "true",
      'aria-label' : this.label(),
      'rôle' : 'dialogue'
    }) ;
  }

  dispose() {
    this.contentEl_ = null ;
    this.descEl_ = null ;
    this.previouslyActiveEl_ = null ;

    super.dispose() ;
  }

  /**
   * Construit le DOM par défaut `className`.
   *
   * @return {string}
   *         Le `nom de classe` du DOM pour cet objet.
   */
  buildCSSClass() {
    return `${MODAL_CLASS_NAME} vjs-hidden ${super.buildCSSClass()}` ;
  }

  /**
   * Renvoie la chaîne de l'étiquette de cette fenêtre modale. Principalement utilisé pour l'accessibilité.
   *
   * @return {string}
   *         l'étiquette localisée ou brute de cette modale.
   */
  label() {
    return this.localize(this.options_.label || 'Fenêtre modale') ;
  }

  /**
   * Renvoie la chaîne de description de cette fenêtre modale. Principalement utilisé pour
   * l'accessibilité.
   *
   * @return {string}
   *         La description localisée ou brute de cette modalité.
   */
  description() {
    let desc = this.options_.description || this.localize('Ceci est une fenêtre modale.') ;

    // Ajouter un message universel de fermeture si la modale est fermable.
    if (this.closeable()) {
      desc += ' ' + this.localize('Cette fenêtre modale peut être fermée en appuyant sur la touche Echap ou en activant le bouton de fermeture.') ;
    }

    retour desc ;
  }

  /**
   * Ouvre la fenêtre modale.
   *
   * @fires ModalDialog#beforemodalopen
   * @fires ModalDialog#modalopen
   */
  open() {
    if (!this.opened_) {
      const player = this.player() ;

      /**
        * Déclenché juste avant l'ouverture d'un `ModalDialog`.
        *
        * @event ModalDialog#beforemodalopen
        * @type {EventTarget~Event}
        */
      this.trigger('beforemodalopen') ;
      this.opened_ = true ;

      // Remplir le contenu si la fenêtre modale n'a jamais été ouverte auparavant et
      // n'a jamais été remplie.
      if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) {
        this.fill() ;
      }

      // Si le lecteur est en cours de lecture, le mettre en pause et prendre note de son état antérieur
      // état de jeu.
      this.wasPlaying_ = !player.paused() ;

      if (this.options_.pauseOnOpen && this.wasPlaying_) {
        player.pause() ;
      }

      this.on('keydown', this.handleKeyDown_) ;

      // Masquer les contrôles et noter s'ils ont été activés.
      this.hadControls_ = player.controls() ;
      player.controls(false) ;

      this.show() ;
      this.conditionalFocus_() ;
      this.el().setAttribute('aria-hidden', 'false') ;

      /**
        * Déclenché juste après l'ouverture d'un `ModalDialog`.
        *
        * @event ModalDialog#modalopen
        * @type {EventTarget~Event}
        */
      this.trigger('modalopen') ;
      this.hasBeenOpened_ = true ;
    }
  }

  /**
   * Si le `ModalDialog` est actuellement ouvert ou fermé.
   *
   * @param {boolean} [valeur]
   *         Si elle est donnée, elle ouvrira (`true`) ou fermera (`false`) la fenêtre modale.
   *
   * @return {boolean}
   *         l'état d'ouverture actuel de la boîte de dialogue modale
   */
  opened(value) {
    if (typeof value === 'boolean') {
      this[value ? 'open' : 'close']() ;
    }
    return this.opened_ ;
  }

  /**
   * Ferme la fenêtre modale, ne fait rien si le `ModalDialog` est
   * n'est pas ouverte.
   *
   * @fires ModalDialog#beforemodalclose
   * @fires ModalDialog#modalclose
   */
  close() {
    if (!this.opened_) {
      retour ;
    }
    const player = this.player() ;

    /**
      * Déclenché juste avant la fermeture d'un `ModalDialog`.
      *
      * @event ModalDialog#beforemodalclose
      * @type {EventTarget~Event}
      */
    this.trigger('beforemodalclose') ;
    this.opened_ = false ;

    if (this.wasPlaying_ && this.options_.pauseOnOpen) {
      player.play() ;
    }

    this.off('keydown', this.handleKeyDown_) ;

    if (this.hadControls_) {
      player.controls(true) ;
    }

    this.hide() ;
    this.el().setAttribute('aria-hidden', 'true') ;

    /**
      * Déclenché juste après la fermeture d'un `ModalDialog`.
      *
      * @event ModalDialog#modalclose
      * @type {EventTarget~Event}
      */
    this.trigger('modalclose') ;
    this.conditionalBlur_() ;

    if (this.options_.temporary) {
      this.dispose() ;
    }
  }

  /**
   * Vérifie si le `ModalDialog` peut être fermé par l'interface utilisateur.
   *
   * @param {boolean} [valeur]
   *         Si elle est donnée sous forme de booléen, l'option `closeable` sera activée.
   *
   * @return {boolean}
   *         Renvoie la valeur finale de l'option fermable.
   */
  closeable(value) {
    if (typeof value === 'boolean') {
      const closeable = this.closeable_ = !!value ;
      let close = this.getChild('closeButton') ;

      // Si cette page est rendue refermable et qu'elle n'a pas de bouton de fermeture, ajoutez-en un.
      if (closeable && !close) {

        // Le bouton de fermeture doit être un enfant de la fenêtre modale - et non son
        // l'élément de contenu, donc changer temporairement l'élément de contenu.
        const temp = this.contentEl_ ;

        this.contentEl_ = this.el_ ;
        close = this.addChild('closeButton', {controlText : 'Close Modal Dialog'}) ;
        this.contentEl_ = temp ;
        this.on(close, 'close', this.close_) ;
      }

      // Si cet élément est rendu impossible à fermer et qu'il dispose d'un bouton de fermeture, supprimez-le.
      if (!closeable && close) {
        this.off(close, 'close', this.close_) ;
        this.removeChild(close) ;
        close.dispose() ;
      }
    }
    return this.closeable_ ;
  }

  /**
   * Remplir l'élément de contenu de la modale avec l'option "content" de la modale.
   * L'élément de contenu sera vidé avant que ce changement n'ait lieu.
   */
  fill() {
    this.fillWith(this.content()) ;
  }

  /**
   * Remplir l'élément de contenu de la modale avec un contenu arbitraire.
   * L'élément de contenu sera vidé avant que ce changement n'ait lieu.
   *
   * @fires ModalDialog#beforemodalfill
   * @fires ModalDialog#modalfill
   *
   * @param {Mixed} [content]
   *        Les mêmes règles s'appliquent à cette option qu'à l'option `content`.
   */
  fillWith(content) {
    const contentEl = this.contentEl() ;
    const parentEl = contentEl.parentNode ;
    const nextSiblingEl = contentEl.nextSibling ;

    /**
      * Déclenché juste avant qu'un `ModalDialog` ne soit rempli de contenu.
      *
      * @event ModalDialog#beforemodalfill
      * @type {EventTarget~Event}
      */
    this.trigger('beforemodalfill') ;
    this.hasBeenFilled_ = true ;

    // Détachement de l'élément de contenu du DOM avant d'effectuer
    // manipulation pour éviter de modifier le DOM en direct plusieurs fois.
    parentEl.removeChild(contentEl) ;
    this.empty() ;
    Dom.insertContent(contentEl, content) ;
    /**
     * Déclenché juste après qu'un `ModalDialog` ait été rempli avec du contenu.
     *
     * @event ModalDialog#modalfill
     * @type {EventTarget~Event}
     */
    this.trigger('modalfill') ;

    // Réinjecter l'élément de contenu rempli à nouveau.
    if (nextSiblingEl) {
      parentEl.insertBefore(contentEl, nextSiblingEl) ;
    } else {
      parentEl.appendChild(contentEl) ;
    }

    // s'assurer que le bouton de fermeture est le dernier dans le DOM du dialogue
    const closeButton = this.getChild('closeButton') ;

    if (closeButton) {
      parentEl.appendChild(closeButton.el_) ;
    }
  }

  /**
   * Vide l'élément de contenu. Cela se produit chaque fois que le modal est rempli.
   *
   * @fires ModalDialog#beforemodalempty
   * @fires ModalDialog#modalempty
   */
  empty() {
    /**
    * Déclenché juste avant qu'un `ModalDialog` ne soit vidé.
    *
    * @event ModalDialog#beforemodalempty
    * @type {EventTarget~Event}
    */
    this.trigger('beforemodalempty') ;
    Dom.emptyEl(this.contentEl()) ;

    /**
    * Déclenché juste après qu'un `ModalDialog` ait été vidé.
    *
    * @event ModalDialog#modalempty
    * @type {EventTarget~Event}
    */
    this.trigger('modalempty') ;
  }

  /**
   * Obtient ou définit le contenu de la fenêtre modale, qui est normalisé avant d'être affiché
   * rendu dans le DOM.
   *
   * Cette opération ne met pas à jour le DOM et ne remplit pas la fenêtre modale, mais elle est appelée au cours de la phase d'exécution du projet
   * ce processus.
   *
   * @param {Mixed} [valeur]
   *         S'il est défini, définit la valeur de contenu interne à utiliser sur le formulaire
   *         le(s) prochain(s) appel(s) à `fill`. Cette valeur est normalisée avant d'être
   *         inséré. Pour "effacer" la valeur interne du contenu, passez `null`.
   *
   * @return {Mixed}
   *         Le contenu actuel du dialogue modal
   */
  content(value) {
    if (typeof value !== 'undefined') {
      this.content_ = valeur ;
    }
    return this.content_ ;
  }

  /**
   * activer conditionnellement la boîte de dialogue modale si l'attention était précédemment portée sur le lecteur.
   *
   * @private
   */
  conditionalFocus_() {
    const activeEl = document.activeElement ;
    const playerEl = this.player_.el_ ;

    this.previouslyActiveEl_ = null ;

    if (playerEl.contains(activeEl) || playerEl === activeEl) {
      this.previouslyActiveEl_ = activeEl ;

      this.focus() ;
    }
  }

  /**
   * flouter conditionnellement l'élément et recentrer le dernier élément mis au point
   *
   * @private
   */
  conditionalBlur_() {
    if (this.previouslyActiveEl_) {
      this.previouslyActiveEl_.focus() ;
      this.previouslyActiveEl_ = null ;
    }
  }

  /**
   * Gestionnaire de la descente de clé. Attaché lorsque le modal est focalisé.
   *
   * @listens keydown
   */
  handleKeyDown(event) {

    // Ne pas permettre aux touches de sortir de la boîte de dialogue modale.
    event.stopPropagation() ;

    if (keycode.isEventKey(event, 'Escape') && this.closeable()) {
      event.preventDefault() ;
      this.close() ;
      retour ;
    }

    // quitter prématurément s'il ne s'agit pas d'une touche de tabulation
    if (!keycode.isEventKey(event, 'Tab')) {
      retour ;
    }

    const focusableEls = this.focusableEls_() ;
    const activeEl = this.el_.querySelector(':focus') ;
    let focusIndex ;

    for (let i = 0 ; i < focusableEls.length ; i++) {
      if (activeEl === focusableEls[i]) {
        focusIndex = i ;
        pause ;
      }
    }

    if (document.activeElement === this.el_) {
      focusIndex = 0 ;
    }

    if (event.shiftKey && focusIndex === 0) {
      focusableEls[focusableEls.length - 1].focus() ;
      event.preventDefault() ;
    } else if (!event.shiftKey && focusIndex === focusableEls.length - 1) {
      focusableEls[0].focus() ;
      event.preventDefault() ;
    }
  }

  /**
   * obtenir tous les éléments focalisables
   *
   * @private
   */
  focusableEls_() {
    const allChildren = this.el_.querySelectorAll('*') ;

    return Array.prototype.filter.call(allChildren, (child) => {
      return ((child instanceof window.HTMLAnchorElement ||
               child instanceof window.HTMLAreaElement) && child.hasAttribute('href')) ||
             ((child instanceof window.HTMLInputElement ||
               child instanceof window.HTMLSelectElement ||
               child instanceof window.HTMLTextAreaElement ||
               child instanceof window.HTMLButtonElement) && !child.hasAttribute('disabled')) ||
             (child instanceof window.HTMLIFrameElement ||
               child instanceof window.HTMLObjectElement ||
               child instanceof window.HTMLEmbedElement) ||
             (child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1) ||
             (child.hasAttribute('contenteditable')) ;
    }) ;
  }
}

/**
 * Options par défaut pour les options par défaut de `ModalDialog`.
 *
 * @type {Objet}
 * @private
 */
ModalDialog.prototype.options_ = {
  pauseOnOpen : true,
  temporaire : vrai
};

Component.registerComponent('ModalDialog', ModalDialog) ;
export default ModalDialog ;