/**
 * Copyright Aquera Inc 2023
 *
 * This source code is licensed under the BSD-3-Clause license found in the
 * LICENSE file in the root directory of this source tree.
 */

import {LitElement, html, CSSResultArray, TemplateResult, nothing} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import {styles} from './nile-tab-group.css';

import '../nile-icon-button/nile-icon-button';
import { classMap } from 'lit/directives/class-map.js';
import { query, state } from 'lit/decorators.js';
import { scrollIntoView } from '../internal/scroll';
import { watch } from '../internal/watch';
import NileElement from '../internal/nile-element';
import type { CSSResultGroup, PropertyValueMap } from 'lit';
import type NileTab from '../nile-tab/nile-tab';
import type NileTabPanel from '../nile-tab-panel/nile-tab-panel';

/**
 * Nile icon component.
 *
 * @tag nile-tab-group
 *
 * @slot - Used for grouping tab panels in the tab group. Must be `<nile-tab-panel>` elements.
 * @slot nav - Used for grouping tabs in the tab group. Must be `<nile-tab>` elements.
 *
 * @event {{ name: String }} nile-tab-show - Emitted when a tab is shown.
 * @event {{ name: String }} nile-tab-hide - Emitted when a tab is hidden.
 *
 * @csspart base - The component's base wrapper.
 * @csspart nav - The tab group's navigation container where tabs are slotted in.
 * @csspart tabs - The container that wraps the tabs.
 * @csspart active-tab-indicator - The line that highlights the currently selected tab.
 * @csspart body - The tab group's body where tab panels are slotted in.
 * @csspart scroll-button - The previous/next scroll buttons that show when tabs are scrollable, an `<nile-icon-button>`.
 * @csspart scroll-button--start - The starting scroll button.
 * @csspart scroll-button--end - The ending scroll button.
 * @csspart scroll-button__base - The scroll button's exported `base` part.
 *
 * @cssproperty --indicator-color - The color of the active tab indicator.
 * @cssproperty --track-color - The color of the indicator's track (the line that separates tabs from panels).
 * @cssproperty --track-width - The width of the indicator's track (the line that separates tabs from panels).
 *
 */
@customElement('nile-tab-group')
export class NileTabGroup extends NileElement {

  static styles: CSSResultGroup = styles;

  private activeTab?: NileTab;
  private mutationObserver: MutationObserver;
  private resizeObserver: ResizeObserver;
  private tabs: NileTab[] = [];
  private panels: NileTabPanel[] = [];

  @query('.tab-group') tabGroup: HTMLElement;
  @query('.tab-group__body') body: HTMLSlotElement;
  @query('.tab-group__nav') nav: HTMLElement;
  @query('.tab-group__indicator') indicator: HTMLElement;

  @property({ type: Boolean, reflect: true, attribute: true }) hasScrollControls = false;

  /** The placement of the tabs. */
  @property() placement: 'top' | 'bottom' | 'start' | 'end' = 'top';

  /** The placement of the tabs. */
  @property({ reflect: true, attribute: 'value', type: String }) activeTabProp: string = '';

  /** Track for showing Indicators and Background. */
  @property({ type: Boolean, reflect: true, attribute:'no-track'}) noTrack = false;

  /**
   * When set to auto, navigating tabs with the arrow keys will instantly show the corresponding tab panel. When set to
   * manual, the tab will receive focus but will not show until the user presses spacebar or enter.
   */
  @property() activation: 'auto' | 'manual' = 'auto';

  /** Disables the scroll arrows that appear when tabs overflow. */
  @property({ attribute: 'no-scroll-controls', type: Boolean }) noScrollControls = false;

  /** Controls whether tabs are centered and have equal width */
  @property({ type: Boolean, reflect: true }) centered = false;

  @state() activeTabName: string = '';

  connectedCallback() {
    const whenAllDefined = Promise.allSettled([
      customElements.whenDefined('nile-tab'),
      customElements.whenDefined('nile-tab-panel')
    ]);

    super.connectedCallback();

    this.resizeObserver = new ResizeObserver(() => {
      this.repositionIndicator();
      // this.updateScrollControls();
    });

    this.mutationObserver = new MutationObserver(mutations => {
      // Update aria labels when the DOM changes
      if (mutations.some(m => !['aria-labelledby', 'aria-controls'].includes(m.attributeName!))) {
        setTimeout(() => this.setAriaLabels());
      }

      // Sync tabs when disabled states change
      if (mutations.some(m => m.attributeName === 'disabled')) {
        this.syncTabsAndPanels();
      }
    });

    // After the first update...
    this.updateComplete.then(() => {
      this.syncTabsAndPanels();
      if(this.activeTabProp) this.activeTabName=this.activeTabProp;
      else this.activeTabName=this.getActiveTab().panel;
      this.syncIndicator();

      this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true });
      this.resizeObserver.observe(this.nav);
      // Wait for tabs and tab panels to be registered
      whenAllDefined.then(() => {
        // Set initial tab state when the tabs become visible
        const intersectionObserver = new IntersectionObserver((entries, observer) => {
            if (entries[0].intersectionRatio > 0) {
              this.setAriaLabels();
              observer.unobserve(entries[0].target);
            }
        });
        intersectionObserver.observe(this.tabGroup);
      });
    });
  }

  disconnectedCallback() {
    this.mutationObserver.disconnect();
    this.resizeObserver.unobserve(this.nav);
  }
 
  protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
    this.addEventListener('nile-toggle-change', this.handleToggleFromTab);
  }

  protected updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
    if(_changedProperties.has('activeTabName')){
      const tab=this.getActiveTab()
      if(!tab) return;
      this.setActiveTab(tab, {scrollBehavior: 'smooth' });
    }
    if(_changedProperties.has('activeTabProp')){
      this.activeTabName=this.activeTabProp;
    }
  }

  // @watch('noScrollControls', { waitUntilFirstUpdate: true })
  // updateScrollControls() {
  //   this.setScrollControls()
  // }

  @watch('placement', { waitUntilFirstUpdate: true })
  syncIndicator() {
    if(!this.indicator) return;
    const tab = this.getActiveTab();

    if (tab) {
      this.indicator.style.display = 'block';
      this.repositionIndicator();
    } else {
      this.indicator.style.display = 'none';
    }
  }
  
  render() {
    return html`
      <div
        part="base"
        class=${classMap({
          'tab-group': true,
          'tab-group--top': this.placement === 'top',
          'tab-group--bottom': this.placement === 'bottom',
          'tab-group--start': this.placement === 'start',
          'tab-group--end': this.placement === 'end',
          'tab-group--rtl': true,
          'tab-group--has-scroll-controls': this.hasScrollControls,
          'hide__track':this.noTrack
        })}
        @click=${this.handleClick}
        @keydown=${this.handleKeyDown}
      >
        <div class="tab-group__nav-container" part="nav">
          ${this.hasScrollControls
            ? html`
                <nile-icon-button
                  part="scroll-button scroll-button--start"
                  exportparts="base:scroll-button__base"
                  class="tab-group__scroll-button tab-group__scroll-button--start"
                  name='var(--nile-icon-arrow-right, var(--ng-icon-chevron-right))'
                  library="system"
                  label="scrollToStart"
                  @click=${this.handleScrollToEnd}
                ></nile-icon-button>
              `
            : ''}

          <div class="tab-group__nav">
            <div part="tabs" class="tab-group__tabs" role="tablist">
              <div part="active-tab-indicator-path" class="tab-group__indicator__path"></div>
              <div
                part="active-tab-indicator"
                class=${classMap({
                  'tab-group__indicator': !this.noTrack,
                })}
              ></div>

              <slot name="nav"  @slotchange=${this.syncTabsAndPanels}></slot>
            </div>
          </div>

          ${this.hasScrollControls
            ? html`
                <nile-icon-button
                  part="scroll-button scroll-button--end"
                  exportparts="base:scroll-button__base"
                  class="tab-group__scroll-button tab-group__scroll-button--end"
                  name='var(--nile-icon-arrow-left, var(--ng-icon-chevron-left))'
                  library="system"
                  label="scrollToEnd"
                  @click=${this.handleScrollToStart}
                ></nile-icon-button>
              `
            : ''}
        </div>

        <slot part="body" class="tab-group__body" @slotchange=${this.syncTabsAndPanels}></slot>
      </div>
    `;
  }

  private getAllTabs(options: { includeDisabled: boolean } = { includeDisabled: true }) {
    const slot = this.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="nav"]')!;

    return [...(slot.assignedElements() as NileTab[])].filter(el => {
      return options.includeDisabled
        ? el.tagName.toLowerCase() === 'nile-tab'
        : el.tagName.toLowerCase() === 'nile-tab' && !el.disabled;
    });
  }

  private getAllPanels() {
    return [...this.body.assignedElements()].filter(el => el.tagName.toLowerCase() === 'nile-tab-panel') as [NileTabPanel];
  }

  private getActiveTab() {
    return this.tabs.find(el => this.activeTabName?el.panel==this.activeTabName:el.active) ?? this.tabs[0];
  }

  private handleClick(event: MouseEvent) {
    const target = event.target as HTMLElement;
    const tab = target.closest('nile-tab');
    const tabGroup = tab?.closest('nile-tab-group');

    // Ensure the target tab is in this tab group
    if (tabGroup !== this) {
      return;
    }

    if (tab !== null && !tab.disabled) {
      this.activeTabName=tab.panel
    }
  }

  setScrollControls(){
    if (this.noScrollControls) {
      this.hasScrollControls = false;
    } else {
      this.hasScrollControls = ['top', 'bottom'].includes(this.placement) && this.nav.scrollWidth > this.nav.clientWidth;
    }
    this.hasScrollControls = false;
  }

  private handleKeyDown(event: KeyboardEvent) {
    const target = event.target as HTMLElement;
    const tab = target.closest('nile-tab');
    const tabGroup = tab?.closest('nile-tab-group');

    // Ensure the target tab is in this tab group
    if (tabGroup !== this) {
      return;
    }

    // Activate a tab
    if (['Enter', ' '].includes(event.key)) {
      if (tab !== null) {
        this.activeTabName=tab.panel
        event.preventDefault();
      }
    }

    // Move focus left or right
    if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
      const activeEl = tab;
      const isRtl = false;

      if (activeEl?.tagName.toLowerCase() === 'nile-tab') {
        let index = this.tabs.indexOf(activeEl);

        if (event.key === 'Home') {
          index = 0;
        } else if (event.key === 'End') {
          index = this.tabs.length - 1;
        } else if (
          (['top', 'bottom'].includes(this.placement) && event.key === (isRtl ? 'ArrowRight' : 'ArrowLeft')) ||
          (['start', 'end'].includes(this.placement) && event.key === 'ArrowUp')
        ) {
          index--;
        } else if (
          (['top', 'bottom'].includes(this.placement) && event.key === (isRtl ? 'ArrowLeft' : 'ArrowRight')) ||
          (['start', 'end'].includes(this.placement) && event.key === 'ArrowDown')
        ) {
          index++;
        }

        if (index < 0) {
          index = this.tabs.length - 1;
        }

        if (index > this.tabs.length - 1) {
          index = 0;
        }

        this.tabs[index].focus({ preventScroll: true });

        if (this.activation === 'auto') {
          this.activeTabName=this.tabs[index].panel
        }

        if (['top', 'bottom'].includes(this.placement)) {
          scrollIntoView(this.tabs[index], this.nav, 'horizontal');
        }

        event.preventDefault();
      }
    }
  }

  private handleScrollToStart() {
    this.nav.scroll({
      left:-this.nav.scrollWidth,
      behavior: 'smooth'
    });
  }

  private handleScrollToEnd() {
    this.nav.scroll({
      left:this.nav.scrollWidth,
      behavior: 'smooth'
    });
  }

  private setActiveTab(tab: NileTab, options?: { emitEvents?: boolean; scrollBehavior?: 'auto' | 'smooth' }) {
    options = {
      emitEvents: true,
      scrollBehavior: 'auto',
      ...options
    };

    if (tab !== this.activeTab && !tab.disabled) {

      const previousTab = this.activeTab;
      this.activeTab = tab;

      // Sync active tab and panel
      this.tabs.map(el => (el.active = el === this.activeTab));
      this.panels.map(el => (el.active = el.name === this.activeTab?.panel));
      this.syncIndicator();

      if (['top', 'bottom'].includes(this.placement)) {
        scrollIntoView(this.activeTab, this.nav, 'horizontal', options.scrollBehavior);
      }

      // Emit events
      if (options.emitEvents) {
        if (previousTab) {
          this.emit('nile-tab-hide', { value: previousTab.panel });
        }

        this.emit('nile-tab-show', { value: this.activeTab.panel });
      }
    }
  }

  private setAriaLabels() {
    // Link each tab with its corresponding panel
    this.tabs.forEach(tab => {
      const panel = this.panels.find(el => el.name === tab.panel);
      if (panel) {
        tab.setAttribute('aria-controls', panel.getAttribute('id')!);
        panel.setAttribute('aria-labelledby', tab.getAttribute('id')!);
      }
    });
  }

  private repositionIndicator() {
    const currentTab = this.getActiveTab();

    if (!currentTab || !this.indicator) {
      return;
    }

    const width = currentTab.clientWidth;
    const height = currentTab.clientHeight;
    const isRtl = false;

    // We can't used offsetLeft/offsetTop here due to a shadow parent issue where neither can getBoundingClientRect
    // because it provides invalid values for animating elements: https://bugs.chromium.org/p/chromium/issues/detail?id=920069
    const allTabs = this.getAllTabs();
    const precedingTabs = allTabs.slice(0, allTabs.indexOf(currentTab));
    const offset = precedingTabs.reduce(
      (previous, current) => ({
        left: previous.left + current.clientWidth + 12,
        top: previous.top + current.clientHeight + 12
      }),
      { left: 0, top: 0 }
    );

    switch (this.placement) {
      case 'top':
      case 'bottom':
        this.indicator.style.width = `${width - 5}px`;
        this.indicator.style.height = 'auto';
        this.indicator.style.translate = `${offset.left + 2}px`;
        break;

      case 'start':
      case 'end':
        this.indicator.style.width = 'auto';
        this.indicator.style.height = `${height}px`;
        this.indicator.style.translate = `0 ${offset.top}px`;
        break;
    }
  }

  // This stores tabs and panels so we can refer to a cache instead of calling querySelectorAll() multiple times.
  private syncTabsAndPanels() {
    this.tabs = this.getAllTabs({ includeDisabled: false });
    this.panels = this.getAllPanels();

    this.tabs.forEach((tab:any) => {
      tab.centered = this.centered;
    });

    // After updating, show or hide scroll controls as needed
    // this.updateComplete.then(() => this.updateScrollControls());
  }
  
  private handleToggleFromTab(e:CustomEvent){
    e.stopPropagation();
    this.activeTabName=e.detail.value;
  }

}

export default NileTabGroup;

declare global {
  interface HTMLElementTagNameMap {
    'nile-tab-group': NileTabGroup;
  }
}
