import type { EditorModules } from '../types-internal/editor-modules';
import type { EditorConfig } from '../../types';
import type { ModuleConfig } from '../types-internal/module-config';
import Listeners from './utils/listeners';
import type EventsDispatcher from './utils/events';
import type { EditorEventMap } from './events';

/**
 * The type <T> of the Module generic.
 * It describes the structure of nodes used in modules.
 */
export type ModuleNodes = object;

/**
 * @abstract
 * @class      Module
 * @classdesc  All modules inherits from this class.
 * @typedef {Module} Module
 * @property {object} config - Editor user settings
 * @property {EditorModules} Editor - List of Editor modules
 */
export default class Module<T extends ModuleNodes = Record<string, HTMLElement>> {
  /**
   * Each module can provide some UI elements that will be stored in this property
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  public nodes: T = {} as any;

  /**
   * Editor modules list
   *
   * @type {EditorModules}
   */
  protected Editor: EditorModules;

  /**
   * Editor configuration object
   *
   * @type {EditorConfig}
   */
  protected config: EditorConfig;

  /**
   * Editor event dispatcher class
   */
  protected eventsDispatcher: EventsDispatcher<EditorEventMap>;

  /**
   * Util for bind/unbind DOM event listeners
   */
  protected listeners: Listeners = new Listeners();

  /**
   * This object provides methods to push into set of listeners that being dropped when read-only mode is enabled
   */
  protected readOnlyMutableListeners = {
    /**
     * Assigns event listener on DOM element and pushes into special array that might be removed
     *
     * @param {EventTarget} element - DOM Element
     * @param {string} eventType - Event name
     * @param {Function} handler - Event handler
     * @param {boolean|AddEventListenerOptions} options - Listening options
     */
    on: (
      element: EventTarget,
      eventType: string,
      handler: (event: Event) => void,
      options: boolean | AddEventListenerOptions = false
    ): void => {
      this.mutableListenerIds.push(
        this.listeners.on(element, eventType, handler, options)
      );
    },

    /**
     * Clears all mutable listeners
     */
    clearAll: (): void => {
      for (const id of this.mutableListenerIds) {
        this.listeners.offById(id);
      }

      this.mutableListenerIds = [];
    },
  };

  /**
   * The set of listener identifiers which will be dropped in read-only mode
   */
  private mutableListenerIds: string[] = [];

  /**
   * @class
   * @param options - Module options
   * @param options.config - Module config
   * @param options.eventsDispatcher - Common event bus
   */
  constructor({ config, eventsDispatcher }: ModuleConfig) {
    if (new.target === Module) {
      throw new TypeError('Constructors for abstract class Module are not allowed.');
    }

    this.config = config;
    this.eventsDispatcher = eventsDispatcher;
  }

  /**
   * Editor modules setter
   *
   * @param {EditorModules} Editor - Editor's Modules
   */
  public set state(Editor: EditorModules) {
    this.Editor = Editor;
  }

  /**
   * Remove memorized nodes
   */
  public removeAllNodes(): void {
    for (const key in this.nodes) {
      const node = this.nodes[key];

      if (node instanceof HTMLElement) {
        node.remove();
      }
    }
  }

  /**
   * Returns true if current direction is RTL (Right-To-Left)
   */
  protected get isRtl(): boolean {
    return this.config.i18n.direction === 'rtl';
  }
}
