import { By, Key, Locator, WebDriver, WebElement, WebElementPromise } from 'selenium-webdriver';
import { OjMenu, OjSwipeActions, ojMenu, ojSwipeActions } from '../elements';

/**
 * A proxy to the underlying WebElement which represents a slot/single row within
 * the component.
 */
export interface SlotProxy {
  /**
   * Find a WebElement within this SlotProxy
   * @param locator The element locator
   */
  findElement(locator: Locator): WebElementPromise;
  /**
   * Find an array of matching elements within this SlotProxy
   * @param locator The elements locator
   */
  findElements(locator: Locator): Promise<WebElement[]>;
  /**
   * Opens the contextMenu and the invokes the action provided for the element associated with this SlotProxy.
   * @param action The value of the &lt;oj-option> to select
   * @since "11.0.0”
   * @example
   * <pre>
   *  const proxy = await treeview.findItem({ key: "news" });
   *  await proxy.doContextualAction("action1");
   * </pre>
   *
   */
  doContextualAction(action: string): Promise<void>;
}

/**
 * Create a SlotProxy proxy for the given WebElement
 * @param el The WebElement to proxy
 * @param parent Optional WebElement hostComponent
 * @return {SlotProxy} A SlotProxy implementation which wraps the given WebElement.
 */
export function slotProxy(el: WebElement, parent?: WebElement): SlotProxy {
  if (!el) {
    throw Error('No root element given for SlotProxy');
  }
  return new SlotProxyImpl(el, parent);
}

/**
 * Default implementation of SlotProxy, used to wrap the underlying WebElement
 * for the collection row and delegate exposed functionality to that WebElement.
 * @private
 */
class SlotProxyImpl implements SlotProxy {
  private _root: WebElement;
  private _parent: WebElement | undefined;

  constructor(el: WebElement, parent?: WebElement) {
    this._root = el;
    this._parent = parent;
  }

  getDriver(): WebDriver {
    return this._root.getDriver();
  }

  findElement(locator: Locator): WebElementPromise {
    return this._root.findElement(locator);
  }

  findElements(locator: Locator): Promise<WebElement[]> {
    return this._root.findElements(locator);
  }
  /*
   * Invokes the contextual Action of the rootNode.
   */
  async doContextualAction(action: string): Promise<void> {
    if (!this._parent) {
      throw Error('Slot Proxy must be initialized with a parent to do a contextual action');
    }
    await this.scrollIntoView();
    const doesSwipeActionExist = await this.doesSwipeActionExist();
    if (doesSwipeActionExist) {
      const ojSwipeAction = await this.getSwipeAction();
      return ojSwipeAction.doAction(action);
    }
    const menu = await this.locateMenu();
    if (menu) {
      await this.openMenu(menu);
      const menuOption = await this.findMenuOptionByValue(action, menu);
      return this.doMenuAction(menuOption, menu);
    }
    throw Error('No Context menu configured for this host component.');
  }
  /*
   * Scrolls root node into view.
   */
  private async scrollIntoView(): Promise<void> {
    const actions = this.getDriver().actions();
    // selenium-webdriver 3.6.0
    // @ts-ignore
    if (actions.mouseMove) {
      // @ts-ignore
      return actions.mouseMove(this._root).perform();
    } else if (actions.move) {
      return actions.move({ origin: this._root }).perform();
    }
  }
  /*
   * Finds out if a swipe action menu exists.
   */
  private async doesSwipeActionExist(): Promise<boolean> {
    const swipeActions = await this._root.findElements(By.css('OJ-SWIPE-ACTIONS'));
    return swipeActions.length > 0;
  }
  /*
   * Returns the swipe action menu for root node.
   */
  private async getSwipeAction(): Promise<OjSwipeActions> {
    return ojSwipeActions(this._root, By.css('OJ-SWIPE-ACTIONS'));
  }
  /*
   * Locates and returns a menu if it exists.
   */
  private async locateMenu(): Promise<OjMenu | undefined> {
    const doesClosedMenuExist = await this.doesClosedMenuExist();
    if (doesClosedMenuExist && this._parent) {
      return ojMenu(this._parent, By.css('oj-menu[slot=contextMenu]'));
    } else {
      const doesOpenMenuExist = await this.doesOpenMenuExist();
      if (doesOpenMenuExist) {
        const menuId = await this.getOpenMenuId();
        if (menuId) {
          const menu = await ojMenu(this.getDriver(), By.id(menuId));
          await this.closeMenu(menu);
          return Promise.resolve(menu);
        }
      }
      return Promise.resolve(undefined);
    }
  }
  /*
   * Closes menu programmatically.
   */
  private async closeMenu(menu: OjMenu): Promise<void> {
    return this.getDriver().executeScript<void>(
      `
     const menu = arguments[0];
     menu.close();
  `,
      menu
    );
  }
  /*
   * Finds out if a closed menu exists.
   */
  private async doesClosedMenuExist(): Promise<boolean> {
    if (this._parent) {
      const menus = await this._parent.findElements(By.css('oj-menu[slot=contextMenu]'));
      return menus.length > 0;
    }
    return Promise.resolve(false);
  }
  /*
   * Gets menu ID from the surrogate.
   */
  private async getOpenMenuId(): Promise<string | undefined> {
    if (this._parent) {
      const surrogate = await this._parent.findElement(By.css('OJ-SURROGATE'));
      const surrogateId = await surrogate.getAttribute('id');
      const lastIndex = surrogateId.lastIndexOf('_layer_surrogate');
      return surrogateId.substring(0, lastIndex);
    }
    return Promise.resolve(undefined);
  }
  /*
   * Finds out if an open menu exists.
   */
  private async doesOpenMenuExist(): Promise<boolean> {
    if (this._parent) {
      const surrogates = await this._parent.findElements(By.css('OJ-SURROGATE'));
      return surrogates.length > 0;
    }
    return Promise.resolve(false);
  }
  /*
   * Opens menu programmatically.
   */
  private async openMenu(menu: WebElement): Promise<void> {
    await this.giveRootFocus();
    return this.getDriver().executeScript<void>(
      `
      const menu = arguments[0];
      const launcher = arguments[1];
      const clickEvent = new CustomEvent('click');
      launcher.dispatchEvent(clickEvent);
      menu.open(clickEvent, { launcher: launcher })
    `,
      menu,
      this._root
    );
  }
  /*
   * Give the root node focus by clicking or tapping.
   */
  private async giveRootFocus() {
    await this.getDriver().executeScript(
      `
    const root = arguments[0];
    root.click();
    `,
      this._root
    );
  }
  /*
   * Finds a given menu option given its value.
   */
  private async findMenuOptionByValue(targetValue: string, menu: OjMenu) {
    const options = await menu.findElements(By.css('oj-option'));
    for (let i = 0; i < options.length; i++) {
      const value = await options[i].getAttribute('value');
      if (value === targetValue) {
        return options[i];
      }
    }
    throw Error('No option associated with value: ' + targetValue);
  }
  /*
   * Executes menu action given a menu and the item to be executed.
   */
  private async doMenuAction(item: WebElement, menu: OjMenu) {
    const a = await item.findElement(By.css('a')); // get the item anchor which should be active
    const aId = await a.getAttribute('id');
    let activeAId = await menu.getAttribute('aria-activedescendant');
    while (activeAId !== aId) {
      await menu.sendKeys(Key.DOWN);
      activeAId = await menu.getAttribute('aria-activedescendant');
    }
    await item.click();
    await menu.whenBusyContextReady();
  }
}
