/**
 * @file plugin.js
 */
import evented de './mixins/evented' ;
import stateful from './mixins/stateful' ;
import * as Events from './utils/events' ;
import log from './utils/log' ;
import Player from './player' ;

/**
 * Le nom du plugin de base.
 *
 * @private
 * @constant
 * @type {string}
 */
const BASE_PLUGIN_NAME = 'plugin' ;

/**
 * Clé sur laquelle est stocké le cache des plugins actifs d'un lecteur.
 *
 * @private
 * @constant
 * @type {string}
 */
const PLUGIN_CACHE_KEY = 'activePlugins_' ;

/**
 * Stocke les plugins enregistrés dans un espace privé.
 *
 * @private
 * @type {Objet}
 */
const pluginStorage = {} ;

/**
 * Indique si un plugin a été enregistré ou non.
 *
 * @private
 * @param {string} name
 *          Le nom d'un plugin.
 *
 * @return {boolean}
 *          Si le plugin a été enregistré ou non.
 */
const pluginExists = (name) => pluginStorage.hasOwnProperty(name) ;

/**
 * Obtenir un plugin enregistré par son nom.
 *
 * @private
 * @param {string} name
 *          Le nom d'un plugin.
 *
 * @return {Fonction|undefined}
 *          Le plugin (ou non défini).
 */
const getPlugin = (name) => pluginExists(name) ? pluginStorage[name] : undefined ;

/**
 * Marque un plugin comme étant "actif" sur un lecteur.
 *
 * Il s'assure également que le lecteur dispose d'un objet permettant de suivre les plugins actifs.
 *
 * @private
 * @param {Player} player
 *          Une instance de lecteur Video.js.
 *
 * @param {string} name
 *          Le nom d'un plugin.
 */
const markPluginAsActive = (player, name) => {
  player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {} ;
  player[PLUGIN_CACHE_KEY][name] = true ;
};

/**
 * Déclenche une paire d'événements de configuration du plugin.
 *
 * @private
 * @param {Player} player
 *         Une instance de lecteur Video.js.
 *
 * @param {Plugin~PluginEventHash} hash
 *         Un hachage de l'événement du plugin.
 *
 * @param {boolean} [before]
 *         Si vrai, préfixe le nom de l'événement par "before". En d'autres termes,
 *         à utiliser pour déclencher "beforepluginsetup" au lieu de "pluginsetup".
 */
const triggerSetupEvent = (player, hash, before) => {
  const eventName = (before ? 'before' : '') + 'pluginsetup' ;

  player.trigger(eventName, hash) ;
  player.trigger(eventName + ':' + hash.name, hash) ;
};

/**
 * Prend une fonction d'extension de base et renvoie une fonction d'enveloppe qui marque
 * sur le lecteur que le plugin a été activé.
 *
 * @private
 * @param {string} name
 *          Le nom du plugin.
 *
 * @param {Fonction} plugin
 *          Le plugin de base.
 *
 * @return {Fonction}
 *          Une fonction d'enveloppe pour le plugin donné.
 */
const createBasicPlugin = function(name, plugin) {
  const basicPluginWrapper = function() {

    // Nous déclenchons les événements "beforepluginsetup" et "pluginsetup" sur le lecteur
    // peu importe, mais nous voulons que le hachage soit cohérent avec le hachage fourni
    // pour les plugins avancés.
    //
    // La seule chose potentiellement contre-intuitive ici est la `instance` dans
    // l'événement "pluginsetup" est la valeur retournée par la fonction `plugin`.
    triggerSetupEvent(this, {name, plugin, instance : null}, true) ;

    const instance = plugin.apply(this, arguments) ;

    markPluginAsActive(this, name) ;
    triggerSetupEvent(this, {name, plugin, instance}) ;

    retourner l'instance ;
  };

  Object.keys(plugin).forEach(function(prop) {
    basicPluginWrapper[prop] = plugin[prop] ;
  }) ;

  return basicPluginWrapper ;
};

/**
 * Prend une sous-classe de plugin et renvoie une fonction d'usine pour la génération de
 * instances de celui-ci.
 *
 * Cette fonction d'usine se remplacera elle-même par une instance de la fonction demandée
 * sous-classe de Plugin.
 *
 * @private
 * @param {string} name
 *          Le nom du plugin.
 *
 * @param {Plugin} Sous-classe de plugin
 *          Le plugin avancé.
 *
 * @return {Fonction}
 */
const createPluginFactory = (name, PluginSubClass) => {

  // Ajouter une propriété `name` au prototype du plugin pour que chaque plugin puisse
  // se référer à lui-même par son nom.
  PluginSubClass.prototype.name = name ;

  return function(...args) {
    triggerSetupEvent(this, {name, plugin : PluginSubClass, instance : null}, true) ;

    const instance = new PluginSubClass(...[this, ...args]) ;

    // Le plugin est remplacé par une fonction qui renvoie l'instance actuelle.
    this[name] = () => instance ;

    triggerSetupEvent(this, instance.getEventHash()) ;

    retourner l'instance ;
  };
};

/**
 * Classe mère pour tous les plugins avancés.
 *
 * @mixes module:evented~EventedMixin
 * @mixes module:stateful~StatefulMixin
 * @fires Player#beforepluginsetup
 * @fires Player#beforepluginsetup:$name
 * @fires Player#pluginsetup
 * @fires Player#pluginsetup:$name
 * @listens Player#dispose
 * @throws {Error}
 *          Si l'on tente d'instancier la classe {@link Plugin} de base
 *          directement au lieu de passer par une sous-classe.
 */
class Plugin {

  /**
   * Crée une instance de cette classe.
   *
   * Les sous-classes doivent appeler `super` pour s'assurer que les plugins sont correctement initialisés.
   *
   * @param {Player} player
   *        Une instance de lecteur Video.js.
   */
  constructor(player) {
    if (this.constructor === Plugin) {
      lancer une nouvelle erreur ('Plugin must be sub-classed ; not directly instantiated.') ;
    }

    this.player = player ;

    if (!this.log) {
      this.log = this.player.log.createLogger(this.name) ;
    }

    // Faire de cet objet un objet événementiel, mais supprimer la méthode `trigger` ajoutée pour que nous puissions
    // utiliser la version prototype à la place.
    evented(this) ;
    supprimer ce.trigger ;

    stateful(this, this.constructor.defaultState) ;
    markPluginAsActive(player, this.name) ;

    // Lier automatiquement la méthode dispose afin de pouvoir l'utiliser en tant qu'auditeur et la délier
    // il est facile de l'utiliser plus tard.
    this.dispose = this.dispose.bind(this) ;

    // Si le lecteur est éliminé, éliminez le plugin.
    player.on('dispose', this.dispose) ;
  }

  /**
   * Obtenir la version du plugin qui a été définie sur <pluginName>.VERSION
   */
  version() {
    return this.constructor.VERSION ;
  }

  /**
   * Chaque événement déclenché par les plugins comprend un hachage de données supplémentaires avec
   * propriétés conventionnelles.
   *
   * Elle renvoie cet objet ou modifie un hachage existant.
   *
   * @param {Objet} [hash={}]
   *          Un objet à utiliser en tant qu'événement - un hachage d'événement.
   *
   * @return {Plugin~PluginEventHash}
   *          Un objet de hachage d'événement avec les propriétés fournies mélangées.
   */
  getEventHash(hash = {}) {
    hash.name = this.name ;
    hash.plugin = this.constructor ;
    hash.instance = this ;
    return hash ;
  }

  /**
   * Déclenche un événement sur l'objet du plugin et surcharge la fonction
   * {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}.
   *
   * @param {string|Object} event
   *          Un type d'événement ou un objet avec une propriété de type.
   *
   * @param {Objet} [hash={}]
   *          Hachage de données supplémentaires à fusionner avec un
   *          {@link Plugin~PluginEventHash|PluginEventHash}.
   *
   * @return {boolean}
   *          Si le défaut a été évité ou non.
   */
  trigger(event, hash = {}) {
    return Events.trigger(this.eventBusEl_, event, this.getEventHash(hash)) ;
  }

  /**
   * Gère les événements "statechanged" du plugin. No-op par défaut, surcharge par
   * la sous-classification.
   *
   * @abstract
   * @param {Event} e
   *           Un objet d'événement fourni par un événement "statechanged".
   *
   * @param {Objet} e.changes
   *           Un objet décrivant les changements survenus avec le paramètre "statechanged"
   *           événement.
   */
  handleStateChanged(e) {}

  /**
   * Supprime un plugin.
   *
   * Les sous-classes peuvent surcharger cette fonction si elles le souhaitent, mais pour des raisons de sécurité,
   * il est probablement préférable de s'abonner à l'événement "dispose".
   *
   * @fires Plugin#dispose
   */
  dispose() {
    const {nom, joueur} = this ;

    /**
     * Signale qu'un plugin avancé est sur le point d'être éliminé.
     *
     * @event Plugin#dispose
     * @type {EventTarget~Event}
     */
    this.trigger('dispose') ;
    this.off() ;
    player.off('dispose', this.dispose) ;

    // Éliminez toutes les sources possibles de fuites de mémoire en nettoyant les éléments suivants
    // les références entre le joueur et l'instance de plugin et les annuler
    // l'état du plugin et le remplacement des méthodes par une fonction qui lance.
    player[PLUGIN_CACHE_KEY][name] = false ;
    this.player = this.state = null ;

    // Enfin, remplacer le nom du plugin sur le lecteur par une nouvelle fabrique
    // pour que le plugin soit prêt à être réinstallé.
    player[name] = createPluginFactory(name, pluginStorage[name]) ;
  }

  /**
   * Détermine si un plugin est un plugin de base (c'est-à-dire qu'il n'est pas une sous-classe de `Plugin`).
   *
   * @param {string|Function} plugin
   *          S'il s'agit d'une chaîne, correspond au nom d'un plugin. S'il s'agit d'une fonction, elle sera
   *          testé directement.
   *
   * @return {boolean}
   *          Si un plugin est un plugin de base ou non.
   */
  static isBasic(plugin) {
    const p = (typeof plugin === 'string') ? getPlugin(plugin) : plugin ;

    return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype) ;
  }

  /**
   * Enregistrer un plugin Video.js.
   *
   * @param {string} name
   *          Le nom du plugin à enregistrer. Doit être une chaîne et
   *          ne doit pas correspondre à un plugin existant ou à une méthode du `Player`
   *          prototype.
   *
   * @param {Fonction} plugin
   *          Une sous-classe de `Plugin` ou une fonction pour les plugins de base.
   *
   * @return {Fonction}
   *          Pour les plugins avancés, une fonction d'usine pour ce plugin. Pour
   *          les plugins de base, une fonction d'encapsulation qui initialise le plugin.
   */
  static registerPlugin(name, plugin) {
    if (typeof name !== 'string') {
      throw new Error(`Nom de plugin illégal, "${nom}", doit être une chaîne, était ${typeof name}.`) ;
    }

    if (pluginExists(name)) {
      log.warn(`Un plugin nommé "${nom}" existe déjà. Vous voudrez peut-être éviter de réenregistrer les plugins!`) ;
    } else if (Player.prototype.hasOwnProperty(name)) {
      throw new Error(`Nom de plugin illégal, "${nom}", ne peut pas partager un nom avec une méthode de lecteur existante!`) ;
    }

    if (typeof plugin !== 'function') {
      throw new Error(`Illegal plugin for "${name}", must be a function, was ${typeof plugin}.`) ;
    }

    pluginStorage[name] = plugin ;

    // Ajouter une méthode de prototype de lecteur pour tous les plugins sous-classés (mais pas pour le plugin
    // la classe de base du plugin).
    if (name !== BASE_PLUGIN_NAME) {
      if (Plugin.isBasic(plugin)) {
        Player.prototype[name] = createBasicPlugin(name, plugin) ;
      } else {
        Player.prototype[name] = createPluginFactory(name, plugin) ;
      }
    }

    return plugin ;
  }

  /**
   * Désenregistrement d'un plugin Video.js.
   *
   * @param {string} name
   *         Le nom du plugin à désenregistrer. Doit être une chaîne de caractères qui
   *         correspond à un plugin existant.
   *
   * @throws {Error}
   *         En cas de tentative de désenregistrement du plugin de base.
   */
  static deregisterPlugin(name) {
    if (name === BASE_PLUGIN_NAME) {
      lancer une nouvelle erreur ('Cannot de-register base plugin.') ;
    }
    if (pluginExists(name)) {
      supprimer pluginStorage[nom] ;
      supprimer Player.prototype[name] ;
    }
  }

  /**
   * Obtient un objet contenant plusieurs plugins Video.js.
   *
   * @param {Array} [names]
   *          S'il est fourni, il doit s'agir d'un tableau de noms de plugins. La valeur par défaut est _tous_
   *          noms des plugins.
   *
   * @return {Objet|non défini}
   *          Un objet contenant un ou des plugin(s) associé(s) à leur(s) nom(s) ou à leur(s)
   *          `undefined` si aucun plugin correspondant n'existe).
   */
  static getPlugins(names = Object.keys(pluginStorage)) {
    laisser le résultat ;

    names.forEach(name => {
      const plugin = getPlugin(name) ;

      if (plugin) {
        résultat = résultat || {} ;
        result[name] = plugin ;
      }
    }) ;

    retourner le résultat ;
  }

  /**
   * Obtient la version d'un plugin, si elle est disponible
   *
   * @param {string} name
   *          Le nom d'un plugin.
   *
   * @return {string}
   *          La version du plugin ou une chaîne vide.
   */
  static getPluginVersion(name) {
    const plugin = getPlugin(name) ;

    return plugin && plugin.VERSION || '' ;
  }
}

/**
 * Récupère un plugin par son nom s'il existe.
 *
 * @statique
 * @method getPlugin
 * @memberOf Plugin
 * @param {string} name
 *           Le nom d'un plugin.
 *
 * @returns {Fonction|undefined}
 *           Le plugin (ou `undefined`).
 */
Plugin.getPlugin = getPlugin ;

/**
 * Le nom de la classe de base du plugin telle qu'elle est enregistrée.
 *
 * @type {string}
 */
Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME ;

Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin) ;

/**
 * Documenté dans player.js
 *
 * @ignore
 */
Player.prototype.usingPlugin = function(name) {
  return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true ;
};

/**
 * Documenté dans player.js
 *
 * @ignore
 */
Player.prototype.hasPlugin = function(name) {
  return !!pluginExists(name) ;
};

exporter le plugin par défaut ;

/**
 * Signale qu'un plugin est sur le point d'être installé sur un lecteur.
 *
 * @event Player#beforepluginsetup
 * @type {Plugin~PluginEventHash}
 */

/**
 * Signale qu'un plugin est sur le point d'être installé sur un lecteur - par son nom. Le nom
 * est le nom du plugin.
 *
 * @event Player#beforepluginsetup:$name
 * @type {Plugin~PluginEventHash}
 */

/**
 * Signale qu'un plugin vient d'être installé sur un lecteur.
 *
 * @event Player#pluginsetup
 * @type {Plugin~PluginEventHash}
 */

/**
 * Signale qu'un plugin vient d'être installé sur un lecteur - par son nom. Le nom
 * est le nom du plugin.
 *
 * @event Player#pluginsetup:$name
 * @type {Plugin~PluginEventHash}
 */

/**
 * @typedef {Objet} Plugin~PluginEventHash
 *
 * @property {string} instance
 *           Pour les plugins de base, la valeur de retour de la fonction du plugin. Pour
 *           plugins avancés, l'instance de plugin sur laquelle l'événement est déclenché.
 *
 * @property {string} name
 *           Le nom du plugin.
 *
 * @property {string} plugin
 *           Pour les plugins de base, la fonction du plugin. Pour les plugins avancés, l'option
 *           classe/constructeur de plugin.
 */