import { transitionHiddenElement } from "@cloudfour/transition-hidden-element";
import {
  createPopper,
  Instance as PopperInstance,
  Options as PopperOptions,
} from "@popperjs/core";

import { keys } from "../../utils";

export interface TooltipConfig {
  rootContainerSelector: string;
  removeDialogOnDestroy: boolean;
  popperOptions: PopperOptions;
}

export const defaultConfig: TooltipConfig = {
  rootContainerSelector: "#root-tooltips",
  removeDialogOnDestroy: false,
  popperOptions: {
    placement: "bottom",
    strategy: "absolute",
    modifiers: [
      {
        name: "arrow",
        options: {
          element: ".tooltip__arrow",
          padding: 10,
        },
      },
      {
        name: "eventListeners",
        options: {
          scroll: false,
          resize: false,
        },
      },
      {
        name: "flip",
        options: {
          fallbackPlacements: ["right", "top", "auto"],
        },
      },
      {
        name: "offset",
        options: {
          offset: ({ placement }: { placement: string }) => {
            if (placement.includes("start")) {
              return [-10, 0];
            } else if (placement.includes("end")) {
              return [10, 0];
            }
            return [0, 0];
          },
        },
      },
    ],
  },
};

export default class Tooltip {
  public element: HTMLElement;
  public config: TooltipConfig;
  public instance!: PopperInstance;
  public trigger!: HTMLElement;
  public rootContainer?: HTMLElement | null;
  public transitioner: any;
  private isOpen = false;
  private ignoreNextBlur = false;

  constructor(element: HTMLElement, config?: Partial<TooltipConfig>) {
    this.element = element;
    this.config = { ...defaultConfig, ...config };
    this.config.popperOptions = {
      ...defaultConfig.popperOptions,
      ...(config?.popperOptions || {}),
    };
    this.config.popperOptions.modifiers = [
      ...defaultConfig.popperOptions.modifiers!,
      ...(config?.popperOptions?.modifiers || []),
    ];

    this.handleVisibility = this.handleVisibility.bind(this);
    this.onKeydown = this.onKeydown.bind(this);
    this.onDialogMouseDown = this.onDialogMouseDown.bind(this);
    this.onDocumentClick = this.onDocumentClick.bind(this);

    (this.element as any).ODS_Tooltip = this;

    this.init();

    return this;
  }

  static getInstance(el: HTMLElement): Tooltip | null {
    return el && (el as any).ODS_Tooltip ? (el as any).ODS_Tooltip : null;
  }

  wrapLastWordAndButton(): void {
    if (!this.element.hasAttribute("data-tooltip-keep-with-text")) return;
    const previousSibling = this.trigger.previousSibling;
    if (!previousSibling || previousSibling.nodeType !== Node.TEXT_NODE) {
      console.warn("Previous sibling is not a text node.");
      return;
    }
    const textContent = previousSibling.textContent?.trim() || "";
    const words = textContent.split(" ");
    const lastWord = words.pop();
    const updatedTextContent = words.join(" ");
    previousSibling.textContent = updatedTextContent + " ";
    const span = document.createElement("span");
    span.className = "text-nowrap";
    span.innerHTML = `${lastWord} ${this.trigger.outerHTML}`;
    this.trigger.parentNode?.replaceChild(span, this.trigger);
    this.trigger = span.querySelector("[data-tooltip-trigger]") as HTMLElement;
  }

  private init(): void {
    this.trigger = document.querySelector(
      `[data-tooltip-trigger][aria-describedby=${this.element.getAttribute("id")}]`,
    ) as HTMLElement;
    if (!this.trigger) {
      console.warn(
        `tooltip trigger element is not defined and the tooltip cannot be attached. Please provide an element with [aria-describedby=${this.element.getAttribute("id")}]`,
      );
    }
    if (this.element.hasAttribute("data-tooltip-keep-with-text")) {
      this.wrapLastWordAndButton();
    }
    this.rootContainer = document.querySelector(
      this.config.rootContainerSelector,
    );
    if (this.rootContainer) {
      this.rootContainer.appendChild(this.element);
    } else {
      console.warn(
        "rootContainer element is not defined. Tooltips will be placed inside content which can affect positioning. Please provide rootContainer element (should be placed outside of main content, usually in end of <body /> tag)",
      );
    }
    const customPlacement = this.element.getAttribute("data-tooltip-placement");
    if (customPlacement) {
      this.config.popperOptions.placement = customPlacement as any;
    }
    this.instance = createPopper(
      this.trigger,
      this.element,
      this.config.popperOptions,
    );
    this.transitioner = transitionHiddenElement({
      element: this.element,
      visibleClass: "is-visible",
    });
    this.element.addEventListener("mousedown", this.onDialogMouseDown);
    this.trigger.addEventListener("click", this.handleVisibility);
    this.trigger.addEventListener("keydown", this.handleVisibility);
    this.trigger.addEventListener("blur", this.handleVisibility);
  }

  private onDialogMouseDown(): void {
    this.ignoreNextBlur = true;
  }

  private onDocumentClick(e: MouseEvent): void {
    const target = e.target as Node | null;

    if (!target) {
      return;
    }

    if (this.trigger.contains(target) || this.element.contains(target)) {
      return;
    }

    this.hide();
  }

  private handleVisibility(e: Event): void {
    if (e.type === "click") {
      this.toggle();
      return;
    }
    if (e.type === "keydown") {
      this.onKeydown(e as KeyboardEvent);
      return;
    }
    if (e.type === "blur") {
      if (this.ignoreNextBlur) {
        this.ignoreNextBlur = false;
        return;
      }
      this.hide();
    }
  }

  private onKeydown(e: KeyboardEvent): void {
    if (
      (e.key === "Enter" || e.key === " ") &&
      this.trigger.tagName !== "BUTTON"
    ) {
      e.preventDefault();
      this.toggle();
      return;
    }
    if (keys.ESC.includes(e.key)) {
      this.hide();
    }
  }

  private toggle(): void {
    if (this.isOpen) {
      this.hide();
    } else {
      this.show();
    }
  }

  public show(): void {
    this.transitioner.show();
    this.trigger.classList.add("is-active");
    this.isOpen = true;
    this.instance.forceUpdate();
    document.addEventListener("click", this.onDocumentClick);
  }

  public hide(): void {
    this.transitioner.hide();
    this.trigger.classList.remove("is-active");
    this.isOpen = false;
    document.removeEventListener("click", this.onDocumentClick);
  }

  public update(): void {
    this.instance.forceUpdate();
  }

  public destroy(): void {
    this.element.removeEventListener("mousedown", this.onDialogMouseDown);
    this.trigger.removeEventListener("click", this.handleVisibility);
    this.trigger.removeEventListener("keydown", this.handleVisibility);
    this.trigger.removeEventListener("blur", this.handleVisibility);
    document.removeEventListener("click", this.onDocumentClick);
    this.instance.destroy();
    (this.element as any).ODS_Tooltip = null;
    if (this.config.removeDialogOnDestroy) {
      this.element.remove();
    }
  }
}
