<template>
  <div>
    <popper
      ref="tf-tooltip"
      v-bind="{
        appendToBody: true,
        disabled,
        forceShow: showPopover,
        options: {
          placement: 'bottom-start',
          modifiers: {
            flip: {
              // disabling to force popper to stay below
              // input regardless of viewport height
              enabled: false,
            },
          },
        },
        visibleArrow: false
      }"
      trigger="clickToToggle"
      @show="showPopover = true"
    >
      <!--
        Due to a bug in vue-popper, the content should be the first element:
        https://github.com/RobinCK/vue-popper/issues/71
      -->
      <div
        v-click-outside="onClickOutside"
        :class="[
          `tf-popper tf-filter-type-${filter.type}`,
          { [`tf-filter-type-${dateType}`]: dateType },
        ]"
        :style="{ width: filter.popoverWidth, minWidth: filter.width }"
      >
        <!-- Wrap the content in v-if to prevent contents from loading. -->
        <template v-if="showPopover">
          <template v-if="['string', 'range', 'array'].includes(filter.type)">
            <span
              v-if="!localOptions.length && searchValue"
              class="empty-search"
            >
              No results found
            </span>
            <ElCheckboxGroup v-model="localValue">
              <TfCheckbox
                v-for="option in localOptions"
                ref="checkbox"
                :key="option[valueKey]"
                :data-test="`select ${filter.prop} filter`"
                :disabled="option.disabled"
                :label="option[valueKey]"
                @keydown.native.enter="$event.target.click()"
              >
                <span>
                  <template v-if="option[labelKey]">
                    {{ option[labelKey] }}
                  </template>
                  <slot
                    v-else
                    name="noValue"
                  >
                    &mdash;
                  </slot>
                </span>
              </TfCheckbox>
            </ElCheckboxGroup>
          </template>
          <TfFilterDate
            v-else
            v-model="localValue"
            :date-type="dateType"
            :filter="filter"
            @changeDateFilterType="changeDateFilterType"
            @closePopover="closePopover"
          />
          <footer>
            <div class="btn-group">
              <TfButton
                :disabled="disableBtns"
                :data-test="`reset ${filter.prop} filter`"
                size="medium"
                type="secondary"
                @click="resetFilters"
                v-text="'Reset'"
              />
              <TfButton
                :disabled="disableBtns"
                :data-test="`apply ${filter.prop} filter`"
                size="medium"
                @click="applyFilter"
                v-text="'Done'"
              />
            </div>
          </footer>
        </template>
      </div>
      <ElInput
        slot="reference"
        v-bind="{
          class: [{ 'is-active': showPopover && !disabled }, 'input-filter'],
          disabled,
          name: filterPropName,
          placeholder: filter.label,
          readonly: !['string', 'range', 'array'].includes(filter.type),
        }"
        v-model="searchValue"
        :data-test="`edit ${filter.prop} filter`"
        @keydown.native.tab="focusOnCheckbox"
      >
        <icon
          slot="suffix"
          name="caret-down"
          :data-prop-name="filterPropName"
          @click="toggleIcon"
        />
      </ElInput>
    </popper>
    <div
      v-if="value.length"
      class="tf-filter-values-container"
    >
      <span
        v-for="option in displayOptions"
        :key="option.text"
        class="tf-filter-value"
      >
        <template v-if="option.text">
          {{ option.text }}
        </template>
        <slot
          v-else
          name="noValue"
        >
          &mdash;
        </slot>
        <TfButton
          no-padding
          class="btn-remove-filter-value"
          type="primary"
          icon="times"
          size="icon-no-fill"
          @click="removeValue(option.id)"
        />
      </span>
    </div>
  </div>
</template>

<script>
  import TfFilterDate from './Date';

  export default {
    components: { TfFilterDate },
    props: {
      disabled: {
        type: Boolean,
        default: false,
      },
      filter: {
        type: Object,
        required: true,
      },
      labelKey: {
        type: String,
        default: 'label',
      },
      value: {
        type: Array,
        default: () => [],
      },
      valueKey: {
        type: String,
        default: 'value',
      },
    },
    data: (vm) => ({
      dateType: null,
      localValue: [...vm.value],
      localOptions: [],
      searchValue: '',
      showPopover: false,
      timer: 0,
    }),
    computed: {
      /**
       * If the filter is a 'date-range', 'date-range-input', or 'month' dateType, then test for all values.
       *
       * @returns {Array}
       */
      disableBtns() {
        let disableBtns = false;

        if (['date-range-input', 'date-range'].includes(this.dateType)) {
          disableBtns = this.localValue[0]
            ? this.localValue[0].includes('')
            : true;
        } else if (this.dateType === 'month') {
          disableBtns = !this.localValue[0];
        }

        return disableBtns;
      },
      /**
       * Map this.value to either the value or the appropriate label.
       * For filter types that are a range, we want to create this list
       * from this.filter.options since that has the appropriate order.
       *
       * @returns {Array}
       */
      displayOptions() {
        const values = this.value.map((id) => {
          const formatDate = (dateValue, format = 'MM/DD/YY') => this.$TF.DatesUtils.formatSimpleDate(dateValue, format);
          let text;

          if (this.filter.type === 'range') {
            ({ [this.labelKey]: text } = this.filter.options.find(
              ({ [this.valueKey]: localValue }) => localValue === id,
            ));
          } else if (this.filter.type === 'string' || this.filter.type === 'array') {
            text = id;
          } else if (this.filter.dateType === 'month') {
            text = formatDate(id, 'MMMM YYYY');
          } else if (['date-range', 'date-range-input'].includes(this.filter.dateType)) {
            const [from, to] = id;

            text = `${formatDate(from)}-${formatDate(to)}`;
          }

          return { id, text };
        });
        let displayOptions;

        if (this.filter.type === 'range') {
          displayOptions = [];

          this.filter.options.forEach((option) => {
            if (this.value.includes(option[this.valueKey])) {
              displayOptions.push({
                id: option[this.valueKey],
                text: option[this.labelKey],
              });
            }
          });
        } else {
          // eslint-disable-next-line vue/no-side-effects-in-computed-properties
          displayOptions = this.$TF.Formatting.sortObjects(values, 'text');
        }

        return displayOptions;
      },
      /**
       * Cache a string based on the filter's prop to be used throughout the template.
       *
       * @returns {string}
       */
      filterPropName() {
        return `filter-${this.filter.prop}`;
      },
    },
    watch: {
      /**
       * The reset button is controlled in the parent so we want to watch
       * for when the model is emptied from the parent to reset the local filter values.
       */
      value() {
        if (!this.value.length) {
          this.resetFilters();
        }
      },
      searchValue: {
        /**
         * Debounce the search value to filter the options.
         *
         * @param {string} val
         */
        handler(val) {
          if (!['range', 'string', 'array'].includes(this.filter.type)) {
            this.localOptions = this.filter.options;
            return;
          }

          clearTimeout(this.timer);

          this.timer = setTimeout(() => {
            const filteredResults = [];
            let maxResults = 0;
            let i = 0;

            while (maxResults < 30 && i < this.filter.options.length) {
              if (
                !val
                || this.filter.options[i][this.labelKey].toLowerCase().includes(val.toLowerCase())
              ) {
                filteredResults.push(this.filter.options[i]);
                maxResults += 1;
              }
              i += 1;
            }

            this.localOptions = filteredResults;
          }, 200);
        },
        immediate: true,
      },
    },
    created() {
      this.changeDateType();
    },
    methods: {
      /**
       * Close the popover and if there is a change in filter values,
       * $emit the localValue to the parent.
       */
      applyFilter() {
        if (
          this.value.length === this.localValue.length
          && this.localValue.every((value) => this.value.includes(value))
        ) {
          this.closePopover();
          return;
        }

        if (this.dateType) {
          this.$emit('changeDateFilterType', this.dateType);
        }

        this.$emit('input', this.localValue);
        this.closePopover();
      },

      /**
       * Clear or reset the localValue based on the passed param.
       * $emit the localValue to the parent.
       *
       * @param {string} dateFilterType
       */
      changeDateFilterType(dateFilterType) {
        const newLocalValue = this.filter.dateType === dateFilterType
          ? this.value
          : [];

        this.dateType = dateFilterType;
        this.$set(this, 'localValue', newLocalValue);

        /* Changing the filter type will change the content of the
         * popover but not inform the underlying Popover component
         * that it has been updated. Calling updatePopper explicitly
         * on tf-tooltip will schedule a re-render / repositioning */
        if (this.$refs['tf-tooltip'].updatePopper) {
          this.$refs['tf-tooltip'].updatePopper();
        }
      },

      /**
       * Clear or reset the localValue based on the passed param.
       * $emit the localValue to the parent.
       */
      changeDateType() {
        if (!this.filter.dateType) {
          return;
        }

        let dateType = this.filter.dateType || this.filter.type;

        if (this.filter.type === 'month-and-date-range') {
          dateType = this.value[0] && Array.isArray(this.value[0])
            ? 'date-range'
            : 'month';
        }

        this.dateType = dateType;
      },

      /**
       * Clear the search value, closes the popover.
       * Clear the localValue if desired.
       *
       * @param {boolean} resetValue
       */
      closePopover(resetValue = false) {
        if (!this.showPopover) {
          return;
        }
        this.showPopover = false;
        this.searchValue = '';

        if (resetValue) {
          this.$set(this, 'localValue', [...this.value]);
          this.changeDateType();
        }

        // I'm wrapping this due a test failure that
        // I'm not sure of a way around right now.
        // TypeError: this.$refs.tf-tooltip.doClose is not a function
        if (this.$refs['tf-tooltip'].doClose) {
          this.$refs['tf-tooltip'].doClose();
        }
      },

      /**
       * If the user hits tab while focused on the main input,
       * we move the user to the next checkbox. Element does something odd
       * so this needs to be done on nextTick().
       *
       * @param {object} $event
       * @param {string} $event.shiftKey
       */
      focusOnCheckbox({ shiftKey }) {
        if (!shiftKey) {
          this.$nextTick(() => {
            this.$refs.checkbox[0].$el.querySelector('input').focus();
          });
        }
      },

      /**
       * Evaluate the the clicked element's name to close the popover and reset the values.
       *
       * @param {object} event
       */
      onClickOutside(event) {
        if ((event.target.name
          || event.target.getAttribute('data-prop-name'))
          === `${this.filterPropName}`) {
          return;
        }
        this.closePopover(true);
      },

      /**
       * Close popover with caret
       *
       */
      toggleIcon() {
        if (this.showPopover) {
          // delay close of popover as multiple popovers may exist and conflict with onClickOutside
          this.$forceNextTick(() => {
            this.closePopover(true);
          });
        }
      },

      /**
       * Remove the value from the localValue and $emit to the parent.
       *
       * @param {string | Array} value
       */
      removeValue(value) {
        const index = this.localValue.findIndex((localValue) => localValue === value);

        this.localValue.splice(index, 1);
        this.$emit('input', this.localValue);
      },
      /**
       * Reset the localValue and create a random ID for the calendars.
       */
      resetFilters() {
        this.$set(this, 'localValue', []);
      },
    },
  };
</script>

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

  footer {
    padding: 10px 0;
    border-top: 1px solid $tf-medium-gray;
  }

  .tf-button {
    &.btn-remove-filter-value {
      margin-top: 2px;
    }

    &.tf-button--secondary {
      border-color: $tf-white;
    }
  }

  .tf-popper {
    padding: 0;
    box-sizing: border-box;
    background: $tf-white;
    border: 1px solid $tf-blue;
    border-radius: 2px;
    text-align: left;
  }

  .tf-filter {
    &-value {
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
      padding: 5px 7px 5px 15px;
      font-size: 14px;

      > span {
        text-overflow: ellipsis;
        white-space: nowrap;
        overflow: hidden;
      }
    }

    &-values-container {
      background: $tf-extra-light-gray-2;
      border: 1px solid $tf-light-gray;
      border: {
        top: 0;
        radius: 0 0 2px 2px;
      }
    }
  }

  ::v-deep .el {
    &-checkbox {
      display: flex;
      padding: 12px 10px;
      cursor: pointer;

      &:not(:last-child) {
        border-bottom: 1px $tf-extra-light-gray-1 solid;
      }

      &-group {
        box-sizing: border-box;
        max-height: 300px;
        width: auto;
        min-width: 200px;
        overflow-y: auto;
        position: relative;
      }

      &__inner {
        margin-top: 2px;
      }

      &__label {
        line-height: 20px;
      }

      + .el-checkbox {
        margin-left: 0;
      }

      &:hover {
        background: $tf-light-blue;
        color: $tf-blue;
      }
    }

    /* stylelint-disable selector-max-compound-selectors */
    /* stylelint-disable max-nesting-depth  */
    &-input {
      &__suffix {
        display: flex;
        align-items: center;
        margin-right: 6px;
        cursor: pointer;

        svg {
          height: 22px;
          width: 14px;

          path {
            pointer-events: none;
          }
        }
      }

      &__suffix-inner {
        padding-top: 3px;
      }

      &.input-filter.is-active {
        svg {
          fill: $tf-blue;
          transform: rotate(180deg);
        }
      }

      input {
        height: 30px;
        line-height: 30px;

        &::placeholder {
          opacity: 1;
          color: $tf-medium-gray;
        }
      }

      svg {
        transition: transform .2s ease-out;
        transform-origin: center;
        transform: rotate(0deg);
      }
    }
    /* stylelint-enable selector-max-compound-selectors */
    /* stylelint-enable max-nesting-depth  */
  }

  .empty-search {
    display: block;
    padding: 10px 15px;
    color: $tf-medium-gray;
    font-style: italic;
  }
</style>
