import { ElementObserver } from './elementObserver';
import { NodeSelector } from './nodeSelector';

/**
 * This class manages a `MutationObserver` for the `document.body`. It monitors nodes that are
 * added or removed from the DOM and notifies the observers for each observable element
 * when they are ready to be watched.
 *
 * How that works:
 *
 * When the `observe` function is called, it attaches a `MutationObserver` to the `document.body` element.
 * All changes to `document.body` are tracked by this observer. When changes are detected, the `MutationObserver`
 * iterates through all added and removed nodes and checks if each node exists in the main map of observable elements.
 *
 * If a match is found, it notifies the corresponding `ElementObserver` instance to start monitoring changes for that element.
 *
 * The primary `MutationObserver` remains active until the `destroy` function is called. Calling `destroy` will disconnect all
 * individual element observers and release any associated resources.
 *
 * There is no need to explicitly call the `observe` function. It is automatically invoked when a `useElementObserver` hook is instantiated.
 *
 */
export class DOMObserver {
  private static documentObserver: MutationObserver | null;

  private static listeners: Map<string, ElementObserver> = new Map();

  private static notifyAdition(records: MutationRecord[], nodeMapping: Map<Node, ElementObserver>) {
    records.forEach(record =>
      record.addedNodes.forEach(node => {
        nodeMapping.get(node)?.watch(node);
        nodeMapping.delete(node);
      }),
    );
  }

  private static notifyRemoval(records: MutationRecord[], nodeMapping: Map<Node, ElementObserver>) {
    records.forEach(record =>
      record.removedNodes.forEach(node => {
        nodeMapping.get(node)?.destroy();
        nodeMapping.delete(node);
      }),
    );
  }

  private static notifyChildrenChanges(nodeMapping: Map<Node, ElementObserver>) {
    Array.from(nodeMapping.values()).forEach(observer => observer.synchronize());
  }

  private static mapNodeInstancesToListeners(): Map<Node, ElementObserver> {
    const nodeMapping = new Map<Node, ElementObserver>();
    this.listeners.forEach(observer => {
      const node = observer.getNode() || observer.getSelector()?.parse();
      if (node) {
        nodeMapping.set(node, observer);
      }
    });
    return nodeMapping;
  }

  static add(selector: NodeSelector): ElementObserver {
    const node = selector.getPattern();
    let elementObserver = this.listeners.get(node);
    if (elementObserver) {
      return elementObserver;
    }

    elementObserver = new ElementObserver(selector);
    this.listeners.set(node, elementObserver);
    return elementObserver;
  }

  static remove(selector: NodeSelector) {
    const node = selector.getPattern();
    this.listeners.get(node)?.destroy();
    this.listeners.delete(node);
  }

  static observe() {
    if (!this.documentObserver) {
      this.documentObserver = new MutationObserver(mutations => {
        const nodeMapping = this.mapNodeInstancesToListeners();
        this.notifyAdition(mutations, nodeMapping);
        this.notifyRemoval(mutations, nodeMapping);
        this.notifyChildrenChanges(nodeMapping);
      });
      this.documentObserver.observe(document.body, { childList: true, subtree: true });
    }
  }

  static destroy() {
    this.documentObserver?.disconnect();
    this.listeners.forEach(listener => listener.destroy());
    this.listeners.clear();
    this.documentObserver = null;
  }
}
