import { tagName } from '@ember-decorators/component';
import { action, computed } from '@ember/object';
import Component from '@ember/component';
import defaultValue from 'ember-bootstrap/utils/default-decorator';
import { assert } from '@ember/debug';
import deprecateSubclassing from 'ember-bootstrap/utils/deprecate-subclassing';

const ESCAPE_KEYCODE = 27; // KeyboardEvent.which value for Escape (Esc) key
const SPACE_KEYCODE = 32; // KeyboardEvent.which value for space key
const TAB_KEYCODE = 9; // KeyboardEvent.which value for tab key
const ARROW_UP_KEYCODE = 38; // KeyboardEvent.which value for up arrow key
const ARROW_DOWN_KEYCODE = 40; // KeyboardEvent.which value for down arrow key

const SUPPORTED_KEYCODES = [ESCAPE_KEYCODE, ARROW_DOWN_KEYCODE, ARROW_UP_KEYCODE];

/**
  Bootstrap style [dropdown menus](http://getbootstrap.com/components/#dropdowns), consisting
  of a toggle element, and the dropdown menu itself.

  ### Usage

  Use this component together with the yielded contextual components:
  * [Components.DropdownToggle](Components.DropdownToggle.html)
  * [Components.DropdownButton](Components.DropdownButton.html)
  * [Components.DropdownMenu](Components.DropdownMenu.html)
    * [Components.DropdownMenuItem](Components.DropdownMenuItem.html)
    * [Components.DropdownMenuDivider](Components.DropdownMenuDivider.html)
    * [Components.DropdownMenuLinkTo](Components.DropdownMenuLinkTo.html)

  Furthermore references to the following actions are yielded:

  * `toggleDropdown`
  * `openDropdown`
  * `closeDropdown`

  ```hbs
  <BsDropdown as |dd|>
    <dd.toggle>Dropdown <span class="caret"></span></dd.toggle>
    <dd.menu as |ddm|>
      <ddm.item>
        <ddm.linkTo @route="index">Something</ddm.linkTo>
      </ddm.item>
      <ddm.item>
        <ddm.linkTo @route="index">Something different</ddm.linkTo>
      </ddm.item>
    </dd.menu>
  </BsDropdown>
  ```

  If you need to use dropdowns in a [nav](Components.Nav.html), use the `bs-nav.dropdown`
  contextual component rather than a standalone dropdown to ensure the correct styling
  regardless of your Bootstrap version.

  > Note: the use of angle brackets `<ddm.linkTo>` as shown above is only supported for Ember >= 3.10, as it relies on its
  > Ember's native implementation of the [`LinkComponent`](https://api.emberjs.com/ember/3.12/classes/Ember.Templates.helpers/methods/link-to?anchor=link-to).
  > For older Ember versions please use the legacy syntax with positional arguments:
  > `{{#ddm.link-to "bar" this.model}}Bar{{/ddm.link-to}}`

  ### Button dropdowns

  To use a button as the dropdown toggle element (see http://getbootstrap.com/components/#btn-dropdowns), use the
  `Components.DropdownButton` component as the toggle:

  ```hbs
  <BsDropdown as |dd|>
    <dd.button>Dropdown <span class="caret"></span></dd.button>
    <dd.menu as |ddm|>
      <ddm.item>
        <ddm.linkTo @route="index">Something</ddm.linkTo>
      </ddm.item>
      <ddm.item>
        <ddm.linkTo @route="index">Something different</ddm.linkTo>
      </ddm.item>
    </dd.menu>
  </BsDropdown>
  ```

  It has all the functionality of a `Components.Button` with additional dropdown support.

  ### Split button dropdowns

  To have a regular button with a dropdown button as in http://getbootstrap.com/components/#btn-dropdowns-split, use a
  `Components.Button` component and a `Components.DropdownButton`:

  ```hbs
  <BsDropdown as |dd|>
    <BsButton>Dropdown</BsButton>
    <dd.button>Dropdown <span class="caret"></span></dd.button>
    <dd.menu as |ddm|>
      <ddm.item>
        <ddm.linkTo @route="index">Something</ddm.linkTo>
      </ddm.item>
      <ddm.item>
        <ddm.linkTo @route="index">Something different</ddm.linkTo>
      </ddm.item>
    </dd.menu>
  </BsDropdown>
  ```

  ### Dropup style

  Set the `direction` property to "up" to switch to a "dropup" style:

  ```hbs
  <BsDropdown @direction="up" as |dd|>
    ...
  </BsDropdown>
  ```

  ### Open, close or toggle the dropdown programmatically

  If you wanted to control when the dropdown opens and closes programmatically, the `bs-dropdown` component yields the
  `openDropdown`, `closeDropdown` and `toggleDropdown` actions which you can then pass to your own handlers. For example:

  ```hbs
  <BsDropdown @closeOnMenuClick={{false}} as |dd|>
    <BsButton>Dropdown</BsButton>
    <dd.button>Dropdown <span class="caret"></span></dd.button>
    <dd.menu as |ddm|>
      {{#each this.items as |item|}}
        <ddm.item>
          <a href onclick={{action "changeItems" item dd.closeDropdown}}>
            {{item.text}}
          </a>
        </ddm.item>
      {{/each}}
    </dd.menu>
  </BsDropdown>
  ```

  Then in your controller or component, optionally close the dropdown:

  ```js
  ...
  actions: {
    handleDropdownClicked(item, closeDropdown) {
      if(item.isTheRightOne) {
        this.chosenItems.pushObject(item);
        closeDropdown();
      } else {
        this.set('item', this.getRandomItems());
      }
    },
  }
  ```


  ### Bootstrap 3/4 Notes

  If you need to use dropdowns in a [nav](Components.Nav.html), use the `bs-nav.dropdown`
  contextual component rather than a standalone dropdown to ensure the correct styling
  regardless of your Bootstrap version.

  If you use the [dropdown divider](Components.DropdownMenuDivider), you don't have to worry
  about differences in the markup between versions.

  Be sure to use the [dropdown menu link-to](Component.DropdownMenuLinkTo), for in-application
  links as dropdown menu items. This is essential for proper styling regardless of Bootstrap
  version and will also provide automatic `active` highlighting on dropdown menu items. If you
  wish to have a dropdown menu item refer to an external link, be sure to apply the `dropdown-item`
  class to the `<a>` tag for Bootstrap 4 compatibility.

  The dropdown menu will be positioned using the `popper.js` library, just as the original Bootstrap
  version does. This also allows you to set `renderInPlace=false` on the menu component to render it in a wormhole,
  which you might want to do if you experience clipping issues by an outer `overflow: hidden` element.

  *Note that only invoking the component in a template as shown above is considered part of its public API. Extending from it (subclassing) is generally not supported, and may break at any time.*

  @class Dropdown
  @namespace Components
  @extends Ember.Component
  @public
s*/
@tagName('')
@deprecateSubclassing
export default class Dropdown extends Component {
  /**
   * The tag name used for the dropdown element.
   *
   * @property htmlTag
   * @default 'div'
   * @type {string}
   * @public
   */
  @defaultValue
  htmlTag = 'div';

  /**
   * This property reflects the state of the dropdown, whether it is open or closed.
   *
   * @property isOpen
   * @default false
   * @type boolean
   * @private
   */
  @defaultValue
  isOpen = false;

  /**
   * By default, clicking on an open dropdown menu will close it. Set this property to false for the menu to stay open.
   *
   * @property closeOnMenuClick
   * @default true
   * @type boolean
   * @public
   */
  @defaultValue
  closeOnMenuClick = true;

  /**
   * By default, the dropdown menu will expand downwards. Other options include, 'up', 'left' and 'right'
   *
   * @property direction
   * @type string
   * @default 'down'
   * @public
   */
  @defaultValue
  direction = 'down';

  /**
   * Indicates the dropdown is being used as a navigation item dropdown.
   *
   * @property inNav
   * @type boolean
   * @default false
   * @private
   */

  /**
   * A computed property to generate the suiting class for the dropdown container, either "dropdown", "dropup" or "btn-group".
   * BS4 only: "dropleft", "dropright"
   *
   * @property containerClass
   * @type string
   * @readonly
   * @private
   */
  @computed('direction', 'hasButton', 'toggleElement.classList')
  get containerClass() {
    if (this.hasButton && !this.toggleElement.classList.contains('btn-block')) {
      return this.direction !== 'down' ? `btn-group drop${this.direction}` : 'btn-group';
    } else {
      return `drop${this.direction}`;
    }
  }

  get hasButton() {
    return this.toggleElement && this.toggleElement.tagName === 'BUTTON';
  }

  /**
   * @property toggleElement
   * @private
   */
  toggleElement = null;

  /**
   * The DOM element of the `.dropdown-menu` element
   * @type object
   * @readonly
   * @private
   */
  menuElement = null;

  /**
   * Action is called when dropdown is about to be shown
   *
   * @event onShow
   * @param {*} value
   * @public
   */
  onShow(value) {} // eslint-disable-line no-unused-vars

  /**
   * Action is called when dropdown is about to be hidden
   * Returning `false` will block closing the dropdown
   *
   * @event onHide
   * @param {*} value
   * @public
   */
  onHide(value) {} // eslint-disable-line no-unused-vars

  @action
  toggleDropdown() {
    if (this.isOpen) {
      this.closeDropdown();
    } else {
      this.openDropdown();
    }
  }

  @action
  openDropdown() {
    this.set('isOpen', true);
    this.onShow();
  }

  @action
  closeDropdown() {
    if (this.onHide() === false) return;

    this.set('isOpen', false);
  }

  /**
   * Handler for click events to close the dropdown
   *
   * @method closeOnClickHandler
   * @param e
   * @protected
   */
  @action
  closeHandler(e) {
    let { target } = e;
    let { toggleElement, menuElement } = this;

    if (
      !this.isDestroyed &&
      ((e.type === 'keyup' && e.which === TAB_KEYCODE && menuElement && !menuElement.contains(target)) ||
        (e.type === 'click' &&
          toggleElement &&
          !toggleElement.contains(target) &&
          ((menuElement && !menuElement.contains(target)) || this.closeOnMenuClick)))
    ) {
      this.closeDropdown();
    }
  }

  @action
  handleKeyEvent(event) {
    // If not input/textarea:
    //  - And not a key in REGEXP_KEYDOWN => not a dropdown command
    // If input/textarea:
    //  - If space key => not a dropdown command
    //  - If key is other than escape
    //    - If key is not up or down => not a dropdown command
    //    - If trigger inside the menu => not a dropdown command
    if (
      ['input', 'textarea'].includes(event.target.tagName.toLowerCase())
        ? event.which === SPACE_KEYCODE ||
          (event.which !== ESCAPE_KEYCODE &&
            ((event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE) ||
              this.menuElement.contains(event.target)))
        : !SUPPORTED_KEYCODES.includes(event.which)
    ) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();

    if (!this.isOpen) {
      this.openDropdown();
      return;
    } else if (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE) {
      this.closeDropdown();
      this.toggleElement.focus();
      return;
    }

    let items = [].slice.call(this.menuElement.querySelectorAll('.dropdown-item:not(.disabled):not(:disabled)'));

    if (items.length === 0) {
      return;
    }

    let index = items.indexOf(event.target);

    if (event.which === ARROW_UP_KEYCODE && index > 0) {
      // Up
      index--;
    }

    if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) {
      // Down
      index++;
    }

    if (index < 0) {
      index = 0;
    }

    items[index].focus();
  }

  @action
  registerChildElement(element, [type]) {
    assert(`Unknown child element type "${type}"`, type === 'toggle' || type === 'menu');
    assert(`Registered ${type} element must be an HTMLElement`, element instanceof HTMLElement);

    this.set(`${type}Element`, element);
  }

  @action
  unregisterChildElement(element, [type]) {
    assert(`Unknown child element type "${type}"`, type === 'toggle' || type === 'menu');

    this.set(`${type}Element`, null);
  }

  /**
   * @property buttonComponent
   * @type {String}
   * @private
   */

  /**
   * @property toggleComponent
   * @type {String}
   * @private
   */

  /**
   * @property menuComponent
   * @type {String}
   * @private
   */
}