/**
 * 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 { html, nothing } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.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 NileNavTab from '../nile-nav-tab/nile-nav-tab';
import type NileNavTabPanel from '../nile-nav-tab-panel/nile-nav-tab-panel';
import { styles } from './nile-nav-tab-group.css';
import { MouseKey } from '../internal/enum';
import '../nile-icon-button/nile-icon-button';

/**
 * Tab group navigation component.
 *
 * @tag nile-nav-tab-group
 *
 * @slot - Used for grouping tab panels in the tab group. Must be `<nile-nav-tab-panel>` elements.
 * @slot nav - Used for grouping tabs in the tab group. Must be `<nile-nav-tab>` elements.
 *
 * @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 toggle-frame - Toggle variant: absolutely positioned ring that draws the group chrome (tabs paint above it).
 * @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).
 * @cssproperty --show-indicator-on-hover - Whether to show the indicator on hover.
 *
 * @event nile-close - Bubbled from a closable `<nile-nav-tab>`; re-emitted so parents can listen on this element.
 */
@customElement('nile-nav-tab-group')
export class NileNavTabGroup extends NileElement {

  static styles: CSSResultGroup = styles;

  /**
   * Selection model (for reviewers):
   * - `activeTabName` is the internal source of truth; clicks and keyboard update it.
   * - `activeTabProp` / attribute `value` is the host-controlled API; changes in `updated()` copy into `activeTabName`.
   * - `setActiveTab()` syncs each tab’s `active` flag, each panel’s `active`, `activeTabProp` / `value`, indicator/pill geometry, and optionally emits `nile-tab-change`.
   * - `this.tabs` only includes non-disabled tabs (see `syncTabsAndPanels`); keyboard roving focus uses that list.
   */
  private activeTab?: NileNavTab;
  private mutationObserver: MutationObserver;
  private resizeObserver: ResizeObserver;
  private visibilityObserver: IntersectionObserver;
  private pillRepositionTimer: ReturnType<typeof setTimeout> | null = null;
  private pillTransitionClassTimer: ReturnType<typeof setTimeout> | null = null;
  private pillReady = false;
  private observedTabs = new Set<NileNavTab>();
  private tabs: NileNavTab[] = [];
  private panels: NileNavTabPanel[] = [];

  /** Tabs emit `nile-close` on the host; bubble is stopped so we re-emit from the group with `{ panel }` for consumers. */
  private readonly handleCloseEvent = (e: Event) => {
    if (e.target === this) return;

    const tab = e.target as NileNavTab | null;
    const panel = tab?.getAttribute('panel');

    if (!panel) return;

    // Other listeners on this host must not see the child event (no `detail`) after we synthesize one.
    e.stopImmediatePropagation();
    e.stopPropagation();

    this.syncIndicator();
    this.emit('nile-close', { panel });
  };

  /** Tracks visibility for snapping the indicator when a hidden parent (e.g. dropdown) is reopened. */
  private tabGroupWasIntersecting = false;

  /** Last measured nav width; used to detect layout resume after display:none / collapse. */
  private lastNavClientWidth = 0;

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

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

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

  /** The currently active tab value. */
  @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;


  /** 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;

  @property({ type: String, reflect: true }) variant: 'underline' | 'filled' | 'toggle' | 'neutral-filled' | 'toggle-button' = 'underline';

  @property({ type: String, reflect: true }) indicatorPlacement = '';

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

  @property({ type: String, reflect: true }) width = '';

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

  @state() activeTabName: string = '';

  private get showScrollControlButtons(): boolean {
    return !this.noScrollControls && this.hasScrollControls;
  }

  connectedCallback() {
    super.connectedCallback();

    this.resizeObserver = new ResizeObserver(() => {
      this.setScrollControls();
      const nw = this.nav?.clientWidth ?? 0;
      const layoutResumed = this.lastNavClientWidth === 0 && nw > 0;
      this.lastNavClientWidth = nw;
      this.repositionIndicator({ skipTransition: layoutResumed });
      this.debouncedPositionPill();
    });

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

      if (mutations.some(m => m.attributeName === 'disabled')) {
        this.syncTabsAndPanels();
      }

      if (mutations.some(m => m.attributeName === 'active')) {
        const activeTab = this.getAllTabs({ includeDisabled: false }).find(t => t.active);
        if (activeTab && activeTab.panel !== this.activeTabName) {
          this.activeTabName = activeTab.panel;
        }
      }

      if (mutations.some(m => m.type === 'childList' || m.type === 'characterData')) {
        this.repositionIndicator();
        this.debouncedPositionPill();
        requestAnimationFrame(() => this.setScrollControls());
      }
    });

    this.updateComplete.then(async () => {
      this.setScrollControls();
      await Promise.all(this.getAllTabs({ includeDisabled: true }).map(tab => tab.updateComplete));
      await Promise.all(this.getAllPanels().map(panel => panel.updateComplete));
      this.syncTabsAndPanels();

      if (this.activeTabProp) {
        this.activeTabName = this.activeTabProp;
      } else {
        const activeTab = this.getActiveTab();
        this.activeTabName = activeTab ? activeTab.panel : '';
      }

      // First paint: disable indicator transition so it jumps to the correct tab without sliding from (0,0).
      requestAnimationFrame(() => {
        if (this.indicator) {
          this.indicator.style.transition = 'none';
        }

        this.syncIndicator();
        this.positionPill();

        requestAnimationFrame(() => {
          if (this.indicator) {
            this.indicator.style.transition = '';
          }
        });
      });

      this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true, characterData: true });
      this.resizeObserver.observe(this.nav);

      this.visibilityObserver = new IntersectionObserver((entries) => {
        const entry = entries[0];
        const nowVisible = entry.isIntersecting && entry.intersectionRatio > 0;
        if (nowVisible) {
          this.setAriaLabels();
          this.debouncedPositionPill();
          const becameVisible = !this.tabGroupWasIntersecting;
          this.tabGroupWasIntersecting = true;
          this.repositionIndicator({ skipTransition: becameVisible });
        } else {
          this.tabGroupWasIntersecting = false;
        }
      });
      this.visibilityObserver.observe(this.tabGroup);
    });

    this.addEventListener('nile-close', this.handleCloseEvent);
    this.addEventListener('click', this.handleNavClick);
    this.addEventListener('keydown', this.handleKeyDown);
  }

  disconnectedCallback() {
    this.mutationObserver.disconnect();
    this.resizeObserver.disconnect();
    this.observedTabs.clear();
    this.visibilityObserver?.disconnect();
    this.removeEventListener('nile-close', this.handleCloseEvent);
    this.removeEventListener('click', this.handleNavClick);
    this.removeEventListener('keydown', this.handleKeyDown);
  }

  protected updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
    let nameChanged = _changedProperties.has('activeTabName');

    if (_changedProperties.has('activeTabProp') && this.activeTabName !== this.activeTabProp) {
      this.activeTabName = this.activeTabProp;
      nameChanged = true;
    }

    if (nameChanged) {
      const tab = this.getActiveTab();
      if (tab) {
        this.setActiveTab(tab, { scrollBehavior: 'smooth' });
      }
    }
    if (_changedProperties.has('noScrollControls')) {
      requestAnimationFrame(() => this.setScrollControls());
    }
  }

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

    if (tab && !tab.disabled) {
      this.indicator.style.display = 'block';
      this.repositionIndicator();
    } else {
      this.indicator.style.display = 'none';
    }
  }

  private handleNavClick(event: MouseEvent) {
    const tab = event.composedPath().find(
      (el): el is NileNavTab =>
        el instanceof HTMLElement && el.tagName.toLowerCase() === 'nile-nav-tab'
    );

    if (!tab) return;

    const tabsNow = this.getAllTabs({ includeDisabled: true });
    if (!tabsNow.includes(tab)) return;

    if (tab.hasAttribute('disabled') || tab.disabled) return;

    const isModifiedClick =
    Object.values(MouseKey).some(key => event[key as keyof MouseEvent]) || event.button !== 0;

    if (isModifiedClick) return;

    this.activeTabProp = tab.panel;
    this.activeTabName = tab.panel;
    tab.focus();
  }

  private positionPill(animate = false) {
    const pill = this.shadowRoot?.querySelector<HTMLElement>('.nav-tab-group__pill');
    if (!pill) return;

    if (this.pillTransitionClassTimer) {
      clearTimeout(this.pillTransitionClassTimer);
      this.pillTransitionClassTimer = null;
    }

    const tab = this.getActiveTab();
    if (!tab || tab.disabled) {
      pill.classList.add('nav-tab-group__pill--inactive');
      pill.classList.remove('nav-tab-group__pill--transitioning');
      return;
    }

    pill.classList.remove('nav-tab-group__pill--inactive');

    const target = tab.shadowRoot?.querySelector<HTMLElement>('.nav-tab-container');
    if (!target) return;

    const container = this.shadowRoot?.querySelector<HTMLElement>('.nav-tab-group__tabs');
    if (!container) return;

    if (target.offsetWidth === 0 || target.offsetHeight === 0) return;

    const box = this.getLayoutBoxRelativeToContainer(target, container);
    if (!box) return;

    const x = box.x;
    const y = box.y;
    const w = box.width;
    const h = box.height;

    const cw = container.clientWidth;
    const rightInset = Math.max(0, cw - x - w);

    if (animate) {
      const ease = 'cubic-bezier(0.4, 0, 0.2, 1)';
      pill.style.transition = `left 400ms ${ease}, right 400ms ${ease}, top 400ms ${ease}, height 400ms ${ease}`;
      if (this.variant === 'toggle') {
        pill.classList.add('nav-tab-group__pill--transitioning');
      }
    } else {
      pill.style.transition = '';
      if (this.variant === 'toggle') {
        pill.classList.remove('nav-tab-group__pill--transitioning');
      }
    }

    pill.style.transform = '';
    pill.style.left = `${x}px`;
    pill.style.right = `${rightInset}px`;
    pill.style.width = 'auto';
    pill.style.top = `${y}px`;
    pill.style.height = `${h}px`;
    pill.style.opacity = '1';

    if (!this.pillReady) {
      this.pillReady = true;
    }

    if (animate) {
      const onEnd = () => {
        pill.style.transition = '';
        pill.removeEventListener('transitionend', onEnd);
      };
      pill.addEventListener('transitionend', onEnd);
      if (this.variant === 'toggle') {
        this.pillTransitionClassTimer = setTimeout(() => {
          this.pillTransitionClassTimer = null;
          pill.classList.remove('nav-tab-group__pill--transitioning');
        }, 420);
      }
    }
  }

  private debouncedPositionPill() {
    if (this.pillRepositionTimer) {
      clearTimeout(this.pillRepositionTimer);
    }
    this.pillRepositionTimer = setTimeout(() => {
      this.pillRepositionTimer = null;
      this.positionPill();
    }, 150);
  }

  private handleTabHover(event: MouseEvent) {
    if (!this.showIndicatorOnHover) return;

    const tab = event.composedPath().find(
      (el): el is NileNavTab =>
        el instanceof HTMLElement && el.tagName.toLowerCase() === 'nile-nav-tab'
    );

    if (!tab || tab.disabled || tab.hasAttribute('disabled')) return;

    const tabsNow = this.getAllTabs({ includeDisabled: true });
    if (!tabsNow.includes(tab)) return;

    this.positionHoverIndicator(tab);
  }

  private handleTabHoverLeave() {
    if (!this.showIndicatorOnHover || !this.hoverIndicator) return;
    this.hoverIndicator.style.opacity = '0';
  }

  private positionHoverIndicator(tab: NileNavTab) {
    if (!this.hoverIndicator) return;

    const container = this.shadowRoot?.querySelector<HTMLElement>('.nav-tab-group__tabs');
    if (!container) return;

    const target = tab.shadowRoot?.querySelector<HTMLElement>('.nav-tab-container');
    if (!target) return;

    const layoutBox = this.getLayoutBoxRelativeToContainer(target, container);
    if (!layoutBox) return;

    this.hoverIndicator.style.transition = 'none';

    switch (this.placement) {
      case 'top':
      case 'bottom':
        this.hoverIndicator.style.width = `${target.offsetWidth}px`;
        this.hoverIndicator.style.height = 'auto';
        this.hoverIndicator.style.translate = `${layoutBox.x}px 0px`;
        break;

      case 'start':
      case 'end':
        this.hoverIndicator.style.height = `${target.offsetHeight}px`;
        this.hoverIndicator.style.width = 'auto';
        this.hoverIndicator.style.translate = `0px ${layoutBox.y}px`;
        break;
    }

    this.hoverIndicator.style.opacity = '1';
  }

  render() {
    const usesPill =
      this.variant === 'filled' ||
      this.variant === 'neutral-filled' ||
      this.variant === 'toggle' ||
      this.variant === 'toggle-button';

    const showsIndicator =
      this.variant !== 'filled' &&
      this.variant !== 'neutral-filled' &&
      this.variant !== 'toggle-button' &&
      this.variant !== 'toggle';

    const toggleVariant = this.variant === 'toggle';

    return html`
      <div
        part="base"
        class=${classMap({
          'nav-tab-group': true,
          'nav-tab-group--top': this.placement === 'top',
          'nav-tab-group--bottom': this.placement === 'bottom',
          'nav-tab-group--start': this.placement === 'start',
          'nav-tab-group--end': this.placement === 'end',
          'nav-tab-group--rtl': true,
          'nav-tab-group--has-scroll-controls': this.showScrollControlButtons,
          'hide__track': this.noTrack,
        })}
        style=${styleMap(this.width ? { '--nav-tab-item-width': this.width } : {})}
      >
        <nav class="nav-tab-group__nav-container" part="nav">
          ${toggleVariant
            ? html`<div class="nav-tab-group__toggle-frame" part="toggle-frame"></div>`
            : nothing}
          ${this.showScrollControlButtons
            ? html`
                <nile-icon-button
                  part="scroll-button scroll-button--start"
                  exportparts="base:scroll-button__base"
                  class="nav-tab-group__scroll-button nav-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>
              `
            : nothing}

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

              ${!this.noTrack && showsIndicator && this.showIndicatorOnHover ? html`
                <div class="nav-tab-group__hover-indicator" part="hover-tab-indicator"></div>
              ` : nothing}

              ${usesPill ? html`<div class="nav-tab-group__pill" part="active-tab-pill"></div>` : nothing}

              <slot
                name="nav"
                @mouseover=${this.handleTabHover}
                @mouseleave=${this.handleTabHoverLeave}
                @slotchange=${this.syncTabsAndPanels}
              ></slot>
            </ul>
          </div>

          ${this.showScrollControlButtons
            ? html`
                <nile-icon-button
                  part="scroll-button scroll-button--end"
                  exportparts="base:scroll-button__base"
                  class="nav-tab-group__scroll-button nav-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>
              `
            : nothing}
        </nav>

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

  private getAllTabs(options: { includeDisabled: boolean } = { includeDisabled: true }) {
    const slot = this.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="nav"]')!;
    const navTabs = slot.assignedElements().filter(
      (el): el is NileNavTab => el.tagName.toLowerCase() === 'nile-nav-tab'
    );
    return options.includeDisabled ? navTabs : navTabs.filter(tab => !tab.disabled);
  }

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

  /**
   * Resolves the tab to treat as selected for indicator/pill wiring.
   * Prefer `activeTabName` match; if empty (before init), fall back to `.active` or first usable tab.
   */
  private getActiveTab() {
    return this.tabs.find(el =>
      this.activeTabName ? el.panel === this.activeTabName : el.active
    ) ?? this.tabs.find(el => !el.disabled) ?? this.tabs[0];
  }

  /** Reflects horizontal overflow into `hasScrollControls` (top/bottom only; vertical stacks do not scroll this strip). */
  setScrollControls() {
    if (!this.nav) return;
    if (this.noScrollControls) {
      this.hasScrollControls = false;
      return;
    }
    this.hasScrollControls =
      ['top', 'bottom'].includes(this.placement) &&
      this.nav.scrollWidth > this.nav.clientWidth;
  }

  private handleKeyDown(event: KeyboardEvent) {
    const tab = event.composedPath().find(
      (el): el is NileNavTab =>
        el instanceof HTMLElement && el.tagName.toLowerCase() === 'nile-nav-tab'
    );

    if (tab?.disabled) {
      event.preventDefault();
      return;
    }

    if (!tab || !this.tabs.includes(tab)) {
      return;
    }

    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      this.activeTabProp = tab.panel;
      this.activeTabName = tab.panel;
      return;
    }

    const navKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'];
    if (!navKeys.includes(event.key)) return;

    let index = this.tabs.indexOf(tab);
    const isHorizontal = ['top', 'bottom'].includes(this.placement);
    const isRtl = getComputedStyle(this).direction === 'rtl';

    if (event.key === 'Home') {
      index = 0;
    } else if (event.key === 'End') {
      index = this.tabs.length - 1;
    } else if (
      (isHorizontal &&
        ((isRtl && event.key === 'ArrowRight') || (!isRtl && event.key === 'ArrowLeft'))) ||
      (['start', 'end'].includes(this.placement) && event.key === 'ArrowUp')
    ) {
      index--;
    } else if (
      (isHorizontal &&
        ((isRtl && event.key === 'ArrowLeft') || (!isRtl && event.key === '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 (isHorizontal) {
      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: NileNavTab, 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;
      if (this.activeTabProp !== this.activeTab.panel) {
        this.activeTabProp = this.activeTab.panel;
      }

      this.tabs.forEach(el => (el.active = el === this.activeTab));
      this.panels.forEach(el => (el.active = el.name === this.activeTab?.panel));
      this.syncIndicator();
      this.positionPill(this.pillReady);

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

      if (options.emitEvents) {
        this.emit('nile-tab-change', {
          value: this.activeTab.panel,
          previousValue: previousTab?.panel ?? null,
          index: this.tabs.indexOf(this.activeTab),
          previousIndex: previousTab ? this.tabs.indexOf(previousTab) : -1,
          link: this.activeTab.link
        });
      }
    }
  }

  private setAriaLabels() {
    const allTabs = this.getAllTabs({ includeDisabled: true });
    for (const tab of allTabs) {
      const panel = this.panels.find(el => el.name === tab.panel);
      if (tab.disabled) {
        tab.removeAttribute('aria-controls');
        if (panel) panel.removeAttribute('aria-labelledby');
        continue;
      }
      if (!panel) continue;
      const panelId = panel.getAttribute('id');
      const tabId = tab.getAttribute('id');
      if (!panelId || !tabId) continue;

      tab.setAttribute('aria-controls', panelId);
      panel.setAttribute('aria-labelledby', tabId);
    }
  }

  
  private getRelativeOffsetToAncestor(element: HTMLElement, ancestor: HTMLElement): { x: number; y: number } | null {
    let x = 0;
    let y = 0;
    let el: HTMLElement | null = element;
    while (el && el !== ancestor) {
      x += el.offsetLeft;
      y += el.offsetTop;
      el = el.offsetParent as HTMLElement | null;
    }
    return el === ancestor ? { x, y } : null;
  }

  private getLayoutBoxRelativeToContainer(
    target: HTMLElement,
    container: HTMLElement
  ): { x: number; y: number; width: number; height: number } | null {
    const c = container.getBoundingClientRect();
    const r = target.getBoundingClientRect();
    if (r.width === 0 || r.height === 0) return null;

    const scaleX = container.offsetWidth > 0 ? c.width / container.offsetWidth : 1;
    const scaleY = container.offsetHeight > 0 ? c.height / container.offsetHeight : 1;

    if (scaleX > 0 && scaleY > 0 && Number.isFinite(scaleX) && Number.isFinite(scaleY)) {
      const scaleIsUnity =
        Math.abs(scaleX - 1) < 0.001 && Math.abs(scaleY - 1) < 0.001;
      if (scaleIsUnity) {
        const off = this.getRelativeOffsetToAncestor(target, container);
        if (off) {
          return {
            x: off.x,
            y: off.y,
            width: target.offsetWidth,
            height: target.offsetHeight,
          };
        }
      }

      const x = (r.left - c.left) / scaleX;
      const y = (r.top - c.top) / scaleY;
      let width = r.width / scaleX;
      let height = r.height / scaleY;
      const ow = target.offsetWidth;
      const oh = target.offsetHeight;
      if (ow > 0) width = Math.min(width, ow);
      if (oh > 0) height = Math.min(height, oh);
      return { x, y, width, height };
    }

    const offset = this.getRelativeOffsetToAncestor(target, container);
    if (!offset) return null;
    return {
      x: offset.x,
      y: offset.y,
      width: target.offsetWidth,
      height: target.offsetHeight,
    };
  }

  private repositionIndicator(options?: { skipTransition?: boolean }) {
    const currentTab = this.getActiveTab();
    if (!currentTab || !this.indicator) return;

    const container = this.shadowRoot?.querySelector<HTMLElement>('.nav-tab-group__tabs');
    if (!container) return;

    const target = currentTab.shadowRoot?.querySelector<HTMLElement>('.nav-tab-container');
    if (!target) return;

    const skipTransition = options?.skipTransition === true;
    if (skipTransition) {
      this.indicator.style.transition = 'none';
    }

    const layoutBox = this.getLayoutBoxRelativeToContainer(target, container);
    if (!layoutBox) {
      if (skipTransition) {
        requestAnimationFrame(() => {
          if (this.indicator) this.indicator.style.transition = '';
        });
      }
      return;
    }

    const x = layoutBox.x;
    const y = layoutBox.y;

    switch (this.placement) {
      case 'top':
      case 'bottom':
        this.indicator.style.width = `${layoutBox.width}px`;
        this.indicator.style.height = 'auto';
        this.indicator.style.translate = `${x}px 0px`;
        break;

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

    if (skipTransition) {
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          if (this.indicator) this.indicator.style.transition = '';
        });
      });
    }
  }

  private syncTabsAndPanels() {
    this.tabs = this.getAllTabs({ includeDisabled: false });
    this.panels = this.getAllPanels();

    const allTabs = this.getAllTabs({ includeDisabled: true });
    const enabledTabs = this.getAllTabs({ includeDisabled: false });
    const size = enabledTabs.length;

    const currentTabs = new Set(allTabs);

    for (const tab of this.observedTabs) {
      if (!currentTabs.has(tab)) {
        this.resizeObserver.unobserve(tab);
        this.observedTabs.delete(tab);
      }
    }

    allTabs.forEach(tab => {
      tab.centered = this.centered;
      if (tab.disabled) {
        tab.removeAttribute('aria-posinset');
        tab.removeAttribute('aria-setsize');
      } else {
        const i = enabledTabs.indexOf(tab);
        tab.setAttribute('aria-posinset', String(i + 1));
        tab.setAttribute('aria-setsize', String(size));
      }

      if (!this.observedTabs.has(tab)) {
        this.resizeObserver.observe(tab);
        this.observedTabs.add(tab);
      }
    });

    this.setScrollControls();
    this.setAriaLabels();
  }
}

export default NileNavTabGroup;

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