import { WebElement } from 'selenium-webdriver';
import { OjTableBase } from './OjTableBase';
import { slotProxy, SlotProxy } from '../../';
import { executeWithModules } from '../../lib/oj-module-proxy';

/**
 * The component WebElement for [oj-table](../../jsdocs/oj.ojTable.html).
 * Do not instantiate this class directly, instead, use
 * [ojTable](../functions/elements.ojTable-1.html).
 */
export class OjTable extends OjTableBase {
  /**
   * Sets the value of "selected" property.
   * Specifies the current selected rows and/or columns in the table. See the Help documentation for more information.
   * @param selected The value to set for "selected"
   * @override
   * @typeparam K Type of keys
   * @deprecated Since 17.0.0 use changeSelectedKeys as the replacement.
   */
  async changeSelected<K>(selected: { row: Array<K>; column: Array<K> }) {
    await this.whenReady();
    return executeWithModules<void>(
      this.getDriver(),
      ['KeySet'],
      [this, selected],
      `
      (({ KeySetImpl }, ele, selected) => {
        ele.selected = {
          row: new KeySetImpl(selected.row),
          column: new KeySetImpl(selected.column)
        };
      })
      `
    );
  }

  private _changeScrollPosition(scrollPosition: { rowIndex: number } | { rowKey: any }) {
    // OjTableBase.ScrollPosition marks all properties required, although they're not
    // @ts-ignore
    return super.changeScrollPosition(scrollPosition);
  }

  /**
   * Gets the value of "selected" property.
   * Specifies the current selected rows and/or columns in the table. See the Help documentation for more information.
   * @override
   * @typeparam K Type of keys
   * @return The value of "selected" property.
   * @deprecated Since 17.0.0 use getSelectedKeys as the replacement.
   */
  async getSelected<K>(): Promise<{ row: Array<K>; column: Array<K> }> {
    await this.whenReady();
    const selected: { row: Array<K>; column: Array<K> } = await this.getDriver().executeScript(
      `
      const ele = arguments[0];
      const selected = {
        row: Array.from(ele.selected.row.values()),
        column: Array.from(ele.selected.column.values())
      }
      return selected;
    `,
      this
    );
    return selected;
  }

  /**
   * Retrieve a SlotProxy which represents a single cell.
   * Should be used only if cell contents are rendered using cell template/renderer/slot. Otherwise method will throw an exception.
   * @param itemLocator Either column index or key and row index or key of the cell.
   */
  async findCell<K>(
    itemLocator: (
      | { columnIndex: number; columnKey?: never }
      | { columnKey: string; columnIndex?: never }
    ) &
      ({ rowIndex: number; rowKey?: never } | { rowKey: K; rowIndex?: never })
  ): Promise<SlotProxy> {
    // Call changeScrollPosition to scroll the row into view and ensure it's
    // rendered in DOM
    if (typeof itemLocator.rowIndex !== 'undefined') {
      await this._changeScrollPosition({
        rowIndex: itemLocator.rowIndex
      });
    } else {
      await this._changeScrollPosition({
        rowKey: itemLocator.rowKey
      });
    }
    const el = await executeWithModules<WebElement>(
      this.getDriver(),
      ['KeyUtils'],
      [this, itemLocator],
      `
      (({ equals }, element, { rowIndex, columnIndex, rowKey, columnKey }) => {
      const columns = element.columns;
      if (columnIndex == null) {
        for (let i = 0; i < columns.length; i++) {
          if (columns[i].id === columnKey) {
            columnIndex = i;
            break;
          }
        }
      }
      if (columnIndex == null || columnIndex >= columns.length) {
        return null;
      }
      const defaultCellTemplate = element.querySelectorAll('[slot="cellTemplate"]')[0];
      if (columns[columnIndex].template == null && columns[columnIndex].renderer == null &&
          defaultCellTemplate == null) {
            return null;
      } else {
        if (rowIndex != null) {
          return element.getNodeBySubId({ subId: 'oj-table-cell', rowIndex: rowIndex, columnIndex: columnIndex });
        } else {
          const rows = element.querySelectorAll('.oj-table-body-row');
          let row;
          for (let i = 0; i < rows.length; i++) {
            if (equals(rows[i]['oj-table-oj-row-item'].key, rowKey)) {
              row = rows[i];
              break;
            }
          }
          if (row == null) {
            return null;
          }
          const cells = row.querySelectorAll('.oj-table-data-cell');
          if (cells.length === columns.length) {
            return cells[columnIndex];
          } else {
            let index = 0;
            for (let i = 0; i < cells.length; i++) {
              let colSpan = cells[i].colSpan
              if (columnIndex >= index && columnIndex < index + colSpan) {
                return cells[i];
              }
              index += colSpan;
            }
          }
        }
      }
      })
    `
    );
    if (el) {
      return slotProxy(el, this);
    } else {
      throw Error(`No corresponding cell template or renderer found for the itemLocator`);
    }
  }

  /**
   * Retrieve a SlotProxy which represents a single row.
   * Should be used only if row is rendered using rowTemplate slot or renderer. Otherwise method will throw an exception.
   * @param itemLocator Either index or key of the row.
   */
  async findRow<K>(
    itemLocator: { rowIndex: number; rowKey?: never } | { rowKey: K; rowIndex?: never }
  ): Promise<SlotProxy> {
    // Call changeScrollPosition to scroll the row into view and ensure it's
    // rendered in DOM
    if (typeof itemLocator.rowIndex !== 'undefined') {
      await this._changeScrollPosition({
        rowIndex: itemLocator.rowIndex
      });
    } else {
      await this._changeScrollPosition({
        rowKey: itemLocator.rowKey
      });
    }
    const el = await executeWithModules<WebElement>(
      this.getDriver(),
      ['KeyUtils'],
      [this, itemLocator],
      `
      (({ equals }, element, { rowIndex, rowKey }) => {
      const rowTemplate = element.querySelectorAll('[slot="rowTemplate"]')[0];
      const rowRenderer = element.rowRenderer;
      if (rowTemplate == null && rowRenderer == null) {
        return null;
      }
        const rows = element.querySelectorAll('.oj-table-body-row');
        if (rowKey != null) {
          for (let i = 0; i < rows.length; i++) {
            if (equals(rows[i]['oj-table-oj-row-item'].key, rowKey)) {
              return rows[i];
            }
          }
          return null;
        } else {
          if (rowIndex == null || rowIndex >= rows.length){
            return null;
          }
          return rows[rowIndex];
        }
      })
    `
    );
    if (el) {
      return slotProxy(el, this);
    } else {
      throw Error(`No corresponding row template or renderer found for the itemLocator`);
    }
  }

  /**
   * Retrieve a SlotProxy which represents the given header cell.
   * Should be used only if header is rendered using template/renderer/slot. Otherwise method will throw an exception.
   * @param itemLocator Either index or key of the header cell.
   */
  async findHeader(
    itemLocator:
      | { columnIndex: number; columnKey?: never }
      | { columnKey: string; columnIndex?: never }
  ): Promise<SlotProxy> {
    await this.whenReady();
    const el = await this.getDriver().executeScript<WebElement>(
      `
      const element = arguments[0];
      let columnIndex = arguments[1];
      const columnKey = arguments[2];
      const columns = element.columns;
      if (columnIndex == null) {
        for (let i = 0; i < columns.length; i++) {
          if (columns[i].id === columnKey) {
            columnIndex = i;
            break;
          }
        }
      }
      if (columnIndex == null || columnIndex >= columns.length) {
        return null;
      }
      const defaultHeaderTemplate = element.querySelectorAll('[slot="headerTemplate"]')[0];
      if (columns[columnIndex].headerTemplate == null &&
        columns[columnIndex].headerRenderer == null && defaultHeaderTemplate == null) {
        return null;
      }
      return element.getNodeBySubId({ subId: 'oj-table-header', index: columnIndex})
    `,
      this,
      itemLocator.columnIndex,
      itemLocator.columnKey
    );
    if (el) {
      return slotProxy(el, this);
    } else {
      throw Error(`No corresponding header template or renderer found for the itemLocator`);
    }
  }

  /**
   * Retrieve a SlotProxy which represents the given footer cell.
   * Should be used only if footer is rendered using template/renderer/slot. Otherwise method will throw an exception.
   * @param itemLocator Either index or key of the footer cell.
   */
  async findFooter(
    itemLocator:
      | { columnIndex: number; columnKey?: never }
      | { columnKey: string; columnIndex?: never }
  ): Promise<SlotProxy> {
    await this.whenReady();
    const el = await this.getDriver().executeScript<WebElement>(
      `
      const element = arguments[0];
      let columnIndex = arguments[1];
      const columnKey = arguments[2];
      const columns = element.columns;
      if (columnIndex == null) {
        for (let i = 0; i < columns.length; i++) {
          if (columns[i].id === columnKey) {
            columnIndex = i;
            break;
          }
        }
      }
      if (columnIndex == null || columnIndex >= columns.length) {
        return null;
      }
      const defaultFooterTemplate = element.querySelectorAll('[slot="footerTemplate"]')[0];
      if (columns[columnIndex].footerTemplate == null &&
        columns[columnIndex].footerRenderer == null && defaultFooterTemplate == null) {
        return null;
      }
      return element.getNodeBySubId({ subId: 'oj-table-footer', index: columnIndex })
    `,
      this,
      itemLocator.columnIndex,
      itemLocator.columnKey
    );
    if (el) {
      return slotProxy(el, this);
    } else {
      throw Error(`No corresponding footer template or renderer found for the itemLocator`);
    }
  }

  /**
   * Sort the given column.
   * @param header The key of the header which need to be sorted.
   * @param {'ascending'|'descending'} direction The direction of the sort.
   */
  async doSort(header: string, direction: 'ascending' | 'descending') {
    await this.whenReady();
    const msg = await executeWithModules<string>(
      this.getDriver(),
      ['CustomElementUtils'],
      [this, header, direction],
      `
      (({ getElementBridge }, element, headerId, sortDirection) => {
        // Retrieve the widget instance from the element
        const bridge = getElementBridge(element);
        const widget = bridge._WIDGET_INSTANCE;
        return widget._doSortHelper(headerId, sortDirection);
      })
    `
    );
    if (msg) {
      throw Error(msg);
    }
  }

  /**
   * Set a given row active. Triggers a ojRowAction Event.
   * @param itemLocator.rowKey The rowKey within the collection's dataset associated with the item. (Optional if rowIndex is provided)
   * @param itemLocator.rowIndex The rowIndex within the collection's dataset associated with the item. (Optional if rowKey is provided)
   */
  async doRowAction<K>(
    itemLocator: { rowIndex: number; rowKey?: never } | { rowKey: K; rowIndex?: never }
  ): Promise<void> {
    // Call changeScrollPosition to scroll the row into view and ensure it's
    // rendered in DOM
    if (typeof itemLocator.rowIndex !== 'undefined') {
      await this._changeScrollPosition({
        rowIndex: itemLocator.rowIndex
      });
    } else {
      await this._changeScrollPosition({
        rowKey: itemLocator.rowKey
      });
    }
    const el = await executeWithModules<WebElement>(
      this.getDriver(),
      ['KeyUtils'],
      [this, itemLocator],
      `
      (({ equals }, element, { rowIndex, rowKey }) => {
        const rows = element.querySelectorAll('.oj-table-body-row');
        if (rowKey != null) {
          for (let i = 0; i < rows.length; i++) {
            if (equals(rows[i]['oj-table-oj-row-item'].key, rowKey)) {
              return rows[i];
            }
          }
          return null;
        } else {
          if (rowIndex == null || rowIndex >= rows.length){
            return null;
          }
          return rows[rowIndex];
        }
      })
    `
    );
    if (el) {
      const actions = this.getDriver().actions({ bridge: true });
      return actions.click(el).perform();
    } else {
      throw Error(`No corresponding row template or renderer found for the itemLocator`);
    }
  }

  /**
   * Sets the value of "selected" property.
   * If row/column are both unspecified selected for both will be empty.
   * If no object is set on row/column, selected for that axis will be empty.
   * If 'all' is unset for row/column object only 'keys' will be considered, if no 'keys' are specified selected for that axis will be empty.
   * See the Help documentation for more information.
   * @param selected The value to set for "selected"
   * @typeparam K Type of keys
   */
  async changeSelectedKeys<K>(selected: {
    row?:
      | { all: true; keys?: never; deletedKeys: Array<K> }
      | { all: false; keys: Array<K>; deletedKeys?: never };
    column?:
      | { all: true; keys?: never; deletedKeys: Array<K> }
      | { all: false; keys: Array<K>; deletedKeys?: never };
  }) {
    await this.whenReady();
    return executeWithModules<void>(
      this.getDriver(),
      ['KeySet'],
      [this, selected],
      `
      (({ KeySetImpl, AllKeySetImpl }, ele, selected, all) => {
        ele.selected = {
          row: selected.row?.all ? new AllKeySetImpl().delete(selected.row.deletedKeys) : new KeySetImpl(selected.row?.keys),
          column: selected.column?.all ? new AllKeySetImpl().delete(selected.column.deletedKeys) : new KeySetImpl(selected.column?.keys)
        };
      })
      `
    );
  }

  /**
   * Gets the value of "selected" property.
   * @typeparam K Type of keys
   * @return The value of "selected" property.
   */
  async getSelectedKeys<K>(): Promise<{
    row:
      | { all: true; keys?: never; deletedKeys: Array<K> }
      | { all: false; keys: Array<K>; deletedKeys?: never };
    column:
      | { all: true; keys?: never; deletedKeys: Array<K> }
      | { all: false; keys: Array<K>; deletedKeys?: never };
  }> {
    await this.whenReady();
    const selected: {
      row:
        | { all: true; keys?: never; deletedKeys: Array<K> }
        | { all: false; keys: Array<K>; deletedKeys?: never };
      column:
        | { all: true; keys?: never; deletedKeys: Array<K> }
        | { all: false; keys: Array<K>; deletedKeys?: never };
    } = await this.getDriver().executeScript(
      `
      const ele = arguments[0];
      let row = ele.selected.row.keys.all ? { all: true, deletedKeys: Array.from(ele.selected.row.keys.deletedKeys) } : 
        { all: false, keys: Array.from(ele.selected.row.keys.keys) };
      let column = ele.selected.column.keys.all ? { all: true, deletedKeys: Array.from(ele.selected.column.keys.deletedKeys) } : 
        { all: false, keys: Array.from(ele.selected.column.keys.keys) };
      return { row, column };
      `,
      this
    );
    return selected;
  }
}
