import select from 'select';

/**
 * Inner class which performs selection from either `text` or `target`
 * properties and then executes copy or cut operations.
 */
class ClipboardAction {
  /**
   * @param {Object} options
   */
  constructor(options) {
    this.resolveOptions(options);
    this.initSelection();
  }

  /**
   * Defines base properties passed from constructor.
   * @param {Object} options
   */
  resolveOptions(options = {}) {
    this.action = options.action;
    this.container = options.container;
    this.emitter = options.emitter;
    this.target = options.target;
    this.text = options.text;
    this.trigger = options.trigger;

    this.selectedText = '';
  }

  /**
   * Decides which selection strategy is going to be applied based
   * on the existence of `text` and `target` properties.
   */
  initSelection() {
    if (this.text) {
      this.selectFake();
    } else if (this.target) {
      this.selectTarget();
    }
  }

  /**
   * Creates a fake textarea element, sets its value from `text` property,
   */
  createFakeElement() {
    const isRTL = document.documentElement.getAttribute('dir') === 'rtl';

    this.fakeElem = document.createElement('textarea');
    // Prevent zooming on iOS
    this.fakeElem.style.fontSize = '12pt';
    // Reset box model
    this.fakeElem.style.border = '0';
    this.fakeElem.style.padding = '0';
    this.fakeElem.style.margin = '0';
    // Move element out of screen horizontally
    this.fakeElem.style.position = 'absolute';
    this.fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px';
    // Move element to the same position vertically
    let yPosition = window.pageYOffset || document.documentElement.scrollTop;
    this.fakeElem.style.top = `${yPosition}px`;

    this.fakeElem.setAttribute('readonly', '');
    this.fakeElem.value = this.text;

    return this.fakeElem;
  }

  /**
   * Get's the value of fakeElem,
   * and makes a selection on it.
   */
  selectFake() {
    const fakeElem = this.createFakeElement();

    this.fakeHandlerCallback = () => this.removeFake();

    this.fakeHandler =
      this.container.addEventListener('click', this.fakeHandlerCallback) ||
      true;

    this.container.appendChild(fakeElem);

    this.selectedText = select(fakeElem);

    this.copyText();

    this.removeFake();
  }

  /**
   * Only removes the fake element after another click event, that way
   * a user can hit `Ctrl+C` to copy because selection still exists.
   */
  removeFake() {
    if (this.fakeHandler) {
      this.container.removeEventListener('click', this.fakeHandlerCallback);
      this.fakeHandler = null;
      this.fakeHandlerCallback = null;
    }

    if (this.fakeElem) {
      this.container.removeChild(this.fakeElem);
      this.fakeElem = null;
    }
  }

  /**
   * Selects the content from element passed on `target` property.
   */
  selectTarget() {
    this.selectedText = select(this.target);
    this.copyText();
  }

  /**
   * Executes the copy operation based on the current selection.
   */
  copyText() {
    let succeeded;

    try {
      succeeded = document.execCommand(this.action);
    } catch (err) {
      succeeded = false;
    }

    this.handleResult(succeeded);
  }

  /**
   * Fires an event based on the copy operation result.
   * @param {Boolean} succeeded
   */
  handleResult(succeeded) {
    this.emitter.emit(succeeded ? 'success' : 'error', {
      action: this.action,
      text: this.selectedText,
      trigger: this.trigger,
      clearSelection: this.clearSelection.bind(this),
    });
  }

  /**
   * Moves focus away from `target` and back to the trigger, removes current selection.
   */
  clearSelection() {
    if (this.trigger) {
      this.trigger.focus();
    }
    document.activeElement.blur();
    window.getSelection().removeAllRanges();
  }

  /**
   * Sets the `action` to be performed which can be either 'copy' or 'cut'.
   * @param {String} action
   */
  set action(action = 'copy') {
    this._action = action;

    if (this._action !== 'copy' && this._action !== 'cut') {
      throw new Error('Invalid "action" value, use either "copy" or "cut"');
    }
  }

  /**
   * Gets the `action` property.
   * @return {String}
   */
  get action() {
    return this._action;
  }

  /**
   * Sets the `target` property using an element
   * that will be have its content copied.
   * @param {Element} target
   */
  set target(target) {
    if (target !== undefined) {
      if (target && typeof target === 'object' && target.nodeType === 1) {
        if (this.action === 'copy' && target.hasAttribute('disabled')) {
          throw new Error(
            'Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute'
          );
        }

        if (
          this.action === 'cut' &&
          (target.hasAttribute('readonly') || target.hasAttribute('disabled'))
        ) {
          throw new Error(
            'Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes'
          );
        }

        this._target = target;
      } else {
        throw new Error('Invalid "target" value, use a valid Element');
      }
    }
  }

  /**
   * Gets the `target` property.
   * @return {String|HTMLElement}
   */
  get target() {
    return this._target;
  }

  /**
   * Destroy lifecycle.
   */
  destroy() {
    this.removeFake();
  }
}

export default ClipboardAction;
