import type { EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Method, Prop, State, h } from '@stencil/core';
import { printIonError } from '@utils/logging';

import type { NavOutlet, RouteID, RouteWrite } from '../router/utils/interface';
import type { TabButtonClickEventDetail } from '../tab-bar/tab-bar-interface';

/**
 * @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component.
 * @virtualProp {"ios" | "md" | "ionic"} theme - The theme determines the visual appearance of the component.
 *
 * @slot - Content is placed between the named slots if provided without a slot.
 * @slot top - Content is placed at the top of the screen.
 * @slot bottom - Content is placed at the bottom of the screen.
 */
@Component({
  tag: 'ion-tabs',
  styleUrl: 'tabs.scss',
  shadow: true,
})
export class Tabs implements NavOutlet {
  private transitioning = false;
  private leavingTab?: HTMLIonTabElement;

  @Element() el!: HTMLIonTabsElement;

  @State() selectedTab?: HTMLIonTabElement;

  /** @internal */
  @Prop({ mutable: true }) useRouter = false;

  /**
   * Emitted when the navigation will load a component.
   * @internal
   */
  @Event() ionNavWillLoad!: EventEmitter<void>;

  /**
   * Emitted when the navigation is about to transition to a new component.
   */
  @Event({ bubbles: false }) ionTabsWillChange!: EventEmitter<{ tab: string }>;

  /**
   * Emitted when the navigation has finished transitioning to a new component.
   */
  @Event({ bubbles: false }) ionTabsDidChange!: EventEmitter<{ tab: string }>;

  async componentWillLoad() {
    if (!this.useRouter) {
      /**
       * JavaScript and StencilJS use `ion-router`, while
       * the other frameworks use `ion-router-outlet`.
       *
       * If either component is present then tabs will not use
       * a basic tab-based navigation. It will use the history
       * stack or URL updates associated with the router.
       */
      this.useRouter =
        (!!this.el.querySelector('ion-router-outlet') || !!document.querySelector('ion-router')) &&
        !this.el.closest('[no-router]');
    }
    if (!this.useRouter) {
      const tabs = this.tabs;
      if (tabs.length > 0) {
        await this.select(tabs[0]);
      }
    }
    this.ionNavWillLoad.emit();
  }

  componentDidLoad() {
    this.updateTabBar();
  }

  componentDidUpdate() {
    this.updateTabBar();
  }

  private updateTabBar() {
    const tabBar = this.el.querySelector('ion-tab-bar');
    if (!tabBar) {
      return;
    }

    const tab = this.selectedTab ? this.selectedTab.tab : undefined;

    // If tabs has no selected tab but tab-bar already has a selected-tab set,
    // don't overwrite it. This handles cases where tab-bar is used without ion-tab elements.
    if (tab === undefined) {
      return;
    }

    if (tabBar.selectedTab === tab) {
      return;
    }

    tabBar.selectedTab = tab;
  }

  /**
   * Select a tab by the value of its `tab` property or an element reference. This method is only available for vanilla JavaScript projects. The Angular, React, and Vue implementations of tabs are coupled to each framework's router.
   *
   * @param tab The tab instance to select. If passed a string, it should be the value of the tab's `tab` property.
   */
  @Method()
  async select(tab: string | HTMLIonTabElement): Promise<boolean> {
    const selectedTab = getTab(this.tabs, tab);
    if (!this.shouldSwitch(selectedTab)) {
      return false;
    }
    await this.setActive(selectedTab);
    await this.notifyRouter();
    this.tabSwitch();

    return true;
  }

  /**
   * Get a specific tab by the value of its `tab` property or an element reference. This method is only available for vanilla JavaScript projects. The Angular, React, and Vue implementations of tabs are coupled to each framework's router.
   *
   * @param tab The tab instance to select. If passed a string, it should be the value of the tab's `tab` property.
   */
  @Method()
  async getTab(tab: string | HTMLIonTabElement): Promise<HTMLIonTabElement | undefined> {
    return getTab(this.tabs, tab);
  }

  /**
   * Get the currently selected tab. This method is only available for vanilla JavaScript projects. The Angular, React, and Vue implementations of tabs are coupled to each framework's router.
   */
  @Method()
  getSelected(): Promise<string | undefined> {
    return Promise.resolve(this.selectedTab ? this.selectedTab.tab : undefined);
  }

  /** @internal */
  @Method()
  async setRouteId(id: string): Promise<RouteWrite> {
    const selectedTab = getTab(this.tabs, id);
    if (!this.shouldSwitch(selectedTab)) {
      return { changed: false, element: this.selectedTab };
    }

    await this.setActive(selectedTab);
    return {
      changed: true,
      element: this.selectedTab,
      markVisible: () => this.tabSwitch(),
    };
  }

  /** @internal */
  @Method()
  async getRouteId(): Promise<RouteID | undefined> {
    const tabId = this.selectedTab?.tab;
    return tabId !== undefined ? { id: tabId, element: this.selectedTab } : undefined;
  }

  private setActive(selectedTab: HTMLIonTabElement): Promise<void> {
    if (this.transitioning) {
      return Promise.reject('transitioning already happening');
    }

    this.transitioning = true;
    this.leavingTab = this.selectedTab;
    this.selectedTab = selectedTab;
    this.ionTabsWillChange.emit({ tab: selectedTab.tab });
    selectedTab.active = true;
    this.updateTabBar();
    return Promise.resolve();
  }

  private tabSwitch() {
    const selectedTab = this.selectedTab;
    const leavingTab = this.leavingTab;

    this.leavingTab = undefined;
    this.transitioning = false;
    if (!selectedTab) {
      return;
    }

    if (leavingTab !== selectedTab) {
      if (leavingTab) {
        leavingTab.active = false;
      }
      this.ionTabsDidChange.emit({ tab: selectedTab.tab });
    }
  }

  private notifyRouter() {
    if (this.useRouter) {
      const router = document.querySelector('ion-router');
      if (router) {
        return router.navChanged('forward');
      }
    }
    return Promise.resolve(false);
  }

  private shouldSwitch(selectedTab: HTMLIonTabElement | undefined): selectedTab is HTMLIonTabElement {
    const leavingTab = this.selectedTab;
    return selectedTab !== undefined && selectedTab !== leavingTab && !this.transitioning;
  }

  private get tabs() {
    return Array.from(this.el.querySelectorAll('ion-tab'));
  }

  private onTabClicked = (ev: CustomEvent<TabButtonClickEventDetail>) => {
    const { href, tab } = ev.detail;
    if (this.useRouter && href !== undefined) {
      const router = document.querySelector('ion-router');
      if (router) {
        router.push(href);
      }
    } else {
      this.select(tab);
    }
  };

  render() {
    return (
      <Host onIonTabButtonClick={this.onTabClicked}>
        <slot name="top"></slot>
        <div class="tabs-inner">
          <slot></slot>
        </div>
        <slot name="bottom"></slot>
      </Host>
    );
  }
}

const getTab = (tabs: HTMLIonTabElement[], tab: string | HTMLIonTabElement): HTMLIonTabElement | undefined => {
  const tabEl = typeof tab === 'string' ? tabs.find((t) => t.tab === tab) : tab;

  if (!tabEl) {
    printIonError(`[ion-tabs] - Tab with id: "${tabEl}" does not exist`);
  }
  return tabEl;
};
