/**
 * @Datei events.js. Ein Ereignis-System (John Resig - Geheimnisse eines JS Ninja http://jsninja.com/)
 * (Die ursprüngliche Buchversion war nicht vollständig brauchbar, daher wurden einige Dinge korrigiert und der Closure Compiler kompatibel gemacht)
 * Dies sollte sehr ähnlich zu jQuery's Ereignisse arbeiten, aber es ist aus dem Buch-Version, die nicht so ist basiert
 * robust wie die von Jquery, daher gibt es wahrscheinlich einige Unterschiede.
 *
 * @Datei events.js
 * @Modul Ereignisse
 * /
domData von './dom-data' importieren;
importiere * als Guid aus './guid.js';
import log from './log.js';
import window from 'global/window';
dokument aus 'global/document' importieren;

/**
 * Bereinigung des Hörer-Caches und der Dispatcher
 *
 * @param {Element|Objekt} elem
 *        Element zum Aufräumen
 *
 * @param {string} type
 *        Art des zu bereinigenden Ereignisses
 * /
function _cleanUpEvents(elem, type) {
  if (!DomData.has(elem)) {
    rückkehr;
  }
  const data = DomData.get(elem);

  // Entfernen Sie die Ereignisse eines bestimmten Typs, wenn es keine mehr gibt
  if (data.handlers[type].length === 0) {
    data.handlers[type] löschen;
    // data.handlers[type] = null;
    // Das Setzen auf Null verursachte einen Fehler bei data.handlers

    // Entfernen Sie den Meta-Handler aus dem Element
    if (elem.removeEventListener) {
      elem.removeEventListener(type, data.dispatcher, false);
    } else if (elem.detachEvent) {
      elem.detachEvent('on' + type, data.dispatcher);
    }
  }

  // Entfernen Sie das Ereignisobjekt, wenn es keine Typen mehr gibt
  if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
    data.handlers löschen;
    data.dispatcher löschen;
    data.disabled löschen;
  }

  // Abschließend werden die Elementdaten entfernt, wenn keine Daten mehr vorhanden sind
  if (Object.getOwnPropertyNames(data).length === 0) {
    DomData.delete(elem);
  }
}

/**
 * Führt eine Schleife durch ein Array von Ereignistypen und ruft die gewünschte Methode für jeden Typ auf.
 *
 * @param {Funktion} fn
 *        Die Ereignismethode, die wir verwenden wollen.
 *
 * @param {Element|Objekt} elem
 *        Element oder Objekt, an das Zuhörer gebunden werden sollen
 *
 * @param {string} type
 *        Typ des Ereignisses, an das gebunden werden soll.
 *
 * @param {EventTarget~EventListener} callback
 *        Ereignis-Hörer.
 * /
function _handleMultipleEvents(fn, elem, types, callback) {
  types.forEach(function(type) {
    // Aufruf der Ereignismethode für jeden der Typen
    fn(elem, typ, rückruf);
  });
}

/**
 * Reparieren eines nativen Ereignisses, um Standard-Eigenschaftswerte zu haben
 *
 * @param {Object} event
 *        Zu behebendes Ereignisobjekt.
 *
 * @return {Object}
 *         Festes Ereignisobjekt.
 * /
export function fixEvent(event) {
  if (event.fixed_) {
    ereignis zurückgeben;
  }

  function returnTrue() {
    true zurückgeben;
  }

  function returnFalse() {
    return false;
  }

  // Testen, ob eine Reparatur erforderlich ist
  // Wird verwendet, um zu prüfen, ob !event.stopPropagation anstelle von isPropagationStopped
  // Aber native Ereignisse geben für stopPropagation true zurück, haben aber keine
  // andere erwartete Methoden wie isPropagationStopped. Scheint ein Problem zu sein
  // mit dem Javascript-Ninja-Code. Wir überschreiben also jetzt einfach alle Ereignisse.
  if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
    const old = event || window.event;

    ereignis = {};
    // Klonen Sie das alte Objekt, damit wir die Werte ändern können event = {};
    // IE8 mag es nicht, wenn man mit nativen Ereigniseigenschaften herumspielt
    // Firefox liefert false für event.hasOwnProperty('type') und andere Requisiten
    // was das Kopieren erschwert.
    // TODO: Wahrscheinlich ist es am besten, eine Whitelist von Ereignisrequisiten zu erstellen
    for (const key in old) {
      // Safari 6.0.3 warnt Sie, wenn Sie versuchen, veraltete layerX/Y zu kopieren
      // Chrome warnt Sie, wenn Sie versuchen, die veraltete keyboardEvent.keyLocation zu kopieren
      // und webkitMovementX/Y
      // Lighthouse beschwert sich, wenn Event.path kopiert wird
      if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' &&
          key !== 'webkitMovementX' && key !== 'webkitMovementY' &&
          schlüssel !== 'Pfad') {
        // Chrome 32+ warnt, wenn Sie versuchen, den veralteten returnValue zu kopieren, aber
        // Wir wollen das auch dann, wenn preventDefault nicht unterstützt wird (IE8).
        if (!(key === 'returnValue' && old.preventDefault)) {
          event[key] = old[key];
        }
      }
    }

    // Das Ereignis ist bei diesem Element eingetreten
    if (!event.target) {
      event.target = event.srcElement || document;
    }

    // Handle, auf welches andere Element sich das Ereignis bezieht
    if (!event.relatedTarget) {
      event.relatedTarget = event.fromElement === event.target ?
        event.toElement :
        event.fromElement;
    }

    // Anhalten der Standard-Browser-Aktion
    event.preventDefault = function() {
      if (old.preventDefault) {
        old.preventDefault();
      }
      event.returnValue = false;
      old.returnValue = false;
      event.defaultPrevented = true;
    };

    event.defaultPrevented = false;

    // Stoppen Sie das Blubbern des Ereignisses
    event.stopPropagation = function() {
      if (old.stopPropagation) {
        old.stopPropagation();
      }
      event.cancelBubble = true;
      old.cancelBubble = true;
      event.isPropagationStopped = returnTrue;
    };

    event.isPropagationStopped = returnFalse;

    // Stoppen des Ereignisses und Ausführen anderer Handler
    event.stopImmediatePropagation = function() {
      if (old.stopImmediatePropagation) {
        old.stopImmediatePropagation();
      }
      event.isImmediatePropagationStopped = returnTrue;
      event.stopPropagation();
    };

    event.isImmediatePropagationStopped = returnFalse;

    // Behandlung der Mausposition
    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);
    }

    // Behandlung von Tastendrucken
    event.which = event.charCode || event.keyCode;

    // Schaltfläche für Mausklicks fixieren:
    // 0 == links; 1 == Mitte; 2 == rechts
    if (event.button !== null && event.button !== undefined) {

      // Das Folgende ist deaktiviert, da es den videojs-standard nicht erfüllt
      // und... igitt.
      /* eslint-disable */
      event.button = (event.button & 1 ? 0 :
        (event.button & 4 ? 1 :
          (event.button & 2 ? 2 : 0)));
      /* eslint-aktivieren */
    }
  }

  event.fixed_ = true;
  // Rückgabe der reparierten Instanz
  ereignis zurückgeben;
}

/**
 * Ob passive Ereignislistener unterstützt werden
 * /
let _supportsPassive;

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

      window.addEventListener('test', null, opts);
      window.removeEventListener('test', null, opts);
    } catch (e) {
      // außer Acht lassen
    }
  }

  return _supportsPassive;
};

/**
 * Berührungsereignisse, von denen Chrome erwartet, dass sie passiv sind
 * /
const passiveEreignisse = [
  touchstart',
  touchmove
];

/**
 * Hinzufügen eines Ereignis-Listeners zum Element
 * Es speichert die Handler-Funktion in einem separaten Cache-Objekt
 * und fügt dem Ereignis des Elements einen allgemeinen Handler hinzu,
 * zusammen mit einer eindeutigen ID (guid) für das Element.
 *
 * @param {Element|Objekt} elem
 *        Element oder Objekt, an das Zuhörer gebunden werden sollen
 *
 * @param {string|string[]} type
 *        Typ des Ereignisses, an das gebunden werden soll.
 *
 * @param {EventTarget~EventListener} fn
 *        Ereignis-Hörer.
 * /
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);

  // Wir brauchen einen Ort, an dem wir alle unsere Handler-Daten speichern können
  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) {
        rückkehr;
      }

      event = fixEvent(event);

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

      if (Handler) {
        // Kopieren Sie die Handler, damit beim Hinzufügen/Entfernen von Handlern während des Prozesses nicht alles durcheinander gerät.
        const handlersCopy = handlers.slice(0);

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

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

      wenn (unterstütztPassiv() &&
        passiveEvents.indexOf(type) > -1) {
        optionen = {passiv: true};
      }
      elem.addEventListener(type, data.dispatcher, options);
    } else if (elem.attachEvent) {
      elem.attachEvent('on' + type, data.dispatcher);
    }
  }
}

/**
 * Entfernt Ereignis-Listener von einem Element
 *
 * @param {Element|Objekt} elem
 *        Objekt, von dem Hörer entfernt werden sollen.
 *
 * @param {string|string[]} [Typ]
 *        Typ des zu entfernenden Hörers. Schließen Sie nicht ein, um alle Ereignisse aus dem Element zu entfernen.
 *
 * @param {EventTarget~EventListener} [fn]
 *        Spezifischer Hörer zum Entfernen. Hörer für ein Ereignis nicht entfernen
 *        typ.
 * /
export function off(elem, type, fn) {
  // Kein Cache-Objekt über getElData hinzufügen, wenn es nicht benötigt wird
  if (!DomData.has(elem)) {
    rückkehr;
  }

  const data = DomData.get(elem);

  // Wenn keine Ereignisse vorhanden sind, gibt es nichts zu entbinden
  if (!data.handlers) {
    rückkehr;
  }

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

  // Utility-Funktion
  const removeType = function(el, t) {
    data.handlers[t] = [];
    _cleanUpEvents(el, t);
  };

  // Werden alle gebundenen Ereignisse entfernt?
  if (type === undefined) {
    for (const t in data.handlers) {
      if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
        removeType(elem, t);
      }
    }
    rückkehr;
  }

  const handlers = data.handlers[type];

  // Wenn keine Handler vorhanden sind, gibt es nichts zu entbinden
  if (!handlers) {
    rückkehr;
  }

  // Wenn kein Hörer angegeben wurde, werden alle Hörer für den Typ
  if (!fn) {
    removeType(elem, type);
    rückkehr;
  }

  // Wir entfernen nur einen einzigen Handler
  if (fn.guid) {
    for (let n = 0; n < handlers.length; n++) {
      if (handlers[n].guid === fn.guid) {
        handlers.splice(n--, 1);
      }
    }
  }

  _cleanUpEvents(elem, type);
}

/**
 * Auslösen eines Ereignisses für ein Element
 *
 * @param {Element|Objekt} elem
 *        Element zum Auslösen eines Ereignisses auf
 *
 * @param {EventTarget~Event|string} event
 *        Eine Zeichenkette (der Typ) oder ein Ereignisobjekt mit einem Typ-Attribut
 *
 * @param {Object} [Raute]
 *        datenhash, der mit dem Ereignis weitergegeben wird
 *
 * @return {boolean|undefined}
 *         Gibt das Gegenteil von `defaultPrevented` zurück, wenn default war
 *         verhindert. Andernfalls wird "undefiniert" zurückgegeben
 * /
export function trigger(elem, event, hash) {
  // Holt Elementdaten und einen Verweis auf das übergeordnete Element (für Bubbling).
  // Es soll nicht für jedes Elternteil ein Datenobjekt in den Cache aufgenommen werden,
  // also zuerst hasElData prüfen.
  const elemData = DomData.has(elem) ? DomData.get(elem) : {};
  const parent = elem.parentNode || elem.ownerDocument;
  // type = event.type || event,
  // handler;

  // Wenn ein Ereignisname als String übergeben wurde, wird daraus ein Ereignis erzeugt
  if (typeof event === 'string') {
    event = {Typ: event, Ziel: elem};
  } else if (!event.target) {
    event.target = elem;
  }

  // Normalisiert die Ereigniseigenschaften.
  event = fixEvent(event);

  // Wenn das übergebene Element einen Dispatcher hat, werden die festgelegten Handler ausgeführt.
  if (elemData.dispatcher) {
    elemData.dispatcher.call(elem, event, hash);
  }

  // Es sei denn, das Ereignis wird explizit gestoppt oder es gibt keine Blasenbildung (z. B. bei Medienereignissen)
  // ruft diese Funktion rekursiv auf, um das Ereignis im DOM nach oben zu blasen.
  if (parent && !event.isPropagationStopped() && event.bubbles === true) {
    trigger.call(null, parent, event, hash);

  // Wenn es sich am oberen Rand des DOM befindet, löst es die Standardaktion aus, sofern es nicht deaktiviert ist.
  } 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);

    // Überprüft, ob das Ziel eine Standardaktion für dieses Ereignis hat.
    if (event.target[event.type]) {
      // Deaktiviert vorübergehend die Ereignisauslösung auf dem Ziel, da wir den Handler bereits ausgeführt haben.
      targetData.disabled = true;
      // Führt die Standardaktion aus.
      if (typeof event.target[event.type] === 'function') {
        event.target[event.type]();
      }
      // Aktiviert die Ereignisverteilung erneut.
      targetData.disabled = false;
    }
  }

  // Informieren Sie den Auslöser, wenn der Standard verhindert wurde, indem Sie false zurückgeben
  return !event.defaultPrevented;
}

/**
 * Lösen Sie einen Listener nur einmal für ein Ereignis aus.
 *
 * @param {Element|Objekt} elem
 *        Element oder Objekt, an das gebunden werden soll.
 *
 * @param {string|string[]} type
 *        Name/Typ der Veranstaltung
 *
 * @param {Event~EventListener} fn
 *        Ereignis-Listener-Funktion
 * /
export function one(elem, type, fn) {
  if (Array.isArray(type)) {
    return _handleMultipleEvents(one, elem, type, fn);
  }
  const func = function() {
    aus(elem, typ, func);
    fn.apply(this, arguments);
  };

  // Kopieren Sie die Guid in die neue Funktion, damit sie mit der ID der ursprünglichen Funktion entfernt werden kann
  func.guid = fn.guid = fn.guid || Guid.newGUID();
  on(elem, typ, func);
}

/**
 * Einen Hörer nur einmal auslösen und dann für alle ausschalten
 * konfigurierte Ereignisse
 *
 * @param {Element|Objekt} elem
 *        Element oder Objekt, an das gebunden werden soll.
 *
 * @param {string|string[]} type
 *        Name/Typ der Veranstaltung
 *
 * @param {Event~EventListener} fn
 *        Ereignis-Listener-Funktion
 * /
export function any(elem, type, fn) {
  const func = function() {
    aus(elem, typ, func);
    fn.apply(this, arguments);
  };

  // Kopieren Sie die Guid in die neue Funktion, damit sie mit der ID der ursprünglichen Funktion entfernt werden kann
  func.guid = fn.guid = fn.guid || Guid.newGUID();

  // mehrere Einschaltungen, aber eine Ausschaltung für alles
  on(elem, typ, func);
}