/**
 * @file events.js. Un système d'événements (John Resig - Secrets of a JS Ninja http://jsninja.com/)
 * (La version originale du livre n'était pas complètement utilisable, nous avons corrigé certaines choses et rendu Closure Compiler compatible)
 * Cela devrait fonctionner de façon très similaire aux événements de jQuery, mais c'est basé sur la version du livre qui n'est pas aussi
 * robuste que celle de jquery, il y a donc probablement des différences.
 *
 * fichier events.js
 * @module events
 */
import DomData from './dom-data' ;
import * as Guid from './guid.js' ;
import log from './log.js' ;
import window from 'global/window' ;
import document from 'global/document' ;

/**
 * Nettoyer le cache de l'auditeur et les répartiteurs
 *
 * @param {Element|Objet} elem
 *        Élément à nettoyer
 *
 * @param {string} type
 *        Type d'événement à nettoyer
 */
function _cleanUpEvents(elem, type) {
  if (!DomData.has(elem)) {
    retour ;
  }
  const data = DomData.get(elem) ;

  // Supprimer les événements d'un type particulier s'il n'y en a plus
  if (data.handlers[type].length === 0) {
    supprimer data.handlers[type] ;
    // data.handlers[type] = null ;
    // La définition de null provoquait une erreur avec data.handlers

    // Supprimer le méta-manipulateur de l'élément
    if (elem.removeEventListener) {
      elem.removeEventListener(type, data.dispatcher, false) ;
    } else if (elem.detachEvent) {
      elem.detachEvent('on' + type, data.dispatcher) ;
    }
  }

  // Supprimer l'objet "événements" s'il n'y a plus de types
  if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
    supprimer data.handlers ;
    supprimer data.dispatcher ;
    supprimer les données.désactivées ;
  }

  // Enfin, supprimer les données de l'élément s'il n'y a plus de données
  if (Object.getOwnPropertyNames(data).length === 0) {
    DomData.delete(elem) ;
  }
}

/**
 * Passe en revue un tableau de types d'événements et appelle la méthode requise pour chaque type.
 *
 * @param {Fonction} fn
 *        La méthode d'événement que nous voulons utiliser.
 *
 * @param {Element|Objet} elem
 *        Élément ou objet auquel lier les récepteurs
 *
 * @param {string} type
 *        Type d'événement à lier.
 *
 * @param {EventTarget~EventListener} callback
 *        Écouteur d'événements.
 */
function _handleMultipleEvents(fn, elem, types, callback) {
  types.forEach(function(type) {
    // Appeler la méthode d'événement pour chacun des types
    fn(elem, type, callback) ;
  }) ;
}

/**
 * Correction d'un événement natif pour qu'il ait des valeurs de propriété standard
 *
 * @param {Objet} événement
 *        Objet de l'événement à fixer.
 *
 * @return {Object}
 *         Objet d'événement fixe.
 */
export function fixEvent(event) {
  if (event.fixed_) {
    retour événement ;
  }

  function returnTrue() {
    retourner vrai ;
  }

  function returnFalse() {
    retourner faux ;
  }

  // Tester si une réparation est nécessaire
  // Utilisé pour vérifier si !event.stopPropagation au lieu de isPropagationStopped
  // Mais les événements natifs renvoient true pour stopPropagation, mais n'ont pas de
  // d'autres méthodes attendues comme isPropagationStopped. Il semble y avoir un problème
  // avec le code Javascript Ninja. Nous nous contentons donc d'ignorer tous les événements.
  if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
    const old = event || window.event ;

    événement = {} ;
    // Clone l'ancien objet afin de pouvoir modifier les valeurs event = {} ;
    // IE8 n'aime pas que l'on modifie les propriétés des événements natifs
    // Firefox retourne false pour event.hasOwnProperty('type') et autres props
    // ce qui rend la copie plus difficile.
    // TODO : Il est probablement préférable de créer une liste blanche d'accessoires d'événements
    for (const key in old) {
      // Safari 6.0.3 vous avertit si vous essayez de copier les couches X/Y obsolètes
      // Chrome vous avertit si vous essayez de copier des données obsolètes keyboardEvent.keyLocation
      // et webkitMovementX/Y
      // Lighthouse se plaint si Event.path est copié
      if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' &&
          key !== 'webkitMovementX' && key !== 'webkitMovementY' &&
          key !== 'path') {
        // Chrome 32+ émet un avertissement si vous essayez de copier une valeur de retour dépréciée, mais
        // nous voulons toujours le faire si preventDefault n'est pas pris en charge (IE8).
        if ( !(key === 'returnValue' && old.preventDefault)) {
          event[key] = old[key] ;
        }
      }
    }

    // L'événement s'est produit sur cet élément
    if (!event.target) {
      event.target = event.srcElement || document ;
    }

    // Gérer l'autre élément auquel l'événement est lié
    if (!event.relatedTarget) {
      event.relatedTarget = event.fromElement === event.target ?
        event.toElement :
        event.fromElement ;
    }

    // Arrêter l'action du navigateur par défaut
    event.preventDefault = function() {
      if (old.preventDefault) {
        old.preventDefault() ;
      }
      event.returnValue = false ;
      old.returnValue = false ;
      event.defaultPrevented = true ;
    };

    event.defaultPrevented = false ;

    // Arrêter le bouillonnement de l'événement
    event.stopPropagation = function() {
      if (old.stopPropagation) {
        old.stopPropagation() ;
      }
      event.cancelBubble = true ;
      old.cancelBubble = true ;
      event.isPropagationStopped = returnTrue ;
    };

    event.isPropagationStopped = returnFalse ;

    // Empêcher l'événement de se multiplier et d'exécuter d'autres gestionnaires
    event.stopImmediatePropagation = function() {
      if (old.stopImmediatePropagation) {
        old.stopImmediatePropagation() ;
      }
      event.isImmediatePropagationStopped = returnTrue ;
      event.stopPropagation() ;
    };

    event.isImmediatePropagationStopped = returnFalse ;

    // Gestion de la position de la souris
    if (event.clientX !== null && event.clientX !== undefined) {
      const doc = document.documentElement ;
      const body = document.body ;

      event.pageX = event.clientX +
        (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
        (doc && doc.clientLeft || body && body.clientLeft || 0) ;
      event.pageY = event.clientY +
        (doc && doc.scrollTop || body && body.scrollTop || 0) -
        (doc && doc.clientTop || body && body.clientTop || 0) ;
    }

    // Gérer les pressions sur les touches
    event.which = event.charCode || event.keyCode ;

    // Fixer le bouton pour les clics de souris :
    // 0 == gauche ; 1 == milieu ; 2 == droite
    if (event.button !== null && event.button !== undefined) {

      // Ce qui suit est désactivé parce qu'il ne satisfait pas à la norme videojs
      // et ................................
      /* eslint-disable */
      event.button = (event.button & 1 ? 0 :
        (event.button & 4 ? 1 :
          (event.button & 2 ? 2 : 0))) ;
      /* eslint-enable */
    }
  }

  event.fixed_ = true ;
  // Retourne l'instance fixée
  retour événement ;
}

/**
 * Si les écouteurs d'événements passifs sont pris en charge
 */
let _supportsPassive ;

const supportsPassive = function() {
  if (typeof _supportsPassive !== 'boolean') {
    _supportsPassive = false ;
    essayez {
      const opts = Object.defineProperty({}, 'passive', {
        get() {
          _supportsPassive = true ;
        }
      }) ;

      window.addEventListener('test', null, opts) ;
      window.removeEventListener('test', null, opts) ;
    } catch (e) {
      // ne pas tenir compte
    }
  }

  return _supportsPassive ;
};

/**
 * Événements tactiles que Chrome considère comme passifs
 */
const passiveEvents = [
  'touchstart',
  'touchmove' (toucher, déplacer)
] ;

/**
 * Ajouter un récepteur d'événements à l'élément
 * Il stocke la fonction de traitement dans un objet de cache distinct
 * et ajoute un gestionnaire générique à l'événement de l'élément,
 * ainsi qu'un identifiant unique (guid) pour l'élément.
 *
 * @param {Element|Objet} elem
 *        Élément ou objet auquel lier les récepteurs
 *
 * @param {string|string[]} type
 *        Type d'événement à lier.
 *
 * @param {EventTarget~EventListener} fn
 *        Écouteur d'événements.
 */
export function on(elem, type, fn) {
  if (Array.isArray(type)) {
    return _handleMultipleEvents(on, elem, type, fn) ;
  }

  if (!DomData.has(elem)) {
    DomData.set(elem, {}) ;
  }

  const data = DomData.get(elem) ;

  // Nous avons besoin d'un endroit pour stocker toutes les données de notre gestionnaire
  if (!data.handlers) {
    data.handlers = {} ;
  }

  if (!data.handlers[type]) {
    data.handlers[type] = [] ;
  }

  if (!fn.guid) {
    fn.guid = Guid.newGUID() ;
  }

  data.handlers[type].push(fn) ;

  if (!data.dispatcher) {
    data.disabled = false ;

    data.dispatcher = function(event, hash) {

      if (data.disabled) {
        retour ;
      }

      event = fixEvent(event) ;

      const handlers = data.handlers[event.type] ;

      if (handlers) {
        // Copier les gestionnaires afin que si des gestionnaires sont ajoutés/supprimés au cours du processus, cela ne perturbe pas tout.
        const handlersCopy = handlers.slice(0) ;

        for (let m = 0, n = handlersCopy.length ; m < n ; m++) {
          if (event.isImmediatePropagationStopped()) {
            pause ;
          } else {
            essayez {
              handlersCopy[m].call(elem, event, hash) ;
            } catch (e) {
              log.error(e) ;
            }
          }
        }
      }
    };
  }

  if (data.handlers[type].length === 1) {
    if (elem.addEventListener) {
      let options = false ;

      si (supportsPassive() &&
        passiveEvents.indexOf(type) > -1) {
        options = {passive : true} ;
      }
      elem.addEventListener(type, data.dispatcher, options) ;
    } else if (elem.attachEvent) {
      elem.attachEvent('on' + type, data.dispatcher) ;
    }
  }
}

/**
 * Supprime les récepteurs d'événements d'un élément
 *
 * @param {Element|Objet} elem
 *        Objet dont il faut retirer les auditeurs.
 *
 * @param {string|string[]} [type]
 *        Type d'auditeur à supprimer. Ne pas inclure pour supprimer tous les événements de l'élément.
 *
 * @param {EventTarget~EventListener} [fn]
 *        Auditeur spécifique à supprimer. Ne pas inclure pour supprimer les auditeurs d'un événement
 *        type.
 */
export function off(elem, type, fn) {
  // Ne pas ajouter un objet de cache via getElData si ce n'est pas nécessaire
  if (!DomData.has(elem)) {
    retour ;
  }

  const data = DomData.get(elem) ;

  // S'il n'y a pas d'événements, il n'y a rien à délier
  if (!data.handlers) {
    retour ;
  }

  if (Array.isArray(type)) {
    return _handleMultipleEvents(off, elem, type, fn) ;
  }

  // Fonction utilitaire
  const removeType = function(el, t) {
    data.handlers[t] = [] ;
    _cleanUpEvents(el, t) ;
  };

  // Est-ce que nous supprimons tous les événements liés ?
  if (type === undefined) {
    for (const t in data.handlers) {
      if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
        removeType(elem, t) ;
      }
    }
    retour ;
  }

  const handlers = data.handlers[type] ;

  // S'il n'y a pas de gestionnaire, il n'y a rien à délier
  if (!handlers) {
    retour ;
  }

  // Si aucun écouteur n'a été fourni, supprimer tous les écouteurs pour le type
  if (!fn) {
    removeType(elem, type) ;
    retour ;
  }

  // Nous ne supprimons qu'un seul gestionnaire
  if (fn.guid) {
    for (let n = 0 ; n < handlers.length ; n++) {
      if (handlers[n].guid === fn.guid) {
        handlers.splice(n--, 1) ;
      }
    }
  }

  _cleanUpEvents(elem, type) ;
}

/**
 * Déclencher un événement pour un élément
 *
 * @param {Element|Objet} elem
 *        Élément permettant de déclencher un événement sur
 *
 * @param {EventTarget~Event|string} event
 *        Une chaîne de caractères (le type) ou un objet d'événement avec un attribut de type
 *
 * @param {Objet} [hash]
 *        hachage de données à transmettre avec l'événement
 *
 * @return {boolean|undefined}
 *         Retourne le contraire de `defaultPrevented` si la valeur par défaut est
 *         empêché. Sinon, renvoie `undefined`
 */
export function trigger(elem, event, hash) {
  // Récupère les données de l'élément et une référence au parent (pour les bulles).
  // Il n'est pas nécessaire d'ajouter un objet de données au cache pour chaque parent,
  // donc vérification de hasElData d'abord.
  const elemData = DomData.has(elem) ? DomData.get(elem) : {} ;
  const parent = elem.parentNode || elem.ownerDocument ;
  // type = event.type || event,
  // handler ;

  // Si un nom d'événement a été transmis sous forme de chaîne, crée un événement à partir de ce nom
  if (typeof event === 'string') {
    event = {type : event, target : elem} ;
  } else if (!event.target) {
    event.target = elem ;
  }

  // Normalise les propriétés de l'événement.
  event = fixEvent(event) ;

  // Si l'élément transmis possède un distributeur, il exécute les gestionnaires établis.
  if (elemData.dispatcher) {
    elemData.dispatcher.call(elem, event, hash) ;
  }

  // Sauf arrêt explicite ou si l'événement ne fait pas de bulles (par exemple, événements médiatiques)
  // appelle récursivement cette fonction pour faire remonter l'événement dans le DOM.
  if (parent && !event.isPropagationStopped() && event.bubbles === true) {
    trigger.call(null, parent, event, hash) ;

  // Si elle se trouve au sommet du DOM, elle déclenche l'action par défaut, à moins qu'elle ne soit désactivée.
  } else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) {
    if (!DomData.has(event.target)) {
      DomData.set(event.target, {}) ;
    }
    const targetData = DomData.get(event.target) ;

    // Vérifie si la cible a une action par défaut pour cet événement.
    if (event.target[event.type]) {
      // Désactive temporairement l'envoi d'événements sur la cible car nous avons déjà exécuté le gestionnaire.
      targetData.disabled = true ;
      // Exécute l'action par défaut.
      if (typeof event.target[event.type] === 'function') {
        event.target[event.type]() ;
      }
      // Réactive l'envoi d'événements.
      targetData.disabled = false ;
    }
  }

  // Informer le déclencheur si le défaut a été empêché en renvoyant false
  return !event.defaultPrevented ;
}

/**
 * Déclencher un écouteur une seule fois pour un événement.
 *
 * @param {Element|Objet} elem
 *        Élément ou objet à lier.
 *
 * @param {string|string[]} type
 *        Nom/type de l'événement
 *
 * @param {Event~EventListener} fn
 *        Fonction d'écoute d'événements
 */
export function one(elem, type, fn) {
  if (Array.isArray(type)) {
    return _handleMultipleEvents(one, elem, type, fn) ;
  }
  const func = function() {
    off(elem, type, func) ;
    fn.apply(this, arguments) ;
  };

  // copie le guid dans la nouvelle fonction afin qu'elle puisse être supprimée en utilisant l'ID de la fonction d'origine
  func.guid = fn.guid = fn.guid || Guid.newGUID() ;
  on(elem, type, func) ;
}

/**
 * Déclencher un écouteur une seule fois puis le désactiver pour tous
 * événements configurés
 *
 * @param {Element|Objet} elem
 *        Élément ou objet à lier.
 *
 * @param {string|string[]} type
 *        Nom/type de l'événement
 *
 * @param {Event~EventListener} fn
 *        Fonction d'écoute d'événements
 */
export function any(elem, type, fn) {
  const func = function() {
    off(elem, type, func) ;
    fn.apply(this, arguments) ;
  };

  // copie le guid dans la nouvelle fonction afin qu'elle puisse être supprimée en utilisant l'ID de la fonction d'origine
  func.guid = fn.guid = fn.guid || Guid.newGUID() ;

  // plusieurs fois par jour, mais une seule fois par jour pour tout
  on(elem, type, func) ;
}