import { RovingTabindex } from "../../utils/keyboard";

interface TabsConfig {
  tabSelector: string;
}

export const defaultConfig: TabsConfig = {
  tabSelector: '[role="tab"]',
};

export const configDocs = {
  tabSelector: "Tab element selector",
};

export default class Tabs {
  private static readonly SCROLL_ALIGNMENT_TOLERANCE = 1;
  private static readonly SCROLL_EDGE_TOLERANCE = 1;

  private element: HTMLElement;
  private config: TabsConfig;
  private tabs: HTMLElement[];
  private tabPanels: (HTMLElement | null)[];
  private activeTabIndex: number | null;
  private rovingTabindex: RovingTabindex | null;
  private resizeObserver: ResizeObserver | null;
  private horizontalScrollHandler: () => void;
  private resizeHandler: () => void;
  private viewportElement: HTMLElement | null;

  constructor(element: HTMLElement, config?: Partial<TabsConfig>) {
    this.element = element;
    this.config = { ...defaultConfig, ...config };

    this.tabs = [];
    this.tabPanels = [];
    this.activeTabIndex = null;

    this.rovingTabindex = null;
    this.resizeObserver = null;
    this.viewportElement = null;

    this.handleClick = this.handleClick.bind(this);
    this.handleTabFocus = this.handleTabFocus.bind(this);
    this.horizontalScrollHandler =
      this.updateHorizontalOverflowState.bind(this);
    this.resizeHandler = this.updateHorizontalOverflowState.bind(this);

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

    this.init();

    return this;
  }

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

  private init(): void {
    this.tabs = Array.from(
      this.element.querySelectorAll<HTMLElement>(this.config.tabSelector),
    );

    this.tabs.map((tab, index) => {
      const controlId = tab.getAttribute("aria-controls");
      this.tabPanels.push(
        controlId ? document.getElementById(controlId) : null,
      );
      tab.addEventListener("click", this.handleClick);
      tab.addEventListener("focus", this.handleTabFocus);

      if (this.isSelected(tab)) {
        this.activeTabIndex = index;
      }

      return tab;
    });

    if (!this.rovingTabindex) {
      this.rovingTabindex = new RovingTabindex(this.tabs, {
        direction: "any",
      });
    } else {
      this.rovingTabindex.update(this.tabs);
    }

    if (this.activeTabIndex !== null) {
      this.setActiveTab(this.activeTabIndex);
    } else if (this.tabs.length > 0) {
      this.activateNthTab(0);
    }

    this.setupOverflowObserver();
  }

  destroy(): void {
    this.teardownOverflowObserver();

    this.tabs.map((tab) => {
      tab.removeEventListener("click", this.handleClick);
      tab.removeEventListener("focus", this.handleTabFocus);
      return tab;
    });

    this.tabs = [];
    this.tabPanels = [];
    if (this.rovingTabindex) {
      this.rovingTabindex.destroy();
    }
    (this.element as any).ODS_Tabs = null;
  }

  private setupOverflowObserver(): void {
    this.teardownOverflowObserver();

    this.viewportElement = this.element.parentElement?.classList.contains(
      "tab-list__viewport",
    )
      ? this.element.parentElement
      : null;

    this.element.addEventListener("scroll", this.horizontalScrollHandler, {
      passive: true,
    });
    window.addEventListener("resize", this.resizeHandler);

    if (typeof ResizeObserver !== "undefined") {
      this.resizeObserver = new ResizeObserver(this.resizeHandler);
      this.resizeObserver.observe(this.element);
      this.tabs.forEach((tab) => this.resizeObserver?.observe(tab));
    }

    this.updateHorizontalOverflowState();
  }

  private teardownOverflowObserver(): void {
    this.element.removeEventListener("scroll", this.horizontalScrollHandler);
    window.removeEventListener("resize", this.resizeHandler);

    if (this.viewportElement) {
      this.viewportElement.classList.remove(
        "has-left-overflow",
        "has-right-overflow",
      );
      this.viewportElement = null;
    }

    if (this.resizeObserver) {
      this.resizeObserver.disconnect();
      this.resizeObserver = null;
    }
  }

  private updateHorizontalOverflowState(): void {
    if (!this.viewportElement) return;

    const maxScrollLeft = this.element.scrollWidth - this.element.clientWidth;
    const hasOverflow = maxScrollLeft > Tabs.SCROLL_EDGE_TOLERANCE;
    const atStart = this.element.scrollLeft <= Tabs.SCROLL_EDGE_TOLERANCE;
    const atEnd =
      this.element.scrollLeft >= maxScrollLeft - Tabs.SCROLL_EDGE_TOLERANCE;

    this.viewportElement.classList.toggle(
      "has-left-overflow",
      hasOverflow && !atStart,
    );
    this.viewportElement.classList.toggle(
      "has-right-overflow",
      hasOverflow && !atEnd,
    );
  }

  update(): void {
    this.destroy();
    this.init();
  }

  private isActive = (el: HTMLElement | null): boolean =>
    el ? !el.hasAttribute("hidden") : false;

  private isSelected = (el: HTMLElement): boolean =>
    el.getAttribute("aria-selected") === "true";

  private setActiveTab(index: number, forceCenter: boolean = false): void {
    this.tabs.map((tab, i) => {
      if (i === index) {
        this.activeTabIndex = i;
        return this.toggleTab(tab, "on");
      }
      return this.toggleTab(tab, "off");
    });

    this.tabPanels.map((tabPanel, i) => {
      if (i === index) {
        return this.toggleTabPanel(tabPanel, "on");
      }
      return this.toggleTabPanel(tabPanel, "off");
    });

    const activeTab = this.tabs[index];
    if (activeTab) {
      this.scrollTabIntoView(activeTab, forceCenter);
    }
  }

  private scrollTabIntoView(
    activeTab: HTMLElement,
    forceCenter: boolean = false,
  ): void {
    const maxScrollLeft = Math.max(
      0,
      this.element.scrollWidth - this.element.clientWidth,
    );
    const contentRect = this.element.getBoundingClientRect();
    const itemRect = activeTab.getBoundingClientRect();
    const itemCenterWithinContent =
      itemRect.left -
      contentRect.left +
      this.element.scrollLeft +
      itemRect.width / 2;
    const targetScrollLeft =
      itemCenterWithinContent - this.element.clientWidth / 2;
    const behavior = window.innerWidth < 768 ? "auto" : "smooth";
    const nextScrollLeft = Math.min(
      maxScrollLeft,
      Math.max(0, targetScrollLeft),
    );

    const isAlreadyAligned =
      Math.abs(this.element.scrollLeft - nextScrollLeft) <=
      Tabs.SCROLL_ALIGNMENT_TOLERANCE;
    if (!forceCenter && isAlreadyAligned) return;

    if (typeof this.element.scrollTo === "function") {
      this.element.scrollTo({
        left: nextScrollLeft,
        behavior,
      });
      return;
    }

    this.element.scrollLeft = nextScrollLeft;
    this.updateHorizontalOverflowState();
  }

  private handleClick(e: MouseEvent): void {
    const clickedTab = e.currentTarget as HTMLElement;

    if (clickedTab.hasAttribute("aria-disabled")) {
      e.preventDefault();
      return;
    }

    const clickedIndex = this.tabs.findIndex((tab) => tab === clickedTab);
    if (clickedIndex >= 0) {
      this.setActiveTab(clickedIndex, true);
    }
  }

  private toggleTab(el: HTMLElement, state: "on" | "off"): void {
    const isActive = this.isSelected(el);

    if (state === "on" && !isActive) {
      el.setAttribute("aria-selected", "true");
      el.setAttribute("tabindex", "0");
    }

    if (state === "off" && isActive) {
      el.setAttribute("aria-selected", "false");
      el.setAttribute("tabindex", "-1");
    }
  }

  private toggleTabPanel(el: HTMLElement | null, state: "on" | "off"): void {
    if (!el) return;

    const isActive = this.isActive(el);

    if (state === "on" && !isActive) {
      el.removeAttribute("hidden");
    }

    if (state === "off" && isActive) {
      el.setAttribute("hidden", "");
    }
  }

  activateNthTab(index: number): void {
    this.setActiveTab(index, true);
  }

  private handleTabFocus(event: FocusEvent): void {
    if (this.rovingTabindex && !this.rovingTabindex.isActive) {
      this.rovingTabindex.init();
    }

    const focusedTab = event.currentTarget as HTMLElement | null;
    if (focusedTab && this.shouldCenterFocusedTab(focusedTab)) {
      this.scrollTabIntoView(focusedTab);
    }
  }

  private shouldCenterFocusedTab(focusedTab: HTMLElement): boolean {
    if (typeof focusedTab.matches !== "function") return true;

    try {
      return focusedTab.matches(":focus-visible");
    } catch {
      return true;
    }
  }
}
