/**
 * Copyright Aquera Inc 2025
 *
 * 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, CSSResultArray, TemplateResult } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { styles } from './nile-grid.css';
import NileElement from '../internal/nile-element';
import { syncStickyShadows, layout } from './nile-grid.layout';
import { listenToEventListeners, removeEventListeners, getHead, getBody, copyArrayValuesInPlace } from './nile-grid.utils';
import { computeNewWidth, resizeWithScroll, resizeNoScroll, updateResizeGuides } from './nile-grid.resize';
import { PrefixColumn } from './types/nile-grid.types';

/**
 * Nile grid component.
 *
 * @tag nile-grid
 *
 */
@customElement('nile-grid')
export class NileGrid extends NileElement {
  /**
   * The styles for nile-grid
   * @remarks If you are extending this class you can extend the base styles with super. Eg `return [super(), myCustomStyles]`
   */
  public static get styles(): CSSResultArray {
    return [styles];
  }

  /** To store the column widths in the beginning and after resizing */
  @state() public columnWidths: number[] = [];

  /** To get the slot element */
  @query('slot') public slotEl!: HTMLSlotElement;

  /** To get the first resize line to show the resize handle */
  @query('.nile-resize-start') resizeStart: HTMLElement;

  /** To get the last resize line to show the resize handle */
  @query('.nile-resize-end') resizeEnd: HTMLElement;

  /** To get the last resize line to show the resize handle */
  @query('.shadow-left') shadowLeft: HTMLElement;

  /** To get the last resize line to show the resize handle */
  @query('.shadow-right') shadowRight: HTMLElement;

  /** To enable horizontal scrolling when user is resizing */
  @property({ type: Boolean, attribute: true, reflect: true }) enableScroll: boolean = false;

  /** To enable hoverable rows */
  @property({ type: Boolean, attribute: true, reflect: true }) hoverable: boolean = false;

  /** Controls vertical overscroll behavior when the grid is scrollable. */
  @property({ type: String, attribute: true, reflect: true }) overflowScrollBehaviour: string = "none";

  /** To store the mutation observer */
  private mo?: MutationObserver;

  /** To store the resize observer */
  private resizeObserver?: ResizeObserver;

  private resizeObserverStickyShadows?: ResizeObserver;

  /** To store the request animation frame */
  private raf = 0;

  /** Minimum column width */
  @property({ type: Number, reflect: true, attribute: true }) minColumnWidth = 40;

  /** To store the sticky index's */
  @state() public stickyLeftIndexes: number[] = [];

  /** To store the sticky index's */
  @state() public stickyRightIndexes: number[] = [];

  /** To calculate the rowspan height */
  @state() public cellHeight: number | null = null;

  @state() public headHeight: number | null = null;

  /** To check if the widths are initialized */
  @state() public widthsInitialized = false;

  /** To check if the widths are initialized */
  @state() public lockedWidthColumns: boolean[] = [];

  /** prefix columns width */
  @state() public prefixSumColumnsWidth: PrefixColumn[] = [];

  /** prefix columns width */
  @state() public stickyColumns: number[] = [];

  /** body rows */
  @state() public bodyRows: HTMLElement[] = [];

  /** head rows */
  @state() public headRows: HTMLElement[] = [];

  @state() public needsStructureLayout = false;

  @state() public needsWidthLayout = false;

  @state() public needsSpanLayout = false;

  @state() public colCount = 0;
  
  public isGridReady = false;

  public shadowLeftVisibleSticky?: boolean;

  public shadowRightVisibleSticky?: boolean;

  public stickyLeftHeadEls: HTMLElement[] = [];

  public stickyRightHeadEls: HTMLElement[] = [];

  public stickyLeftBodyEls: HTMLElement[] = [];

  public stickyRightBodyEls: HTMLElement[] = [];

  private resizeTimer: any = 0;

  /** To throttle sticky shadow updates */
  private shadowRaf = 0;

  public resizeBaseColumnWidths: number[] = [];

  constructor() {
    super();
  }

  connectedCallback(): void {
    super.connectedCallback();
    this.emit('nile-init');
  }

  private syncStickyShadowsRaf = () => {
    if (this.shadowRaf) return;
    this.shadowRaf = requestAnimationFrame(() => {
      this.shadowRaf = 0;
      syncStickyShadows(this);
    });
  };

  private updateOverscrollBehavior() {
    const isVerticallyScrollable = this.scrollHeight > this.clientHeight;
    this.style.overscrollBehavior = isVerticallyScrollable
      ? this.overflowScrollBehaviour
      : '';
  }


  private scheduleLayout() {
    if (this.raf) cancelAnimationFrame(this.raf);
    this.raf = requestAnimationFrame(() => {
      this.raf = 0;
      layout(
        this,
        this.slotEl,
        this.ensureWidths.bind(this),
        this.stickyLeftIndexes,
        this.stickyRightIndexes
      );
      syncStickyShadows(this);
    });
  }

  private ensureWidths(colCount: number) {
    if (this.columnWidths.length !== colCount) {
      this.columnWidths = Array(colCount).fill(0);
      this.lockedWidthColumns = Array(colCount).fill(false); 
    }
  }

  private onResize = (e: Event) => {
    e.stopPropagation();
    this.emit('nile-resize', {
      columnNumber: (e as CustomEvent).detail.col,
      columnWidth: (e as CustomEvent).detail.widthPx,
    });
    const { col, widthPx } = (e as CustomEvent).detail as {
      col: number;
      widthPx: number;
    };
    if (!this.resizeBaseColumnWidths) return;
  
    copyArrayValuesInPlace(this.columnWidths, this.resizeBaseColumnWidths);
    const { prevW, newW, delta0 } = computeNewWidth({
      target: e.target as HTMLElement,
      col,
      widthPx,
      floor: this.minColumnWidth,
      columnWidths: this.columnWidths,
    });

    if (delta0 === 0) {
      updateResizeGuides(this, col);
      return;
    }

    if (!this.enableScroll) {
      resizeNoScroll({ grid: this, col, prevW, newW, floor: this.minColumnWidth });
    } else {
      resizeWithScroll({ grid: this, col, newW });
    }
    updateResizeGuides(this, col);
  };

  protected firstUpdated() {
    this.setAttribute('role', 'table');
    listenToEventListeners(
      this,
      this.onResize,
      this.slotEl,
      this._attachObserverAndLayout.bind(this)
    );
    this.addEventListener('scroll', this.syncStickyShadowsRaf);
    this.resizeObserverStickyShadows = new ResizeObserver(() => {
      syncStickyShadows(this);
      this.updateOverscrollBehavior();
    });
    this.resizeObserverStickyShadows.observe(this);
    this.syncStickyShadowsRaf();
  }

  updated(changed: Map<string, unknown>) {
    if (changed.has("overflowScrollBehaviour")) {
      this.updateOverscrollBehavior();
    }
  }

  private _attachObserverAndLayout() {
    this.mo?.disconnect();
    this.mo = new MutationObserver(() => {
      this.needsStructureLayout = true;
      this.needsWidthLayout = true;
      this.needsSpanLayout = true;
      this.scheduleLayout();
    });
    this.resizeObserver?.disconnect();
    this.resizeObserver = new ResizeObserver(() => {
      if (this.resizeTimer) clearTimeout(this.resizeTimer);
      this.resizeTimer = setTimeout(() => {
        this.resizeTimer = 0;
        this.needsWidthLayout = true;
        this.scheduleLayout();
      }, 16);
    });

    const head = getHead(this.slotEl);
    const body = getBody(this.slotEl);
    
    if (!head && !body) return;
    const target = head ?? this;
    this.mo.observe(target, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['colspan', 'rowspan', 'width', 'lockwidth', 'sticky']
    });
    this.resizeObserver?.observe(this);

    if (!this.colCount || !this.columnWidths?.length) {
      this.needsStructureLayout = true;
      this.needsSpanLayout = true;
      this.needsWidthLayout = true;
      this.scheduleLayout();
    }
  }

  public render(): TemplateResult {
    return html`
      <div part="grid-base" class="nile-grid-base">
        <div part="grid-slot-wrapper">
          <slot part="grid-slot"></slot>
        </div>
        <div part="resize-start" class="nile-resize-start"></div>
        <div part="resize-end" class="nile-resize-end"></div>
      </div>
    `;
  }

  disconnectedCallback(): void {
    this.emit('nile-destroy');
    super.disconnectedCallback?.();
    this.mo?.disconnect();
    this.resizeObserver?.disconnect();
    this.resizeObserverStickyShadows?.disconnect();

    if (this.raf) cancelAnimationFrame(this.raf);
    removeEventListeners(
      this,
      this.onResize,
      this.slotEl,
      this._attachObserverAndLayout.bind(this)
    );
  }

  /* #endregion */
}

export default NileGrid;

declare global {
  interface HTMLElementTagNameMap {
    'nile-grid': NileGrid;
  }
}
