/**
 * @file src/js/event-target.js
 */
import * as Events from './utils/events.js' ;
import window from 'global/window' ;

/**
 * `EventTarget` est une classe qui peut avoir la même API que le DOM `EventTarget`. Il
 * ajoute des fonctions abrégées qui enveloppent les fonctions longues. Par exemple :
 * la fonction `on` est une enveloppe autour de `addEventListener`.
 *
 * @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget}
 * @classe EventTarget
 */
const EventTarget = function() {} ;

/**
 * Un événement DOM personnalisé.
 *
 * @typedef {Objet} EventTarget~Event
 * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent}
 */

/**
 * Tous les récepteurs d'événements doivent respecter le format suivant.
 *
 * @callback EventTarget~EventListener
 * @this {EventTarget}
 *
 * @param {EventTarget~Event} event
 *        l'événement qui a déclenché cette fonction
 *
 * @param {Objet} [hash]
 *        hachage des données envoyées pendant l'événement
 */

/**
 * Un objet contenant des noms d'événements comme clés et des booléens comme valeurs.
 *
 * > NOTE : Si un nom d'événement est défini à une valeur vraie ici {@link EventTarget#trigger}
 *         aura des fonctionnalités supplémentaires. Voir cette fonction pour plus d'informations.
 *
 * @property EventTarget.prototype.allowedEvents_
 * @private
 */
EventTarget.prototype.allowedEvents_ = {} ;

/**
 * Ajoute un "écouteur d'événement" à une instance d'une "cible d'événement". Un "auditeur d'événements" est un élément
 * qui sera appelée lorsqu'un événement portant un certain nom sera déclenché.
 *
 * @param {string|string[]} type
 *        Un nom d'événement ou un tableau de noms d'événements.
 *
 * @param {EventTarget~EventListener} fn
 *        La fonction à appeler avec `EventTarget`s
 */
EventTarget.prototype.on = function(type, fn) {
  // Supprimer l'alias addEventListener avant d'appeler Events.on
  // pour ne pas entrer dans une boucle de type infini
  const ael = this.addEventListener ;

  this.addEventListener = () => {} ;
  Events.on(this, type, fn) ;
  this.addEventListener = ael ;
};

/**
 * Un alias de {@link EventTarget#on}. Permet à `EventTarget` d'imiter
 * l'API DOM standard.
 *
 * @fonction
 * @see {@link EventTarget#on}
 */
EventTarget.prototype.addEventListener = EventTarget.prototype.on ;

/**
 * Supprime un `écoute d'événement` pour un événement spécifique d'une instance de `EventTarget`.
 * Ainsi, le `event listener` ne sera plus appelé lorsque la fonction
 * l'événement nommé se produit.
 *
 * @param {string|string[]} type
 *        Un nom d'événement ou un tableau de noms d'événements.
 *
 * @param {EventTarget~EventListener} fn
 *        La fonction à supprimer.
 */
EventTarget.prototype.off = function(type, fn) {
  Events.off(this, type, fn) ;
};

/**
 * Un alias de {@link EventTarget#off}. Permet à `EventTarget` d'imiter
 * l'API DOM standard.
 *
 * @fonction
 * @see {@link EventTarget#off}
 */
EventTarget.prototype.removeEventListener = EventTarget.prototype.off ;

/**
 * Cette fonction ajoutera un `event listener` qui ne sera déclenché qu'une seule fois. Après la
 * le premier déclencheur, il sera supprimé. Cela revient à ajouter un "écouteur d'événements" (`event listener`)
 * avec {@link EventTarget#on} qui appelle {@link EventTarget#off} sur lui-même.
 *
 * @param {string|string[]} type
 *        Un nom d'événement ou un tableau de noms d'événements.
 *
 * @param {EventTarget~EventListener} fn
 *        La fonction à appeler une fois pour chaque nom d'événement.
 */
EventTarget.prototype.one = function(type, fn) {
  // Suppression de l'alias addEventListener Events.on
  // pour ne pas entrer dans une boucle de type infini
  const ael = this.addEventListener ;

  this.addEventListener = () => {} ;
  Events.one(this, type, fn) ;
  this.addEventListener = ael ;
};

EventTarget.prototype.any = function(type, fn) {
  // Suppression de l'alias addEventListener Events.on
  // pour ne pas entrer dans une boucle de type infini
  const ael = this.addEventListener ;

  this.addEventListener = () => {} ;
  Events.any(this, type, fn) ;
  this.addEventListener = ael ;
};

/**
 * Cette fonction provoque un événement. Cela aura pour conséquence que tous les `écoutants d'événements`
 * qui attendent cet événement pour être appelés. S'il n'y a pas d'"auditeurs d'événements", il n'y a pas d'"auditeurs d'événements"
 * pour un événement, il ne se passera rien.
 *
 * Si le nom de l'événement déclenché se trouve dans `EventTarget.allowedEvents_`.
 * Le déclencheur appellera également la fonction `on` + `uppercaseEventName`.
 *
 * Exemple:
 * 'click' est dans `EventTarget.allowedEvents_`, donc le trigger va essayer d'appeler
 * `onClick` s'il existe.
 *
 * @param {string|EventTarget~Event|Object} event
 *        Le nom de l'événement, un `Event`, ou un objet dont la clé est de type
 *        un nom d'événement.
 */
EventTarget.prototype.trigger = function(event) {
  const type = event.type || event ;

  // dépréciation
  // Dans une prochaine version, nous devrions définir la cible par défaut comme étant `this`
  // similaire à la façon dont nous avons fixé par défaut la cible à `elem` en
  // `Events.trigger`. Pour l'instant, la "cible" par défaut est
  // `document` à cause de l'appel `Event.fixEvent`.
  if (typeof event === 'string') {
    événement = {type} ;
  }
  event = Events.fixEvent(event) ;

  if (this.allowedEvents_[type] && this['on' + type]) {
    this['on' + type](event) ;
  }

  Events.trigger(this, event) ;
};

/**
 * Un alias de {@link EventTarget#trigger}. Permet à `EventTarget` d'imiter
 * l'API DOM standard.
 *
 * @fonction
 * @see {@link EventTarget#trigger}
 */
EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger ;

let EVENT_MAP ;

EventTarget.prototype.queueTrigger = function(event) {
  // ne mettre en place EVENT_MAP que si elle sera utilisée
  if (!EVENT_MAP) {
    EVENT_MAP = nouvelle Map() ;
  }

  const type = event.type || event ;
  let map = EVENT_MAP.get(this) ;

  if (!map) {
    map = nouvelle Map() ;
    EVENT_MAP.set(this, map) ;
  }

  const oldTimeout = map.get(type) ;

  map.delete(type) ;
  window.clearTimeout(oldTimeout) ;

  const timeout = window.setTimeout(() => {
    map.delete(type) ;
    // si nous avons supprimé tous les délais d'attente pour la cible actuelle, supprimer sa carte
    if (map.size === 0) {
      map = null ;
      EVENT_MAP.delete(this) ;
    }

    this.trigger(event) ;
  }, 0) ;

  map.set(type, timeout) ;
};

export default EventTarget ;