import { css, CSSResult, html, TemplateResult } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { ClassInfo, classMap } from "lit/directives/class-map.js";
import { dataAttr } from "../../directives/data-attr.js";
import '../../define/ui/ui-divider.js';

export interface IDataTableOptions {
  /**
   * The name of the property that identifies the key. The key is inserted into each row as `data-key`.
   * Default to `key`.
   */
  key?: string;
  /**
   * The value of the aria-label to render on the table.
   */
  ariaLabel?: string;

  /**
   * When true it dispatches `activate` event when the user double click on a row.
   */
  active?: boolean;
  /**
   * When set it allows a single selection. A list item is highlighted and the `select` event is dispatched when the selection change.
   */
  selection?: 'single' | 'multi';
}

export type RenderContent = string | TemplateResult | TemplateResult[];

type DataEvents = 'header' | 'item' | 'empty' | 'activate' | 'select' | 'render' | 'scrollend';

type CellCallback<T> = (item: T) => TemplateResult[];
type ActivateCallback<T> = (item: T) => void;
type SelectCallback = (item: string | string[]) => void;
type Callback = () => void;

type DataEventsMap<T> = {
  [key in DataEvents]?: Callback | SelectCallback | CellCallback<T> | CellCallback<T[]> | ActivateCallback<T> | ActivateCallback<T[]> | {(): TemplateResult | TemplateResult[]};
};

export interface ICellOptions {
  /**
   * The list of CSS class names to add to the cell.
   */
  class?: string;

  /**
   * The list of data attributes to set on the cell.
   */
  data?: { [key: string]: number | string | boolean };
  /**
   * The value of the aria-label to render on the cell.
   */
  ariaLabel?: string;
}

interface CellPosition { 
  row: number; 
  cell: number; 
}

/**
 * A helper class that allows to build a data table in a programmatic way.
 * 
 * Usage:
 * 
 * const dt = new DataTable(arrayOfItems, { key: 'key' });
 * // or dt.items = arrayOfItems;
 * dt.addEventListener('header', () => { 
 *  const c1 = dt.header('name', { class: 'name-column', data: {a: b} });
 *  const c2 = dt.header('created', { class: 'created-column', data: {a: b} });
 *  return [c1, c2];
 * });
 * dt.addEventListener('item', (item) => { 
 *   const c1 = dt.cell(html`cell template content`, { class: 'a b c', data: {a: b} });
 *   const c2 = dt.cell(`cell text content`, { class: 'xyz', data: {key: 'value'} });
 *   return [c1, c2];
 * });
 * dt.addEventListener('empty', () => { 
 *   return html`<p>The table has no records.</p>`;
 * });
 * 
 * const template = dt.render();
 * 
 * @fires header - An event dispatched when a table header row is being rendered.
 * @fires item - An event dispatched when a table item row is being rendered.
 * @fires empty - An event dispatched when a table has no rows to render.
 * @fires activate - An event dispatched when the user double click on a row.
 * @fires select - An event dispatched when the user selected a table item in a single or multi selection table.
 * @fires render - An event dispatched when the internal state changed and the table must be rendered again.
 */
export class DataTable<T> {
  static get styles(): CSSResult {
    return css`
    .data-table {
      flex: 1;
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }

    .data-table-body {
      flex: 1;
      overflow: auto;
    }

    .data-table-row {
      height: 44px;
      position: relative;

      display: flex;
      align-items: center;

      font-family: var(--md-sys-typescale-body-medium-font-family-name);
      font-style: var(--md-sys-typescale-body-medium-font-family-style);
      font-weight: var(--md-sys-typescale-body-medium-font-weight);
      font-size: var(--md-sys-typescale-body-medium-font-size);
      letter-spacing: var(--md-sys-typescale-body-medium-tracking);
      line-height: var(--md-sys-typescale-body-medium-height);
      text-transform: var(--md-sys-typescale-body-medium-text-transform);
      text-decoration: var(--md-sys-typescale-body-medium-text-decoration);
    }

    .data-table-row-body {
      height: inherit;
      display: flex;
      align-items: center;
      flex: 1;
    }

    .data-table-body .data-table-row:not(.data-table-empty-row):hover::before {
      content: '';
      background-color: var(--md-sys-color-on-surface);
      opacity: var(--md-sys-state-hover-state-layer-opacity);
      position: absolute;
      inset: 0;
      z-index: -1;
    }

    .data-table-header {
      font-family: var(--md-sys-typescale-label-large-font-family-name);
      font-style: var(--md-sys-typescale-label-large-font-family-style);
      font-weight: var(--md-sys-typescale-label-large-font-weight);
      font-size: var(--md-sys-typescale-label-large-font-size);
      letter-spacing: var(--md-sys-typescale-label-large-tracking);
      line-height: var(--md-sys-typescale-label-large-height);
      text-transform: var(--md-sys-typescale-label-large-text-transform);
      text-decoration: var(--md-sys-typescale-label-large-text-decoration);
    }

    .data-table-cell {
      display: flex;
      align-items: center;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      height: inherit;
      padding: 12px;
      box-sizing: border-box;
      position: relative;
    }

    .data-table-row.data-table-empty-row {
      height: auto;
      display: block;
    }

    .data-table-row.data-table-empty-row > [role=cell] {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
    }

    .selected {
      background-color: var(--md-sys-color-secondary-container);
      border-radius: 8px;
    }

    .data-table-cell:focus-visible::before {
      content: '';
      background-color: var(--md-sys-color-on-surface);
      opacity: var(--md-sys-state-focus-state-layer-opacity);
      position: absolute;
      inset: 0;
      z-index: -1;
    }
    `;
  }

  #renderItems?: T[];

  get items(): ArrayLike<T> | undefined {
    return this.#renderItems;
  }

  set items(value: ArrayLike<T> | undefined) {
    if (!value) {
      this.#renderItems = undefined;
      return;
    }
    const arr = Array.from(value);
    this.#renderItems = arr;
  }

  options: IDataTableOptions;
  
  #listeners: DataEventsMap<T> = {};

  #getItems: () => ArrayLike<T>;

  /**
   * The list of keys that correspond to the `key` set on the options of items that are currently selected.
   * Note, this is a common list for single and multi selection. In a single selection only the first is taken into account.
   */
  selected: string[] = [];

  /**
   * Describes currently focused cell.
   * The `row` is the index of the focused row (starting with the header) and the `cell` is the index of the focused cell.
   */
  protected focused?: CellPosition;

  constructor(itemsCallback: () => ArrayLike<T>, options: IDataTableOptions = {}) {
    this.options = options;
    this.#getItems = itemsCallback;
    this.dblclickHandler = this.dblclickHandler.bind(this);
    this.handleClick = this.handleClick.bind(this);
    this.handleKeyDown = this.handleKeyDown.bind(this);
    this.handleFocus = this.handleFocus.bind(this);
    this.handleListScroll = this.handleListScroll.bind(this);
  }

  addEventListener(type: 'header', listener: () => TemplateResult[]): void;

  addEventListener(type: 'render', listener: () => void): void;

  addEventListener(type: 'item', listener: CellCallback<T>): void;

  addEventListener(type: 'empty', listener: () => TemplateResult): void;

  addEventListener(type: 'activate', listener: ActivateCallback<T>): void;

  addEventListener(type: 'select', listener: SelectCallback): void;

  addEventListener(type: 'scrollend', listener: Callback): void;

  addEventListener(type: DataEvents, listener: SelectCallback | CellCallback<T> | CellCallback<T[]> | ActivateCallback<T> | ActivateCallback<T[]> | {(): TemplateResult | TemplateResult[]}): void {
    this.#listeners[type] = listener;
  }

  /**
   * Renders a header cell.
   * 
   * @param content The content of the header cell to render.
   * @param options Cell rendering options
   */
  header(content: RenderContent, options: ICellOptions = {}): TemplateResult {
    const data = options.data || {};
    return html`<span 
      role="columnheader" 
      class="data-table-cell data-table-header-cell ${options.class || ''}"
      aria-label="${ifDefined(options.ariaLabel)}"
      ${dataAttr(data)}
    >${content}</span>`;
  }

  /**
   * Renders a body cell.
   * 
   * @param content The content of the header cell to render.
   * @param options Cell rendering options
   */
  cell(content: RenderContent, options: ICellOptions = {}): TemplateResult {
    const data = options.data || {};
    return html`
    <span 
      role="gridcell" 
      class="data-table-cell data-table-body-cell ${options.class || ''}"
      ${dataAttr(data)}
    >${content}</span>
    `;
  }

  render(): TemplateResult {
    this.items = this.#getItems();
    const { options } = this;
    const clickListener = options.selection ? this.handleClick : undefined;
    return html`
    <div 
      class="data-table" 
      role="grid" 
      aria-label="${ifDefined(options.ariaLabel)}"
      aria-multiselectable="${options.selection === 'multi'}"
      aria-readonly="true"
      aria-rowcount="-1"
      tabindex="0"
      @focus="${this.handleFocus}"
      @click="${clickListener}" 
      @keydown="${this.handleKeyDown}"
    >
      ${this.renderHeader()}
      <ui-divider></ui-divider>
      ${this.renderBody()}
    </div>
    `;
  }

  protected rowFromEvent(e: Event): HTMLElement | undefined {
    const path = e.composedPath();
    const row = path.find(current => {
      const element = current as Element;
      if (element.classList && element.classList.contains('data-table-row')) {
        return true;
      }
      return false;
    }) as HTMLElement | undefined;
    if (!row || !row.dataset.key) {
      return undefined;
    }
    return row;
  }

  protected tableFromEvent(e: Event): HTMLElement | undefined {
    const path = e.composedPath();
    const row = path.find((current) => {
      const element = current as Element;
      if (element.classList && element.classList.contains('data-table')) {
        return true;
      }
      return false;
    }) as HTMLElement | undefined;
    return row;
  }

  protected cellPositionFromEvent(e: Event): CellPosition {
    const result: CellPosition = {
      cell: 0,
      row: 0,
    };
    let row: HTMLElement | undefined;
    let column: HTMLElement | undefined;
    let table: HTMLElement | undefined;
    const path = e.composedPath();
    for (const current of path.values()) {
      const element = current as HTMLElement;
      if (element.classList && element.classList.contains('data-table-row')) {
        row = element;
      } else if (element.classList && element.classList.contains('data-table-cell')) {
        column = element;
      } else if (element.classList && element.classList.contains('data-table')) {
        table = element;
        break;
      }
    }
    if (!table) {
      return result;
    }
    if (row) {
      const rows = Array.from(table.querySelectorAll(`.data-table-row`));
      const rowIndex = rows.indexOf(row);
      if (rowIndex >= 0) {
        result.row = rowIndex;
      }
      if (column) {
        const columns = Array.from(row.querySelectorAll(`.data-table-cell`));
        const cellIndex = columns.indexOf(column);
        if (cellIndex >= 0) {
          result.cell = cellIndex;
        }
      }
    }
    return result;
  }

  protected cellRowFromEvent(e: Event): { column: HTMLElement, row: HTMLElement } | undefined {
    let row: HTMLElement | undefined;
    let column: HTMLElement | undefined;
    const path = e.composedPath();
    for (const current of path.values()) {
      const element = current as HTMLElement;
      if (element.classList && element.classList.contains('data-table-row')) {
        row = element;
        break;
      } else if (element.classList && element.classList.contains('data-table-cell')) {
        column = element;
      }
    }
    if (!row || !column) {
      return undefined;
    }
    return { row, column }
  }

  protected handleFocus(e: Event): void {
    const target = e.target as HTMLElement;
    if (target.classList.contains('data-table')) {
      // focusing on the table, move the focus to the first cell
      this.focusFirstCell(target);
    }
  }

  protected dblclickHandler(e: Event): void {
    const callback = this.#listeners.activate as (item: T) => void;
    if (!callback) {
      return;
    }
    const row = this.rowFromEvent(e)
    if (!row) {
      return;
    }
    const { key } = row.dataset;
    const keyProperty = this.options.key || 'key';
    const item = this.#renderItems?.find(i => (i as unknown as {[key: string]: unknown})[keyProperty] === key);
    if (!item) {
      return;
    }
    callback(item);
  }

  protected handleClick(e: Event): void {
    const { selection } = this.options;
    const row = this.rowFromEvent(e)
    if (!row) {
      return;
    }
    if (selection === 'single') {
      this.handleSingleSelection(row.dataset.key as string);
    } else if (selection === 'multi') {
      this.handleMultiSelection(row.dataset.key as string);
    }
    const table = this.tableFromEvent(e) as HTMLElement;
    this.focused = this.cellPositionFromEvent(e);
    this.focusCell(table, this.focused.row, this.focused.cell);
  }

  protected handleKeyDown(e: KeyboardEvent): void {
    if (e.defaultPrevented) {
      return;
    }
    if (['Space', 'Enter'].includes(e.code)) {
      this.handlePrimaryAction(e);
    } else if (e.code === 'ArrowRight') {
      this.moveFocusRight(e);
    } else if (e.code === 'ArrowDown') {
      this.moveFocusDown(e);
    } else if (e.code === 'ArrowUp') {
      this.moveFocusUp(e);
    } else if (e.code === 'ArrowLeft') {
      this.moveFocusLeft(e);
    }
  }

  protected handleSingleSelection(key: string): void {
    if (this.selected.includes(key)) {
      return;
    }
    this.selected = [key];
    this.dispatchRender();
    this.dispatchSelected(key);
  }

  protected handleMultiSelection(key: string): void {
    const { selected } = this;
    if (selected.includes(key)) {
      const index = selected.indexOf(key);
      selected.splice(index, 1);
    } else {
      selected.push(key);
    }
    this.dispatchRender();
    this.dispatchSelected(selected);
  }

  protected focusFirstCell(table: HTMLElement): void {
    const cell = table.querySelector('[role="columnheader"]') as HTMLElement | null;
    if (!cell) {
      return;
    }
    cell.setAttribute('tabindex', '0');
    cell.focus();
    table.removeAttribute('tabindex');
    this.focused = { cell: 0, row: 0 };
  }

  protected focusCell(table: HTMLElement, rowIndex: number, colIndex: number): boolean {
    const row = table.querySelectorAll('[role="row"]')[rowIndex];
    if (!row) {
      return false;
    }
    const cell = row.querySelectorAll(`.data-table-cell`)[colIndex] as HTMLElement | null;
    if (!cell) {
      return false;
    }
    const current = table.querySelector(`.data-table-cell[tabindex]`);
    cell.setAttribute('tabindex', '0');
    cell.focus();
    if (current) {
      current.removeAttribute('tabindex');
    } 
    if (table.hasAttribute('tabindex')) {
      table.removeAttribute('tabindex');
    }
    return true;
  }

  protected moveFocusRight(e: Event): void {
    e.preventDefault();
    const table = this.tableFromEvent(e);
    if (!table) {
      return;
    }
    const { focused } = this;
    if (!focused) {
      this.focusFirstCell(table);
      return;
    }
    if (this.focusCell(table, focused.row, focused.cell + 1)) {
      focused.cell += 1;
    }
  }

  protected moveFocusLeft(e: Event): void {
    e.preventDefault();
    const table = this.tableFromEvent(e);
    if (!table) {
      return;
    }
    const { focused } = this;
    if (!focused) {
      this.focusFirstCell(table);
      return;
    }
    if (this.focusCell(table, focused.row, focused.cell - 1)) {
      focused.cell -= 1;
    }
  }

  protected moveFocusDown(e: Event): void {
    e.preventDefault();
    const table = this.tableFromEvent(e);
    if (!table) {
      return;
    }
    const { focused } = this;
    if (!focused) {
      this.focusFirstCell(table);
      return;
    }
    if (this.focusCell(table, focused.row + 1, focused.cell)) {
      focused.row += 1;
    }
  }

  protected moveFocusUp(e: Event): void {
    e.preventDefault();
    const table = this.tableFromEvent(e);
    if (!table) {
      return;
    }
    const { focused } = this;
    if (!focused) {
      this.focusFirstCell(table);
      return;
    }
    if (this.focusCell(table, focused.row - 1, focused.cell)) {
      focused.row -= 1;
    }
  }

  protected handlePrimaryAction(e: Event): void {
    e.preventDefault();
    const info = this.cellRowFromEvent(e);
    if (!info) {
      return;
    }
    const { row, column } = info;
    // first check if the cell has an focusable element. If so trigger action on it.
    const focusable = column.querySelector('[tabindex="0"]') as HTMLElement | null;
    if (focusable) {
      focusable.click();
      return;
    }
    const { selection } = this.options;
    if (selection === 'single') {
      this.handleSingleSelection(row.dataset.key as string);
    } else if (selection === 'multi') {
      this.handleMultiSelection(row.dataset.key as string);
    }
  }

  protected handleListScroll(e: Event): void {
    const list = e.target as HTMLElement;
    const { scrollTop, offsetHeight, scrollHeight } = list;
    const bottom = scrollTop + offsetHeight >= scrollHeight - 20; // 20 is the offset which qualifies as the end. An arbitrary number.
    if (bottom) {
      this.dispatchScrollEnd();
    }
  }

  protected renderHeader(): TemplateResult {
    const cells = this.dispatchHeader();
    return html`
    <div role="rowgroup">
      <div role="row" class="data-table-row data-table-header">
        ${cells}
      </div>
    </div>
    `;
  }

  protected renderBody(): TemplateResult {
    const { active = false } = this.options;
    const items = this.#renderItems;
    const contents = !!items && !!items.length ? items.map(item => this.renderItem(item)) : this.renderEmpty();
    const dbListener = active ? this.dblclickHandler : undefined;
    return html`
    <div class="data-table-body" role="rowgroup" @dblclick="${dbListener}" @scroll="${{ handleEvent: this.handleListScroll, passive: true }}">
      ${contents}
    </div>`;
  }

  protected renderItem(item: T): TemplateResult {
    const cells = this.dispatchItem(item);
    const { selected, options } = this;
    const typed = item as unknown as Record<string, string>;
    const key = typed[options.key || 'key'];
    let isSelected = false;
    if (options.selection === 'single') {
      const [selectedKey] = selected;
      if (selectedKey === key) {
        isSelected = true;
      }
    } else if (options.selection === 'multi') {
      isSelected = selected.includes(key);
    }
    const classes: ClassInfo = {
      'data-table-row': true,
      selected: isSelected,
    };
    return html`
    <div role="row" class="${classMap(classes)}" data-key="${key}" aria-selected="${isSelected ? 'true' : 'false'}">
      <div class="data-table-row-body" role="presentation">
        ${cells}
      </div>
    </div>
    `;
  }

  protected renderEmpty(): TemplateResult {
    const content = this.dispatchEmpty();
    return html`
    <div role="row" class="data-table-row data-table-empty-row">
      <div role="cell">
      ${content}
      </div>
    </div>
    `;
  }

  protected dispatchHeader(): TemplateResult[] | undefined {
    const callback = this.#listeners.header as () => TemplateResult[] | undefined;
    if (!callback) {
      return undefined;
    }
    let content: TemplateResult[] | undefined;
    try {
      content = callback();
    } catch (e) {
      // eslint-disable-next-line no-console
      console.warn(e);
    }
    return content;
  }

  protected dispatchItem(item: T): TemplateResult[] | undefined {
    const callback = this.#listeners.item as CellCallback<T> | undefined;
    if (!callback) {
      return undefined;
    }
    let content: TemplateResult[] | undefined;
    try {
      content = callback(item);
    } catch (e) {
      // eslint-disable-next-line no-console
      console.warn(e);
    }
    return content;
  }

  protected dispatchEmpty(): TemplateResult | undefined {
    const callback = this.#listeners.empty as () => TemplateResult | undefined;
    if (!callback) {
      return undefined;
    }
    let content: TemplateResult | undefined;
    try {
      content = callback();
    } catch (e) {
      // eslint-disable-next-line no-console
      console.warn(e);
    }
    return content;
  }

  protected dispatchRender(): void {
    const callback = this.#listeners.render as () => void;
    if (!callback) {
      return;
    }
    try {
      callback();
    } catch (e) {
      // eslint-disable-next-line no-console
      console.warn(e);
    }
  }

  protected dispatchSelected(selection: string | string[]): void {
    const callback = this.#listeners.select as SelectCallback;
    if (!callback) {
      return;
    }
    try {
      callback(selection);
    } catch (e) {
      // eslint-disable-next-line no-console
      console.warn(e);
    }
  }

  protected dispatchScrollEnd(): void {
    const callback = this.#listeners.scrollend as Callback;
    if (!callback) {
      return;
    }
    try {
      callback();
    } catch (e) {
      // eslint-disable-next-line no-console
      console.warn(e);
    }
  }
}
