<template>
  <div
    :class="[
      { 'with-suffix-icon' : suffixClickable },
      { 'with-text-addition' : validationType !== 'string' },
    ]"
  >
    <TfTooltip
      v-if="tooltip"
      :options="{
        placement: 'top',
      }"
      :force-show="showTooltip"
      trigger="click"
    >
      <!-- we need a blank element to act as a reference -->
      <div slot="reference" />
      <div
        class="tooltip-inner"
        :style="`width: ${popoverWidth - 22}px`"
        v-text="tooltip"
      />
    </TfTooltip>

    <span
      v-if="prependValidationType"
      class="prepend-input text-addition"
      v-text="validationTypeText"
    />
    <!-- eslint-disable max-len -->
    <!-- To do: We should replace this with the base <popper /> when we can.-->
    <ElPopover
      v-if="options.length"
      v-model="showAutocomplete"
      v-bind="{
        disabled: disabled || saving,
        width: popoverWidth,
        popperOptions: {
          boundariesElement: 'body',
          gpuAcceleration: true,
          positionFixed: false,
          preventOverflow: false
        },
        popperClass: 'el-select-dropdown autocomplete',
      }"
      placement="bottom-start"
      trigger="manual"
    >
      <div
        ref="popoverContainer"
        class="el-select-dropdown__wrap el-scrollbar__wrap el-scrollbar__wrap--hidden-default"
      >
        <ul
          class="el-scrollbar__view el-select-dropdown__list"
          data-test="dropdown list"
        >
          <li
            v-for="(option, index) in filteredOptions"
            :key="index"
            :data-test="`select ${option[optionKey]}`"
            class="el-select-dropdown__item"
            :class="{ 'is-disabled': option.isDisabled }"
            tabindex="0"
            @click="autoCompleteClick(option[optionKey], option.isDisabled)"
            @keydown.enter.prevent="autoCompleteClick(option[optionKey], option.isDisabled)"
            @keydown.up="handleListItemArrowUp"
            @keydown.down="handleListItemArrowDown"
            @keydown.tab="autoCompleteClick(option[optionKey])"
            @keydown.esc="handleEscKey"
            v-text="option[optionLabel] || option[optionKey]"
          />
          <template v-if="groupedDisabledOptions.length">
            <li
              class="el-select-group__title"
              v-text="groupedDisabledOptionsTitle"
            />
            <li
              v-for="(option, index) in groupedDisabledOptions"
              :key="`disabled-${index}`"
              class="el-select-dropdown__item is-disabled"
              v-text="option[optionLabel] || option[optionKey]"
            />
          </template>
        </ul>
      </div>
      <ElInput
        slot="reference"
        ref="auto-complete-textarea"
        v-model="selectedValue"
        :data-test="`edit ${prop}`"
        :class="autoCollapse ? 'auto-collapse' : ''"
        :disabled="disabled || saving"
        :placeholder="placeholder"
        class="auto-complete-textarea"
        autosize
        size="mini"
        type="textarea"
        @keydown.native="onKeydown"
        @keydown.native.down="handleTextAreaArrowDown"
        @click.native="onInputClick()"
        @focus="onInputFocus"
        @blur="onInputBlur"
      />
    </ElPopover>
    <ElInput
      v-else
      v-model="selectedValue"
      v-bind="{
        disabled: disabled || saving,
        placeholder,
        prop,
      }"
      :class="{ 'auto-collapse' : autoCollapse }"
      :data-test="`edit ${prop}`"
      class="auto-complete-textarea"
      autosize
      size="mini"
      type="textarea"
      @keydown.native="onKeydown"
      @focus="onInputFocus"
      @blur="onInputBlur"
    />
    <span
      v-if="appendValidationType"
      class="append-input text-addition"
      v-text="validationTypeText"
    />
    <TfButton
      v-if="suffixClickable"
      :disabled="disabled"
      :icon="suffixClickable"
      :loading="saving"
      type="secondary"
      size="icon-no-fill"
      :class="[
        { 'is-focused' : showAutocomplete, },
        'button-suffix'
      ]"
      @click="$emit('suffixClick')"
    />
  </div>
</template>

<script>
  import { debounce } from 'lodash';

  export default {
    props: {
      allowEnter: {
        type: Boolean,
        default: true,
      },
      appendValidationType: {
        type: Boolean,
        default: false,
      },
      autoCollapse: {
        type: Boolean,
        default: true,
      },
      disabled: {
        type: Boolean,
        default: false,
      },
      /**
       * "groupedDisabledOptions" provides a list of disabled options that appear below the options in the dropdown.
       */
      groupedDisabledOptions: {
        type: Array,
        default: () => [],
      },
      /**
       * "groupedDisabledOptionsTitle" provides a title for the list of disabled options.
       */
      groupedDisabledOptionsTitle: {
        type: String,
        default: 'Used values',
      },
      optionKey: {
        type: String,
        default: 'normalized_value',
      },
      // Pass this if any objects in the options array has a label that doesn't match the value
      // Example: { normalized_value: '10%', normalized_label: 'Match in-force'}
      optionLabel: {
        type: String,
        default: 'normalized_label',
      },
      /* An array of objects. Each object contains a single option and may optionally contain an
      isDisabled prop that will determine if a specific dropdown item is disabled */
      options: {
        type: Array,
        default: () => [],
      },
      placeholder: {
        type: String,
        default: 'Enter value',
      },
      prependValidationType: {
        type: Boolean,
        default: false,
      },
      prop: {
        type: String,
        default: '',
      },
      saving: {
        type: Boolean,
        default: false,
      },
      suffixClickable: {
        type: String,
        default: '',
      },
      tooltip: {
        type: String,
        default: null,
      },
      validationType: {
        type: String,
        default: 'string',
      },
      value: {
        type: String,
        default: '',
      },
      width: {
        type: [Number, String],
        default: 'auto',
      },
    },
    data: (vm) => ({
      debounceChangeEvent: debounce(vm.sendChangeEvent, 500),
      selectedValue: '',
      showAutocomplete: false,
      showTooltip: false,
      initialValueSet: false,
      autoWidth: null,
      userEdited: false,
      hasFocus: false,
    }),
    computed: {
      /**
       * get the correct width for the popover
       *
       * @returns {string}
       */
      popoverWidth() {
        return this.width === 'auto'
          ? this.autoWidth
          : this.width;
      },

      /**
       * get the correct text for the prepend/append divs
       *
       * @returns {string}
       */
      validationTypeText() {
        let text;

        if (this.validationType === 'dollar') {
          text = '$';
        } else if (this.validationType === 'percentage-whole-number') {
          text = '%';
        }

        return text;
      },

      /**
       * If the user has edited the value of the input,
       * filter options based on what was entered.
       * There is a potential edge case where `options` MIGHT
       * be passed as `null` so we're covering that just in case.
       *
       * @returns {Array}
       */
      filteredOptions() {
        const options = this.options || [];

        // user hasn't edited, there's no value—show all options, or user has hit "enter" w/o a value.
        if (!this.userEdited || !this.selectedValue) {
          return options;
        }

        return options.filter((option) => {
          /*
            Not all objects in the options array need to have a label
            so we filter by optionLabel first and use the optionKeys as the backup testValue.
          */
          const testValue = option[this.optionLabel] || option[this.optionKey];

          return testValue.toLowerCase().includes(this.selectedValue.toLowerCase());
        });
      },
    },
    watch: {
      /**
       * Everytime the value changes we need parse the value based on its validation type and update the selectedValue.
       */
      value() {
        this.selectedValue = this.parseValue(this.value);
      },

      /**
       * Emit the change up when the selectedValue changes
       */
      selectedValue() {
        this.$emit('input', this.selectedValue);

        if (this.initialValueSet) {
          this.debounceChangeEvent();
        }

        this.initialValueSet = true;
      },

      /**
       * Set the correct auto width for the popover when it is shown
       *
       * @param {boolean} showAutocomplete
       */
      showAutocomplete(showAutocomplete) {
        if (showAutocomplete && this.width === 'auto') {
          this.autoWidth = this.$el.clientWidth;
        }
      },

      /**
       * Show/hide the autocomplete popover if there are options/no options and the user has edited the input
       * (overrides existing behavior only for the userEdited autocomplete behavior)
       *
       * @param {Array} newOptions
       */
      filteredOptions(newOptions) {
        if (this.userEdited && this.hasFocus) {
          this.showAutocomplete = !!newOptions.length;
        }
      },
    },

    created() {
      if (this.value) {
        this.selectedValue = this.parseValue(this.value);
      } else {
        this.initialValueSet = true;
      }
    },

    methods: {
      /**
       * Fires When a user clicks on a list item in the popover we hide
       * We updated the 'selectedValue' as well as close the dropdown and focus on the textarea
       *
       * @param {string} value
       * @param {boolean} [isDisabled] - If the item is disabled, do nothing when it is clicked or the enter key is hit
       */
      autoCompleteClick(value, isDisabled) {
        if (isDisabled) {
          return;
        }

        this.selectedValue = this.parseValue(value);

        this.$nextTick(() => {
          this.$el.querySelector('textarea').focus();
          this.showAutocomplete = false;
        });
      },

      /**
       * Handle keydown events
       *
       * @param {object} event
       */
      onKeydown(event) {
        const charCode = event.which ? event.which : event.keyCode;
        // Conditions
        const isNumpad = charCode >= 96 && charCode <= 105;
        const isTopNumbers = charCode >= 48 && charCode <= 57;
        const isNumericKey = isNumpad || isTopNumbers;

        // Allow for 'selecting all', 'copy', and 'paste'
        const isCopyPaste = event.metaKey
          && ['a', 'c', 'v'].includes(event.key);
        const isModifier = event.shiftKey;
        const isArrowKey = [37, 38, 39, 40].includes(charCode);

        if (!isArrowKey) {
          // at this point, we're saying that the user has edited the field, because they pushed a key
          this.userEdited = true;
        }

        // prevent keydown and emit to the parent if key is Enter and !allowEnter
        if (!this.allowEnter && charCode === 13) {
          event.preventDefault();
          this.$emit('keydownEnter');
        }

        // Allow if user is copy/pasting
        const numericValidationTypes = [
          'percentage-whole-number',
          'whole-number',
        ];
        if (!isCopyPaste) {
          if (numericValidationTypes.includes(this.validationType) && charCode > 31) {
            if (!isNumericKey || isModifier) {
              event.preventDefault();
            }
          }
        }
      },

      /**
       * When a user clicks in the text area we open the dropdown
       */
      onInputClick() {
        this.showAutocomplete = true;
      },
      /**
       * When the text area gets focus we open the dropdown
       */
      onInputFocus() {
        this.$emit('focus');
        // reset if the user left
        this.userEdited = false;

        if (this.options.length) {
          this.showAutocomplete = true;
        }
        this.hasFocus = true;
        if (this.tooltip) {
          this.showTooltip = true;
        }
        const self = this;
        window.setTimeout(() => {
          const autocomplete = self.$el.querySelector('.auto-complete-textarea');
          autocomplete.removeAttribute('tabindex');
        }, 100);
      },
      /**
       * When the text area loses we check if the focus is in the dropdown, if it's not we close the dropdown
       *
       * @param {object} event
       */
      onInputBlur(event) {
        if (!event) {
          return;
        }

        this.hasFocus = false;

        const { relatedTarget } = event;
        const popover = this.$refs.popoverContainer;
        const isInPopover = this.isChildOf(relatedTarget, popover);

        if (!isInPopover) {
          if (this.tooltip) {
            this.showTooltip = false;
          }

          this.showAutocomplete = false;
          this.$emit('blur', event);
        }

        this.debounceChangeEvent.flush();
      },

      /**
       * If the user presses down while in the text area we focus on the first list item
       */
      handleTextAreaArrowDown() {
        if (this.filteredOptions.length) {
          this.$refs.popoverContainer.querySelector('li:first-child').focus();
        }
      },

      /**
       * If the user presses up on a list item we focus on the one above it (or on the text area)
       *
       * @param {object} event
       */
      handleListItemArrowUp(event) {
        event.preventDefault();
        const { previousSibling } = event.target;

        if (previousSibling) {
          previousSibling.focus();
        } else {
          this.$el.querySelector('textarea').focus();
        }
      },

      /**
       * If the user presses down on a list item we focus on the next item
       *
       * @param {object} event
       */
      handleListItemArrowDown(event) {
        event.preventDefault();
        const { nextSibling } = event.target;

        if (nextSibling) {
          nextSibling.focus();
        }
      },

      /**
       * If the user presses esc, we close the dropdown and focus on the textarea
       */
      handleEscKey() {
        this.showAutocomplete = false;
        this.$el.querySelector('textarea').focus();
      },

      /**
       * Parse the passed value based on what the validation type is.
       *
       * @param {string} value
       * @returns {string}
       */
      parseValue(value) {
        let parsedValue;

        switch (this.validationType) {
        case 'percentage-whole-number':
          parsedValue = value.replace('%', '');
          break;
        default:
          parsedValue = value;
        }
        return parsedValue;
      },

      /**
       * Let's emit a change event up when the value changes
       */
      sendChangeEvent() {
        let parsedValue;

        switch (this.validationType) {
        case 'percentage-whole-number':
          parsedValue = `${this.selectedValue}%`;
          break;
        default:
          parsedValue = this.selectedValue;
        }

        this.$emit('change', parsedValue);
      },

      /**
       * Is the first element a child of the second
       * We use to decide to if the user is still in the dropdown
       *
       * @param {object} child
       * @param {object} parent
       * @returns {boolean}
       */
      isChildOf(child, parent) {
        if (!child) {
          return false;
        }
        if (child.parentNode === parent) {
          return true;
        } if (child.parentNode === null) {
          return false;
        }
        return this.isChildOf(child.parentNode, parent);
      },
    },
  };
</script>

<style lang="scss" scoped>
  @import 'src/assets/styles/variables';

  .tooltip-inner {
    text-align: left;
  }

  .button-suffix.tf-button--icon-no-fill.has-icon,
  .button-suffix.wt-button--icon-no-fill.has-icon {
    position: absolute;
    top: 0;
    right: 5px;
    bottom: 0;
    padding: 0 5px;
    color: $tf-medium-gray;

    svg {
      fill: $tf-light-gray;
    }
  }

  .text-addition {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 8px;
    color: $tf-medium-gray;
    background: $tf-extra-light-gray-2;
    border: 1px solid $tf-light-gray;

    &.append-input {
      border-left: none;
    }

    &.prepend-input {
      border-right: none;
    }
  }

  .with-suffix-icon {
    position: relative;

    .auto-complete-textarea .el-textarea__inner {
      padding-right: 30px;
    }
  }

  .with-text-addition {
    display: flex;

    > span {
      &:not(.text-addition) {
        flex-grow: 1;
      }
    }
  }

  .el-select-dropdown.autocomplete li:focus {
    background-color: $tf-extra-light-gray-1;
    outline: 0;
  }

  // Override the dropdown items to show full text.
  .el-select-dropdown__item {
    overflow: inherit;
    height: auto;
    padding: {
      top: 5px;
      bottom: 5px;
    }
    text-overflow: inherit;
    line-height: inherit;
    white-space: inherit;

    &.is-disabled {
      pointer-events: none;
    }
  }

  .el-select-dropdown__list {
    .is-disabled {
      &,
      &:hover {
        // I'm having to use important to override Elements native styles ):
        /* stylelint-disable declaration-no-important */
        color: $tf-medium-gray !important;
        background: $tf-extra-light-gray-1 !important;
        /* stylelint-enable declaration-no-important */
        font-style: italic;
      }
    }
  }
</style>
