import { ArrayUtils } from './utils/ArrayUtils';
import { Timeout } from './utils/Timeout';
/**
 * Function interface for event listeners on the {@link EventDispatcher}.
 */
export interface EventListener<Sender, Args> {
  (sender: Sender, args: Args): void;
}

/**
 * Empty type for creating {@link EventDispatcher event dispatchers} that do not carry any arguments.
 */
export interface NoArgs {}

/**
 * Event args for an event that can be canceled.
 */
export interface CancelEventArgs extends NoArgs {
  /**
   * Gets or sets a flag whether the event should be canceled.
   */
  cancel?: boolean;
}

/**
 * Public interface that represents an event. Can be used to subscribe to and unsubscribe from events.
 */
export interface Event<Sender, Args> {
  /**
   * Subscribes an event listener to this event dispatcher.
   * @param listener the listener to add
   */
  subscribe(listener: EventListener<Sender, Args>): void;

  /**
   * Subscribes an event listener to this event dispatcher that is only called once.
   * @param listener the listener to add
   */
  subscribeOnce(listener: EventListener<Sender, Args>): void;

  /**
   * Subscribes an event listener to this event dispatcher that will be called at a limited rate with a minimum
   * interval of the specified milliseconds.
   * @param listener the listener to add
   * @param rateMs the rate in milliseconds to which calling of the listeners should be limited
   */
  subscribeRateLimited(listener: EventListener<Sender, Args>, rateMs: number): void;

  /**
   * Unsubscribes a subscribed event listener from this dispatcher.
   * @param listener the listener to remove
   * @returns {boolean} true if the listener was successfully unsubscribed, false if it isn't subscribed on this
   *   dispatcher
   */
  unsubscribe(listener: EventListener<Sender, Args>): boolean;
}

/**
 * Event dispatcher to subscribe and trigger events. Each event should have its own dispatcher.
 */
export class EventDispatcher<Sender, Args> implements Event<Sender, Args> {
  private listeners: EventListenerWrapper<Sender, Args>[] = [];

  constructor() {}

  /**
   * {@inheritDoc}
   */
  subscribe(listener: EventListener<Sender, Args>) {
    this.listeners.push(new EventListenerWrapper(listener));
  }

  /**
   * {@inheritDoc}
   */
  subscribeOnce(listener: EventListener<Sender, Args>) {
    this.listeners.push(new EventListenerWrapper(listener, true));
  }

  /**
   * {@inheritDoc}
   */
  subscribeRateLimited(listener: EventListener<Sender, Args>, rateMs: number) {
    this.listeners.push(new RateLimitedEventListenerWrapper(listener, rateMs));
  }

  /**
   * {@inheritDoc}
   */
  unsubscribe(listener: EventListener<Sender, Args>): boolean {
    // Iterate through listeners, compare with parameter, and remove if found
    // NOTE: In case we ever remove all matching listeners instead of just the first, we need to reverse-iterate here
    for (let i = 0; i < this.listeners.length; i++) {
      const subscribedListener = this.listeners[i];
      if (subscribedListener.listener === listener) {
        subscribedListener.clear();
        ArrayUtils.remove(this.listeners, subscribedListener);
        return true;
      }
    }

    return false;
  }

  /**
   * Removes all listeners from this dispatcher.
   */
  unsubscribeAll(): void {
    // In case of RateLimitedEventListenerWrapper we need to make sure that the timeout callback won't be called
    for (const listener of this.listeners) {
      listener.clear();
    }

    this.listeners = [];
  }

  /**
   * Dispatches an event to all subscribed listeners.
   * @param sender the source of the event
   * @param args the arguments for the event
   */
  dispatch(sender: Sender, args: Args = null) {
    const listenersToRemove = [];

    // Call every listener
    // We iterate over a copy of the array of listeners to avoid the case where events are not fired on listeners when
    // listeners are unsubscribed from within the event handlers during a dispatch (because the indices change and
    // listeners are shifted within the array).
    // This means that listener x+1 will still be called if unsubscribed from within the handler of listener x, as well
    // as listener y+1 will not be called when subscribed from within the handler of listener y.
    // Array.slice(0) is the fastest array copy method according to: https://stackoverflow.com/a/21514254/370252
    const listeners = this.listeners.slice(0);
    for (const listener of listeners) {
      listener.fire(sender, args);

      if (listener.isOnce()) {
        listenersToRemove.push(listener);
      }
    }

    // Remove one-time listener
    for (const listenerToRemove of listenersToRemove) {
      ArrayUtils.remove(this.listeners, listenerToRemove);
    }
  }

  /**
   * Returns the event that this dispatcher manages and on which listeners can subscribe and unsubscribe event handlers.
   * @returns {Event}
   */
  getEvent(): Event<Sender, Args> {
    // For now, just cast the event dispatcher to the event interface. At some point in the future when the
    // codebase grows, it might make sense to split the dispatcher into separate dispatcher and event classes.
    return <Event<Sender, Args>>this;
  }
}

/**
 * A basic event listener wrapper to manage listeners within the {@link EventDispatcher}. This is a 'private' class
 * for internal dispatcher use and it is therefore not exported.
 */
class EventListenerWrapper<Sender, Args> {
  private eventListener: EventListener<Sender, Args>;
  private once: boolean;

  constructor(listener: EventListener<Sender, Args>, once: boolean = false) {
    this.eventListener = listener;
    this.once = once;
  }

  /**
   * Returns the wrapped event listener.
   * @returns {EventListener<Sender, Args>}
   */
  get listener(): EventListener<Sender, Args> {
    return this.eventListener;
  }

  /**
   * Fires the wrapped event listener with the given arguments.
   * @param sender
   * @param args
   */
  fire(sender: Sender, args: Args) {
    this.eventListener(sender, args);
  }

  /**
   * Checks if this listener is scheduled to be called only once.
   * @returns {boolean} once if true
   */
  isOnce(): boolean {
    return this.once;
  }

  clear(): void {}
}

interface EventAttributes<Sender, Args> {
  sender: Sender;
  args: Args;
}

/**
 * Extends the basic {@link EventListenerWrapper} with rate-limiting functionality.
 */
class RateLimitedEventListenerWrapper<Sender, Args> extends EventListenerWrapper<Sender, Args> {
  private readonly rateMs: number;
  private readonly rateLimitingEventListener: EventListener<Sender, Args>;

  // save last seen event attributes
  private lastSeenEvent: EventAttributes<Sender, Args>;

  private rateLimitTimout: Timeout;

  constructor(listener: EventListener<Sender, Args>, rateMs: number) {
    super(listener); // sets the event listener sink

    this.rateMs = rateMs;

    // starting limiting the events to the given value
    const startRateLimiting = () => {
      this.rateLimitTimout.start();
    };

    // timout for limiting the events
    this.rateLimitTimout = new Timeout(this.rateMs, () => {
      if (this.lastSeenEvent) {
        this.fireSuper(this.lastSeenEvent.sender, this.lastSeenEvent.args);
        startRateLimiting(); // start rateLimiting again to keep rate limit active even after firing the last seen event
        this.lastSeenEvent = null;
      }
    });

    // In case the events stopping during the rateLimiting we need to track the last seen one and delegate after the
    // rate limiting is finished. This prevents missing the last update due to the rate limit.
    this.rateLimitingEventListener = (sender: Sender, args: Args) => {
      // only fire events if the rateLimiting is not running
      if (this.shouldFireEvent()) {
        this.fireSuper(sender, args);
        startRateLimiting();
        return;
      }

      this.lastSeenEvent = {
        sender: sender,
        args: args,
      };
    };
  }

  private shouldFireEvent(): boolean {
    return !this.rateLimitTimout.isActive();
  }

  private fireSuper(sender: Sender, args: Args) {
    // Fire the actual external event listener
    super.fire(sender, args);
  }

  fire(sender: Sender, args: Args) {
    // Fire the internal rate-limiting listener instead of the external event listener
    this.rateLimitingEventListener(sender, args);
  }

  clear(): void {
    super.clear();
    this.rateLimitTimout.clear();
  }
}
