import { ReactElement, useEffect, useRef, useState } from 'react';
import { DOMObserver } from 'src/application/utils/domObserver';
import { ElementObserver } from 'src/application/utils/elementObserver';
import { MouseEventTypes } from 'src/application/utils/mouseEventTypes';
import { NodeSelector } from 'src/application/utils/nodeSelector';

export interface ElementObserverUtility {
  onChange: (fn: (el: HTMLElement | null) => void) => void;
  onAdd: (fn: (el: HTMLElement | null) => void) => void;
  onRemove: (fn: (el: HTMLElement | null) => void) => void;
  onClassChange: (fn: (classes?: DOMTokenList) => void) => void;
  onClassAdded: (className: string, fn: () => void) => void;
  onClassRemoved: (className: string, fn: () => void) => void;
  onAddChild: (fn: (nodes?: DOMTokenList) => void) => void;
  onRemoveChild: (fn: (nodes?: DOMTokenList) => void) => void;
  onEvent: <K extends keyof HTMLElementEventMap>(
    event: K,
    fn: EventListenerOrEventListenerObject,
  ) => void;
  blockRendering: () => void;
  unblockRendering: () => void;
  exists: () => boolean;
  isRendered: () => boolean;
  render: (fn: () => ReactElement) => ReactElement | undefined;
  fire: (event: MouseEventTypes) => void;
  show: () => void;
  hide: () => void;
  applyStyle: (styles: React.CSSProperties) => void;
  targetNode: HTMLElement | null;
}

/*
 * This hook enables the connection between a `MutationObserver` and a React.js component.
 * It abstracts all the necessary validations and implementations to modify HTML elements,
 * monitor their changes, and interact with them. The only required input is the element's selector.
 *
 * How to use it:
 *
 * Let's assume we want to interact with the following element:
 * `<div id="awesome-content"><content /></div>`
 *
 * To hook into that element, simply do the following:
 * `const element = useElementObserver(SelectorFactory.id("awesome-content"))`
 *
 * With the `ElementObserver` instance in hand, we can interact with the HTML element without having
 * to manage its presence in the DOM. The `onAdd` and `onRemove` functions can be used to determine
 * whether the element is present in the DOM or not, and to ensure interaction occurs only when necessary.
 *
 * @param selector
 * @returns
 */
export const useElementObserver = (selector: NodeSelector): ElementObserverUtility => {
  const INITIAL_RENDER_STATE = true;
  const eoRef = useRef<ElementObserver | null>(null);
  const [renderBlocked, setRenderBlocked] = useState(INITIAL_RENDER_STATE);
  eoRef.current = DOMObserver.add(selector);

  /**
   * Fired every time the HTML element changes.
   *
   * @param fn
   */
  const onChange = (fn: (el: HTMLElement | null) => void) => {
    eoRef.current?.registerOnChange(fn);
  };

  /**
   * Fired when the HTML element is added to the DOM.
   *
   * @param fn
   */
  const onAdd = (fn: (el: HTMLElement | null) => void) => {
    eoRef.current?.registerOnAdd(fn);
  };

  /**
   * Fired when the HTML element is removed from the DOM.
   *
   * @param fn
   */
  const onRemove = (fn: (el: HTMLElement | null) => void) => {
    eoRef.current?.registerOnRemove(fn);
  };

  /**
   * Fired when the class of the HTML element changes.
   *
   * @param fn
   */
  const onClassChange = (fn: (classes?: DOMTokenList) => void) => {
    eoRef.current?.registerOnclassChange(fn);
  };

  /**
   * Fired when a class is added to the HTML element.
   *
   * @param className
   * @param fn
   */
  const onClassAdded = (className: string, fn: () => void) => {
    eoRef.current?.registerOnClassAdded(className, fn);
  };

  /**
   * Fired when a class is removed from the HTML element.
   *
   * @param className
   * @param fn
   */
  const onClassRemoved = (className: string, fn: () => void) => {
    eoRef.current?.registerOnClassRemoved(className, fn);
  };

  /**
   * Fired when a child element is added to the HTML element.
   *
   * @param fn
   */
  const onAddChild = (fn: (nodes?: DOMTokenList) => void) => {
    eoRef.current?.registerOnAddChild(fn);
  };

  /**
   * Fired when a child element is removed from the HTML element.
   *
   * @param fn
   */
  const onRemoveChild = (fn: (nodes?: DOMTokenList) => void) => {
    eoRef.current?.registerOnRemoveChild(fn);
  };

  /**
   * Allows hooking event listeners to the HTML element, such as `focus`, `blur`, etc.
   *
   * @param event
   * @param fn
   */
  const onEvent = <K extends keyof HTMLElementEventMap>(
    event: K,
    fn: EventListenerOrEventListenerObject,
  ) => {
    eoRef.current?.registerEvent(event, fn);
  };

  /**
   * Useful when rendering a React.js component inside the HTML element.
   *
   * @param fn
   * @returns
   */
  const render = (fn: () => ReactElement) => {
    if (!renderBlocked) {
      return eoRef.current?.render(fn);
    }
    return undefined;
  };

  /**
   * Checks if the element exists in the DOM.
   *
   * @returns
   */
  const exists = () => !!eoRef.current?.getNode();

  /**
   * Checks if rendering is unblocked.
   *
   * @returns
   */
  const isRendered = () => !renderBlocked;

  /**
   * Triggers an event for the HTML element.
   *
   * @param event
   */
  const fire = (event: MouseEventTypes) => {
    eoRef.current?.fire(event);
  };

  /**
   * Shows the HTML element.
   *
   * @returns
   */
  const show = () => eoRef.current?.show();

  /**
   * Hides the HTML element.
   *
   * @returns
   */
  const hide = () => eoRef.current?.hide();

  /**
   * Blocks the rendering of elements.
   *
   * @returns
   */
  const blockRendering = () => setRenderBlocked(true);

  /**
   * Unblocks the rendering of elements.
   *
   * @returns
   */
  const unblockRendering = () => setRenderBlocked(false);

  /**
   * Applies CSS styles to the HTML element.
   *
   * @param styles
   */
  const applyStyle = (styles: React.CSSProperties) => {
    const node = eoRef?.current?.getNode();
    return node && Object.assign(node.style, styles);
  };

  useEffect(() => {
    eoRef.current?.init();
    eoRef.current?.registerOnReset(() => setRenderBlocked(INITIAL_RENDER_STATE));
    DOMObserver.observe();
    return () => DOMObserver.remove(selector);
  }, [selector.getPattern()]);

  return {
    targetNode: eoRef.current?.getNode(),
    onChange,
    onAdd,
    onRemove,
    onClassChange,
    onClassAdded,
    onClassRemoved,
    onAddChild,
    onRemoveChild,
    onEvent,
    blockRendering,
    unblockRendering,
    exists,
    isRendered,
    render,
    fire,
    show,
    hide,
    applyStyle,
  } satisfies ElementObserverUtility;
};
