import { WebElement } from 'selenium-webdriver';
import { OjTableBase } from './OjTableBase';
import { slotProxy, SlotProxy } from '../../';

/**
 * 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
   */
  async changeSelected<K>(selected: { row: Array<K>; column: Array<K> }) {
    await this.whenReady();
    await this.getDriver().executeScript(
      `
        const ele = arguments[0];
        const selected = arguments[1];
        const keySet = require('ojs/ojkeyset');
        ele.selected = {
          row: new keySet.KeySetImpl(selected.row),
          column: new keySet.KeySetImpl(selected.column)
        };
      `,
      this,
      selected
    );
  }

  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.
   */
  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 this.getDriver().executeAsyncScript<WebElement>(
      `
      const element = arguments[0];
      const rowIndex = arguments[1];
      let columnIndex = arguments[2];
      const rowKey = arguments[3];
      const columnKey = arguments[4];
      const done = arguments[5];
      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) {
        done(null);
      }
      const defaultCellTemplate = element.querySelectorAll('[slot="cellTemplate"]')[0];
      if (columns[columnIndex].template == null && columns[columnIndex].renderer == null &&
          defaultCellTemplate == null) {
            done(null);
      } else {
        if (rowIndex != null) {
          done(element.getNodeBySubId({ subId: 'oj-table-cell', rowIndex: rowIndex, columnIndex: columnIndex }));
        } else {
          require(['ojs/ojcore'], function(oj) {
            const rows = element.querySelectorAll('.oj-table-body-row');
            let row;
            for (let i = 0; i < rows.length; i++) {
              if (oj.KeyUtils.equals(rows[i]['oj-table-oj-row-item'].key, rowKey)) {
                row = rows[i];
                break;
              }
            }
            if (row == null) {
              done(null);
            }
            const cells = row.querySelectorAll('.oj-table-data-cell');
            if (cells.length === columns.length) {
              done(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) {
                  done(cells[i]);
                }
                index += colSpan;
              }
            }
          });
        }
      }
    `,
      this,
      itemLocator.rowIndex,
      itemLocator.columnIndex,
      itemLocator.rowKey,
      itemLocator.columnKey
    );
    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 this.getDriver().executeAsyncScript<WebElement>(
      `
      const element = arguments[0];
      const rowIndex = arguments[1];
      const rowKey = arguments[2];
      const done = arguments[3];
      const rowTemplate = element.querySelectorAll('[slot="rowTemplate"]')[0];
      const rowRenderer = element.rowRenderer;
      if (rowTemplate == null && rowRenderer == null) {
        done(null);
      }
      require(['ojs/ojcore'], function(oj) {
        const rows = element.querySelectorAll('.oj-table-body-row');
        if (rowKey != null) {
          for (let i = 0; i < rows.length; i++) {
            if (oj.KeyUtils.equals(rows[i]['oj-table-oj-row-item'].key, rowKey)) {
              done(rows[i]);
            }
          }
          done(null);
        } else {
          if (rowIndex == null || rowIndex >= rows.length){
            done(null);
          }
          done(rows[rowIndex]);
        }
      });
    `,
      this,
      itemLocator.rowIndex,
      itemLocator.rowKey
    );
    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 this.getDriver().executeAsyncScript<string>(
      `
      const element = arguments[0];
      const done = arguments[3];
      const headerId = arguments[1];
      const sortDirection = arguments[2];
      require(['ojs/ojcustomelement-utils'], function (utils) {
        // Retrieve the widget instance from the element
        const bridge = utils.CustomElementUtils.getElementBridge(element);
        const widget = bridge._WIDGET_INSTANCE;
        const err = widget._doSortHelper(headerId, sortDirection);
        done(err);
      });
    `,
      this,
      header,
      direction
    );
    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 this.getDriver().executeAsyncScript<WebElement>(
      `
      const element = arguments[0];
      const rowIndex = arguments[1];
      const rowKey = arguments[2];
      const done = arguments[3];
      require(['ojs/ojcore'], function(oj) {
        const rows = element.querySelectorAll('.oj-table-body-row');
        if (rowKey != null) {
          for (let i = 0; i < rows.length; i++) {
            if (oj.KeyUtils.equals(rows[i]['oj-table-oj-row-item'].key, rowKey)) {
              done(rows[i]);
            }
          }
          done(null);
        } else {
          if (rowIndex == null || rowIndex >= rows.length){
            done(null);
          }
          done(rows[rowIndex]);
        }
      });
    `,
      this,
      itemLocator.rowIndex,
      itemLocator.rowKey
    );
    if (el) {
      const actions = this.getDriver().actions({ bridge: true });
      await actions.click(el).perform();
      await this.whenBusyContextReady();
    } else {
      throw Error(`No corresponding row template or renderer found for the itemLocator`);
    }
  }
}
