import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import Component from '@glimmer/component';
import arg from 'ember-bootstrap/utils/decorators/arg';
import deprecateSubclassing from 'ember-bootstrap/utils/deprecate-subclassing';

/**
  Implements a HTML button element, with support for all [Bootstrap button CSS styles](http://getbootstrap.com/css/#buttons)
  as well as advanced functionality such as button states.

  ### Basic Usage

  ```hbs
  <BsButton @type="primary" @icon="glyphicon glyphicon-download">
    Downloads
  </BsButton>
  ```

  ### Actions

  Use the `onClick` property of the component to send an action to your controller. It will receive the button's value
  (see the `value` property) as an argument.

  ```hbs
  <BsButton @type="primary" @icon="glyphicon glyphicon-download" @onClick=(action "download")>
    Downloads
  </BsButton>
  ```

  ### Promise support for automatic state change

  When returning a Promise for any asynchronous operation from the `onClick` closure action the button will
  manage an internal state ("default" > "pending" > "fulfilled"/"rejected") automatically.

  The button is disabled by default if it's in pending state. You could override this behavior by passing
  the `disabled` HTML attribute or by setting `@preventConcurrency` to false.

  ```hbs
  <BsButton
    disabled={{false}}
  />
  ```

  ```hbs
  <BsButton
    @preventConcurrency={{false}}
  />
  ```

  The label could be changed automatically according to the state of the promise with `@defaultText`,
  `@pendingText`, `@fulfilledText` and `@rejectedText` arguments:

  ```hbs
  <BsButton
    @type="primary"
    @icon="glyphicon glyphicon-download"
    @defaultText="Download"
    @pendingText="Loading..."
    @fulfilledText="Completed!"
    @rejectedText="Oups!?"
    @onClick={{this.download}}
  />
  ```

  ```js
  // controller.js
  import { Controller } from '@ember/controller';
  import { action } from '@ember/object';

  export default class MyController extends Controller {
    @action
    download(value) {
      return new Promise(...);
    }
  });
  ```

  For further customization `isPending`, `isFulfilled`, `isRejected` and `isSettled` properties are yielded:

  ```hbs
  <BsButton @onClick=(action "download") as |button|>
    Download
    {{#if button.isPending}}
      <span class="loading-spinner"></span>
    {{/if}}
  </BsButton>
  ```

  You can `reset` the state represented by these properties and used for button's text by setting `reset` property to
  `true`.

  *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 Button
  @namespace Components
  @extends Glimmer.Component
  @public
*/
@deprecateSubclassing
export default class Button extends Component {
  /**
   * Default label of the button. Not need if used as a block component
   *
   * @property defaultText
   * @type string
   * @public
   */

  /**
   * Label of the button used if `onClick` event has returned a Promise which is pending.
   * Not considered if used as a block component.
   *
   * @property pendingText
   * @type string
   * @public
   */

  /**
   * Label of the button used if `onClick` event has returned a Promise which succeeded.
   * Not considered if used as a block component.
   *
   * @property fulfilledText
   * @type string
   * @public
   */

  /**
   * Label of the button used if `onClick` event has returned a Promise which failed.
   * Not considered if used as a block component.
   *
   * @property rejectedText
   * @type string
   * @public
   */

  /**
   * Property to disable the button only used in internal communication
   * between Ember Boostrap components.
   *
   * @property _disabled
   * @type ?boolean
   * @default null
   * @private
   */

  get __disabled() {
    if (this.args._disabled !== undefined) {
      return this.args._disabled;
    }

    return this.isPending && this.args.preventConcurrency !== false;
  }

  /**
   * Set the type of the button, either 'button' or 'submit'
   *
   * @property buttonType
   * @type String
   * @default 'button'
   * @deprecated
   * @public
   */
  @arg buttonType = 'button';

  /**
   * Set the 'active' class to apply active/pressed CSS styling
   *
   * @property active
   * @type boolean
   * @default false
   * @public
   */

  /**
   * Property for block level buttons (BS3 and BS4 only!)
   *
   * See the [Bootstrap docs](http://getbootstrap.com/css/#buttons-sizes)
   * @property block
   * @type boolean
   * @default false
   * @public
   */
  @arg block = false;

  /**
   * A click event on a button will not bubble up the DOM tree if it has an `onClick` action handler. Set to true to
   * enable the event to bubble
   *
   * @property bubble
   * @type boolean
   * @default false
   * @public
   */

  /**
   * If button is active and this is set, the icon property will match this property
   *
   * @property iconActive
   * @type String
   * @public
   */

  /**
   * If button is inactive and this is set, the icon property will match this property
   *
   * @property iconInactive
   * @type String
   * @public
   */

  /**
   * Class(es) (e.g. glyphicons or font awesome) to use as a button icon
   * This will render a <i class="{{icon}}"></i> element in front of the button's label
   *
   * @property icon
   * @type String
   * @public
   */
  get icon() {
    return this.args.icon || (this.args.active ? this.args.iconActive : this.args.iconInactive);
  }

  /**
   * Supply a value that will be associated with this button. This will be sent
   * as a parameter of the default action triggered when clicking the button
   *
   * @property value
   * @type any
   * @public
   */

  /**
   * Controls if `onClick` action is fired concurrently. If `true` clicking button multiple times will not trigger
   * `onClick` action if a Promise returned by previous click is not settled yet.
   *
   * This does not affect event bubbling.
   *
   * @property preventConcurrency
   * @type Boolean
   * @default true
   * @public
   */

  /**
   * State of the button. The button's label (if not used as a block component) will be set to the
   * `<state>Text` property.
   * This property will automatically be set when using a click action that supplies the callback with a promise.
   * Possible values are: "default" > "pending" > "fulfilled" / "rejected".
   * It could be resetted by `reset` property.
   *
   * @property state
   * @type String
   * @default 'default'
   * @private
   */
  @tracked _state = 'default';
  get state() {
    return this.args.state ?? this._state;
  }
  set state(state) {
    this._state = state;
  }

  /**
   * Promise returned by `onClick` event is pending.
   *
   * @property isPending
   * @type Boolean
   * @private
   */
  get isPending() {
    return this.state === 'pending';
  }

  /**
   * Promise returned by `onClick` event has been succeeded.
   *
   * @property isFulfilled
   * @type Boolean
   * @private
   */
  get isFulfilled() {
    return this.state === 'fulfilled';
  }

  /**
   * Promise returned by `onClick` event has been rejected.
   *
   * @property isRejected
   * @type Boolean
   * @private
   */
  get isRejected() {
    return this.state === 'rejected';
  }

  /**
   * Promise returned by `onClick` event has been succeeded or rejected.
   *
   * @property isSettled
   * @type Boolean
   * @private
   */
  get isSettled() {
    return this.isFulfilled || this.isRejected;
  }

  /**
   * Set this to `true` to reset the `state`. A typical use case is to bind this attribute with ember-data isDirty flag.
   *
   * The old value is not taken into consideration. Setting a `true` value to `true` again will also reset `state`.
   * In that case it's even to notify the observer system that the property has changed by calling
   * [`notifyPropertyChange()`](https://www.emberjs.com/api/ember/3.2/classes/EmberObject/methods/notifyPropertyChange?anchor=notifyPropertyChange).
   *
   * @property reset
   * @type boolean
   * @public
   */

  /**
   * Property for size styling, set to 'lg', 'sm' or 'xs'
   *
   * Also see the [Bootstrap docs](https://getbootstrap.com/docs/4.3/components/buttons/#sizes)
   *
   * @property size
   * @type String
   * @public
   */

  /**
   * Property for type styling
   *
   * For the available types see the [Bootstrap docs](https://getbootstrap.com/docs/4.3/components/buttons/)
   *
   * @property type
   * @type String
   * @default 'secondary'
   * @public
   */

  /**
   * Property to create outline buttons (BS4+ only)
   *
   * @property outline
   * @type boolean
   * @default false
   * @public
   */

  /**
   * When clicking the button this action is called with the value of the button (that is the value of the "value" property).
   *
   * Return a promise object, and the buttons state will automatically set to "pending", "resolved" and/or "rejected".
   * This could be used to automatically set the button's text depending on promise state (`defaultText`, `pendingText`,
   * `fulfilledText`, `rejectedText`) and for further customization using the yielded `isPending`, `isFulfilled`,
   * `isRejected` properties.
   *
   * The click event will not bubble up, unless you set `bubble` to true.
   *
   * @event onClick
   * @param {*} value
   * @public
   */

  /**
   * This will reset the state property to 'default', and with that the button's label to defaultText
   *
   * @method resetState
   * @private
   */
  @action
  resetState() {
    this.state = 'default';
  }

  get text() {
    return this.args[`${this.state}Text`] || this.args.defaultText;
  }

  /**
   * @method click
   * @private
   */
  @action
  async handleClick(e) {
    const { bubble, onClick, preventConcurrency } = this.args;

    if (typeof onClick !== 'function') {
      return;
    }

    // Shouldn't we prevent propagation regardless if `@onClick` is a function?
    if (!bubble) {
      e.stopPropagation();
    }

    if (preventConcurrency && this.isPending) {
      return;
    }

    this.state = 'pending';

    try {
      await onClick(this.args.value);

      if (!this.isDestroyed) {
        this.state = 'fulfilled';
      }
    } catch (error) {
      if (!this.isDestroyed) {
        this.state = 'rejected';
      }

      throw error;
    }
  }

  constructor() {
    super(...arguments);
  }
}