<template>
  <div>
    <div class="search-controls mb-4">
      <div class="search-container mr-2">
        <input
          type="text"
          v-model="query"
          class="form-control"
          placeholder="zoek..."
          @keydown.down.prevent="down"
          @keydown.up.prevent="up"
          @keydown.enter.prevent="enter"
        />
        <div class="input-icons">
          <clear-input v-if="!!query" :onClear="() => (query = undefined)" />
          <div v-if="isLoading" class="spinner-border" role="status">
            <span class="sr-only">Laden...</span>
          </div>
        </div>
      </div>

      <div v-if="isTranslatable && languageInfos.length > 1" class="language-filter-container">
        <span class="mr-2">Filter op taal:</span>
        <select v-model="languageFilter" class="form-control">
          <option value selected>Alle talen</option>
          <option v-for="language in languageInfos" :value="language.language" :key="language.language">
            {{ language.displayName }}
          </option>
        </select>
      </div>
    </div>

    <h2 class="h3 mb-3">{{ searchResultsAmount }} <span v-text="searchResultsAmount === 1 ? 'resultaat' : 'resultaten'"></span></h2>

    <table class="table table-striped">
      <thead>
        <th v-for="(name, key) in fieldNames" :key="key" @click="sortBy(key)" v-bind:class="{ sortable: isSortable(key) }">
          {{ name }}
          <span v-if="sorting.column === getSortColumn(key) && sorting.order === 'asc'" class="icon-circle-down"></span>
          <span v-if="sorting.column === getSortColumn(key) && sorting.order === 'desc'" class="icon-circle-up"></span>
        </th>
      </thead>
      <tbody :class="selectableClass">
        <tr
          v-for="(item, key) in displayItems"
          @click="actionCheck(item.$item)"
          :key="item.$item.id"
          :class="{
            selected: selectedIndex === key,
            'overview-item-readonly': isReadOnly(item.$item),
            'overview-item-owner': isOwner(item.$item)
          }"
        >
          <td v-for="(name, key) in keyNames" :key="key">
            <component
              :is="customFields[fieldNames[name]] || $options.components.listingCell"
              :value="item[name]"
              :item="item"
              :property-name="fields[fieldNames[name]] || fieldNames[name]"
              :schema="schema && schema.properties[fields[fieldNames[name]]]"
            >
            </component>
          </td>
        </tr>
      </tbody>
    </table>
    <pagination :module="module" />
  </div>
</template>

<style lang="scss" scoped>
.search-controls {
  display: flex;
  flex-wrap: wrap;
}

.search-container {
  position: relative;
  width: 500px;
  max-width: 500px;

  &:hover .icon-cross {
    opacity: 0.5;

    &:hover {
      opacity: 1;
    }
  }
}

.language-filter-container {
  display: flex;
  align-items: center;

  span {
    flex-shrink: 0;
  }
}

.sortable {
  text-decoration: underline;
}

.table-striped {
  thead {
    cursor: default;
    user-select: none;
  }
}

.tbody-selectable {
  cursor: pointer;

  tr:hover,
  tr.selected {
    background: white;
  }
}

.input-icons {
  position: absolute;
  top: 0;
  right: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;

  .icon-cross {
    position: relative;
    top: auto;
    right: auto;
    transform: none;
  }

  .spinner-border {
    width: 1.5rem;
    height: 1.5rem;
    margin-right: 4px;
  }
}
</style>

<script>
import { mapState } from 'vuex';
import _debounce from 'lodash/debounce';
import listingCell from './listing-cell.vue';
import pagination from './pagination.vue';
import clearInput from './clear-input.vue';
import CrudHelpers from '../crud/crud-helpers';
import { PermissionTypes } from '../stores/types/PermissionStoreModel';

export default {
  docs: `Een sorteerbare, filterbare tabel`,
  props: {
    module: String,
    fields: {
      type: Object,
      required: true,
      docs: `Een object die mapt van vriendelijke namen naar property-namen.`,
      demoValue: () => ({
        Naam: 'name',
        Engels: 'english'
      })
    },
    customFields: {
      type: Object,
      docs: `Een object die mapt van vriendelijke namen naar een component-naam.
        Het component krijgt als properties <code>item</code>, <code>property-name</code>, <code>value</code> en <code>schema</code> mee.`,
      default: () => ({})
    },
    items: {
      type: Array,
      required: true,
      docs: `De lijst met objecten om te tonen.`,
      demoValue: () => [
        { id: 0, name: 'Boom', english: 'Tree' },
        { id: 1, name: 'Roos', english: 'Rose' },
        { id: 2, name: 'Vis', english: 'Fish' },
        { id: 3, name: 'Vuur', english: 'Fire' },
        { id: 4, name: 'Mus', english: 'Sparrow' }
      ]
    },
    action: {
      type: Function,
      docs: `De actie die wordt uitgevoerd als je op een regel klikt. Krijgt het gekozen object als argument.`,
      demoValue: (item) => window.alert('Dit heb je gekozen: ' + JSON.stringify(item))
    },
    schema: {
      type: Object,
      docs: `Schema die bij het object hoort`,
      demoValue: () => ({
        properties: {
          title: {
            format: 'string-input',
            title: 'Titel',
            type: 'string'
          }
        }
      })
    },
    isLoading: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      sorting: { order: '', column: '' },
      query: this.$route.query.query,
      selectedIndex: -1,
      languageFilter: this.$route.query.language || ''
    };
  },
  computed: {
    ...mapState('translations', ['languageInfos']),
    columns() {
      return Object.keys(Object.assign({}, this.fields, this.customFields));
    },
    // items filtered with query in url (key=value), for example &name=Qer
    displayItems() {
      return this.items.map((item) => {
        var displayItem = { $item: item };

        for (var key in this.fieldNames) {
          const field = this.fields[this.fieldNames[key]];
          const type = this.schema && this.schema.properties[field];

          // Show title instead of Guid for foreign key references
          // - TODO/nice to have: getDisplayFieldValue can cause a race condition, when the store doesn't have any values. In the ideal situation a listing-cell should getting the displayFieldValue imho.
          displayItem[key] =
            type && type.$ref ? CrudHelpers.getDisplayFieldValueForReference(this.$store, type.$ref, item[field]) : item[field];
        }
        return displayItem;
      });
    },
    fieldNames() {
      var result = {};
      this.columns.forEach((name, ix) => {
        result['key' + ix] = name;
      });
      return result;
    },
    keyNames() {
      return Object.keys(this.fieldNames);
    },
    selectableClass() {
      return this.action != null ? 'tbody-selectable' : '';
    },
    isTranslatable() {
      return this.schema.isTranslatable;
    },
    searchResultsAmount() {
      const results = this.$store.getters[`${this.module}/result`]();
      return results ? results.totalQueryItems : '';
    },
    searchState() {
      return this.$store.getters[`${this.module}/searchState`]();
    },
    sortingParamForState() {
      return this.sorting.column ? `${this.sorting.column}:${this.sorting.order}` : '';
    }
  },
  mounted() {
    // Try decoding sorting. Ignore on failure
    try {
      this.sorting = JSON.parse(this.$route.query.sorting) || this.sorting;
    } catch (error) {
      // ignore on failure
    }

    // TODO typing SearchCrudItemPayload
    const initialSearchState = {
      query: this.query,
      languageFilter: this.languageFilter,
      page: this.$route.query.page || 1,
      sort: this.sortingParamForState
    };

    this.$store.dispatch(`${this.module}/updateSearchState`, initialSearchState);
  },
  methods: {
    isReadOnly(item) {
      return PermissionTypes[String(item.userEntityPermission)] === PermissionTypes.Read;
    },
    isOwner(item) {
      return item.userIsOwner;
    },
    getSortColumn(key) {
      const field = this.fields[this.fieldNames[key]];
      return field && this.schema.properties[field] ? this.schema.properties[field].sortColumn : null;
    },
    isSortable(key) {
      const field = this.fields[this.fieldNames[key]];

      // null check needed for redirect-active on redirects overview
      return this.schema.properties[field] ? this.schema.properties[field].isSortable : false;
    },
    sortBy(key) {
      if (!this.isSortable(key)) return;

      const order = this.sorting.column === this.getSortColumn(key) && this.sorting.order === 'asc' ? 'desc' : 'asc';

      this.sorting = {
        column: this.getSortColumn(key),
        order
      };

      this.$store.dispatch(`${this.module}/updateSearchState`, Object.assign(this.searchState, { sort: this.sortingParamForState }));
    },
    up() {
      this.selectedIndex--;
      if (this.selectedIndex < 0) this.selectedIndex = this.$el.querySelectorAll('tbody tr').length - 1;
    },
    down() {
      this.selectedIndex++;
      if (this.selectedIndex >= this.$el.querySelectorAll('tbody tr').length) this.selectedIndex = 0;
    },
    enter() {
      var selected = this.$el.querySelector('tr.selected');
      if (selected) selected.click();
    },
    actionCheck(item) {
      if (this.action != null) this.action(item);
    },
    updateQueryParams() {
      let routerConfig = {
        query: {
          language: this.searchState.languageFilter || undefined,
          query: this.searchState.query || undefined,
          sorting: this.sorting.column !== '' ? JSON.stringify(this.sorting) : undefined,
          page: this.searchState.page
        }
      };

      this.$router.replace(routerConfig);
    }
  },
  watch: {
    searchState: {
      deep: true,
      handler() {
        this.updateQueryParams();
      }
    },
    query: _debounce(function () {
      // reset page when changing the query
      this.$store.dispatch(`${this.module}/updateSearchState`, Object.assign(this.searchState, { query: this.query, page: 1 }));
    }, 300),
    languageFilter() {
      // reset page when changing the language
      this.$store.dispatch(
        `${this.module}/updateSearchState`,
        Object.assign(this.searchState, { languageFilter: this.languageFilter, page: 1 })
      );
    }
  },
  components: {
    listingCell,
    pagination,
    clearInput
  }
};
</script>
