<script>
import { GlIcon } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import Mousetrap from 'mousetrap';
import VirtualList from 'vue-virtual-scroll-list';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import Item from './item.vue';

export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;

const originalStopCallback = Mousetrap.prototype.stopCallback;

export default {
  components: {
    GlIcon,
    Item,
    VirtualList,
  },
  props: {
    files: {
      type: Array,
      required: true,
    },
    visible: {
      type: Boolean,
      required: true,
    },
    loading: {
      type: Boolean,
      required: true,
    },
    showDiffStats: {
      type: Boolean,
      required: false,
      default: false,
    },
    clearSearchOnClose: {
      type: Boolean,
      required: false,
      default: true,
    },
  },
  data() {
    return {
      focusedIndex: -1,
      searchText: '',
      mouseOver: false,
      cancelMouseOver: false,
    };
  },
  computed: {
    filteredBlobs() {
      const searchText = this.searchText.trim();

      if (searchText === '') {
        return this.files.slice(0, MAX_FILE_FINDER_RESULTS);
      }

      return fuzzaldrinPlus.filter(this.files, searchText, {
        key: 'path',
        maxResults: MAX_FILE_FINDER_RESULTS,
      });
    },
    filteredBlobsLength() {
      return this.filteredBlobs.length;
    },
    listShowCount() {
      return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
    },
    listHeight() {
      return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
    },
    showClearInputButton() {
      return this.searchText.trim() !== '';
    },
  },
  watch: {
    visible() {
      this.$nextTick(() => {
        if (!this.visible) {
          if (this.clearSearchOnClose) {
            this.searchText = '';
          }
        } else {
          this.focusedIndex = 0;

          if (this.$refs.searchInput) {
            this.$refs.searchInput.focus();
          }
        }
      });
    },
    searchText() {
      this.focusedIndex = -1;

      this.$nextTick(() => {
        this.focusedIndex = 0;
      });
    },
    focusedIndex() {
      if (!this.mouseOver) {
        this.$nextTick(() => {
          const el = this.$refs.virtualScrollList.$el;
          const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
          const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;

          if (this.focusedIndex === 0) {
            // if index is the first index, scroll straight to start
            el.scrollTop = 0;
          } else if (this.focusedIndex === this.filteredBlobsLength - 1) {
            // if index is the last index, scroll to the end
            el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT;
          } else if (scrollTop >= bottom + el.scrollTop) {
            // if element is off the bottom of the scroll list, scroll down one item
            el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT;
          } else if (scrollTop < el.scrollTop) {
            // if element is off the top of the scroll list, scroll up one item
            el.scrollTop = scrollTop;
          }
        });
      }
    },
  },
  mounted() {
    if (this.files.length) {
      this.focusedIndex = 0;
    }

    Mousetrap.bind(['t', 'mod+p'], (e) => {
      if (e.preventDefault) {
        e.preventDefault();
      }

      this.toggle(!this.visible);
    });

    Mousetrap.prototype.stopCallback = function customStopCallback(e, el, combo) {
      if (
        (combo === 't' && el.classList.contains('dropdown-input-field')) ||
        el.classList.contains('inputarea')
      ) {
        return true;
      } else if (combo === 'mod+p') {
        return false;
      }

      return originalStopCallback.call(this, e, el, combo);
    };
  },
  methods: {
    toggle(visible) {
      this.$emit('toggle', visible);
    },
    clearSearchInput() {
      this.searchText = '';

      this.$nextTick(() => {
        this.$refs.searchInput.focus();
      });
    },
    onKeydown(e) {
      switch (e.keyCode) {
        case UP_KEY_CODE:
          e.preventDefault();
          this.mouseOver = false;
          this.cancelMouseOver = true;
          if (this.focusedIndex > 0) {
            this.focusedIndex -= 1;
          } else {
            this.focusedIndex = this.filteredBlobsLength - 1;
          }
          break;
        case DOWN_KEY_CODE:
          e.preventDefault();
          this.mouseOver = false;
          this.cancelMouseOver = true;
          if (this.focusedIndex < this.filteredBlobsLength - 1) {
            this.focusedIndex += 1;
          } else {
            this.focusedIndex = 0;
          }
          break;
        default:
          break;
      }
    },
    onKeyup(e) {
      switch (e.keyCode) {
        case ENTER_KEY_CODE:
          this.openFile(this.filteredBlobs[this.focusedIndex]);
          break;
        case ESC_KEY_CODE:
          this.toggle(false);
          break;
        default:
          break;
      }
    },
    openFile(file) {
      this.toggle(false);
      this.$emit('click', file);
    },
    onMouseOver(index) {
      if (!this.cancelMouseOver) {
        this.mouseOver = true;
        this.focusedIndex = index;
      }
    },
    onMouseMove(index) {
      this.cancelMouseOver = false;
      this.onMouseOver(index);
    },
  },
};
</script>

<template>
  <div class="file-finder-overlay" @mousedown.self="toggle(false)">
    <div class="dropdown-menu diff-file-changes file-finder show">
      <div :class="{ 'has-value': showClearInputButton }" class="dropdown-input">
        <input
          ref="searchInput"
          v-model="searchText"
          :placeholder="__('Search files')"
          type="search"
          class="dropdown-input-field"
          autocomplete="off"
          @keydown="onKeydown($event)"
          @keyup="onKeyup($event)"
        />
        <gl-icon
          name="search"
          class="dropdown-input-search"
          :class="{ hidden: showClearInputButton }"
        />
        <gl-icon
          name="close"
          class="dropdown-input-clear"
          role="button"
          :aria-label="__('Clear search input')"
          @click="clearSearchInput"
        />
      </div>
      <div>
        <virtual-list ref="virtualScrollList" :size="listHeight" :remain="listShowCount" wtag="ul">
          <template v-if="filteredBlobsLength">
            <li v-for="(file, index) in filteredBlobs" :key="file.key">
              <item
                :file="file"
                :search-text="searchText"
                :focused="index === focusedIndex"
                :index="index"
                :show-diff-stats="showDiffStats"
                class="disable-hover"
                @click="openFile"
                @mouseover="onMouseOver"
                @mousemove="onMouseMove"
              />
            </li>
          </template>
          <li v-else class="dropdown-menu-empty-item">
            <div class="gl-mr-3 gl-ml-3 gl-mt-3 gl-mb-3">
              <template v-if="loading">
                {{ __('Loading...') }}
              </template>
              <template v-else>
                {{ __('No files found.') }}
              </template>
            </div>
          </li>
        </virtual-list>
      </div>
    </div>
  </div>
</template>

<style scoped>
.file-finder-overlay {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 200;
}

.file-finder {
  top: 10px;
  left: 50%;
  transform: translateX(-50%);
}

.diff-file-changes {
  top: 50px;
  max-height: 327px;
}
</style>
