/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.util.FakeEventTarget');

goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.IReleasable');
goog.require('shaka.util.MultiMap');


/**
 * @summary A work-alike for EventTarget.  Only DOM elements may be true
 * EventTargets, but this can be used as a base class to provide event dispatch
 * to non-DOM classes.  Only FakeEvents should be dispatched.
 *
 * @implements {EventTarget}
 * @implements {shaka.util.IReleasable}
 * @exportInterface
 */
shaka.util.FakeEventTarget = class {
  constructor() {
    /**
     * @private {shaka.util.MultiMap<shaka.util.FakeEventTarget.ListenerType>}
     */
    this.listeners_ = new shaka.util.MultiMap();

    /**
     * The target of all dispatched events.  Defaults to |this|.
     * @type {EventTarget}
     */
    this.dispatchTarget = this;
  }

  /**
   * Add an event listener to this object.
   *
   * @param {string} type The event type to listen for.
   * @param {shaka.util.FakeEventTarget.ListenerType} listener The callback or
   *   listener object to invoke.
   * @param {(!AddEventListenerOptions|boolean)=} options Ignored.
   * @override
   * @exportInterface
   */
  addEventListener(type, listener, options) {
    if (!this.listeners_) {
      return;
    }
    this.listeners_.push(type, listener);
  }

  /**
   * Add an event listener to this object that is invoked for all events types
   * the object fires.
   *
   * @param {shaka.util.FakeEventTarget.ListenerType} listener The callback or
   *   listener object to invoke.
   * @exportInterface
   */
  listenToAllEvents(listener) {
    this.addEventListener(shaka.util.FakeEventTarget.ALL_EVENTS_, listener);
  }

  /**
   * Remove an event listener from this object.
   *
   * @param {string} type The event type for which you wish to remove a
   *   listener.
   * @param {shaka.util.FakeEventTarget.ListenerType} listener The callback or
   *   listener object to remove.
   * @param {(EventListenerOptions|boolean)=} options Ignored.
   * @override
   * @exportInterface
   */
  removeEventListener(type, listener, options) {
    if (!this.listeners_) {
      return;
    }
    this.listeners_.remove(type, listener);
  }

  /**
   * Dispatch an event from this object.
   *
   * @param {!Event} event The event to be dispatched from this object.
   * @return {boolean} True if the default action was prevented.
   * @override
   * @exportInterface
   */
  dispatchEvent(event) {
    // In many browsers, it is complex to overwrite properties of actual Events.
    // Here we expect only to dispatch FakeEvents, which are simpler.
    goog.asserts.assert(event instanceof shaka.util.FakeEvent,
        'FakeEventTarget can only dispatch FakeEvents!');

    if (!this.listeners_) {
      return true;
    }

    let listeners = this.listeners_.get(event.type) || [];
    const universalListeners =
      this.listeners_.get(shaka.util.FakeEventTarget.ALL_EVENTS_);
    if (universalListeners) {
      listeners = listeners.concat(universalListeners);
    }

    // Execute this event on listeners until the event has been stopped or we
    // run out of listeners.
    for (const listener of listeners) {
      // Do this every time, since events can be re-dispatched from handlers.
      event.target = this.dispatchTarget;
      event.currentTarget = this.dispatchTarget;

      try {
        // Check for the |handleEvent| member to test if this is a
        // |EventListener| instance or a basic function.
        if (listener.handleEvent) {
          listener.handleEvent(event);
        } else {
          // eslint-disable-next-line no-restricted-syntax
          listener.call(this, event);
        }
      } catch (exception) {
        // Exceptions during event handlers should not affect the caller,
        // but should appear on the console as uncaught, according to MDN:
        // https://mzl.la/2JXgwRo
        shaka.log.error('Uncaught exception in event handler', exception,
            exception ? exception.message : null,
            exception ? exception.stack : null);
      }

      if (event.stopped) {
        break;
      }
    }

    return event.defaultPrevented;
  }

  /**
   * @override
   * @exportInterface
   */
  release() {
    this.listeners_ = null;
  }
};

/**
 * These are the listener types defined in the closure extern for EventTarget.
 * @typedef {EventListener|function(!Event):*}
 * @exportInterface
 */
shaka.util.FakeEventTarget.ListenerType;


/**
 * @const {string}
 * @private
 */
shaka.util.FakeEventTarget.ALL_EVENTS_ = 'All';
