<template>
  <table>
    <tr>
      <slot name="headers" v-bind:headers="headers" v-bind:toggleSort="toggleSort">
        <th
          v-for="header in headers"
          :key="header.id || header.text"
          @click="toggleSort(header.id || header.text)"
        >{{ header.text }}</th>
      </slot>
    </tr>
    <tr
      v-for="(item, itemIndex) in readonly ? internalValue : modelValue"
      :key="itemKeyFn(item, itemIndex)"
    >
      <slot
        v-bind:item="item"
        v-bind:itemIndex="itemIndex"
        v-bind:deleteItem="deleteItem"
        v-bind:displaceItem="displaceItem"
        v-bind:getAttr="getAttr"
        v-bind:headers="headers"
      >
        <td v-for="header in headers" :key="header.id || header.text">
          <slot
            :name="`td-${header.id||header.text}`"
            v-bind:item="item"
            v-bind:itemIndex="itemIndex"
            v-bind:deleteItem="deleteItem"
            v-bind:displaceItem="displaceItem"
            v-bind:getAttr="getAttr"
            v-bind:data="getAttr(item, header.value)"
          >{{ getAttr(item, header.value) }}</slot>
        </td>
      </slot>
      <td>
        <button type="button" v-if="deleteButton" @click="deleteItem(item)">Delete</button>
        <button type="button" v-if="moveButton" @click="displaceItem(item, -1)">↑</button>
        <button type="button" v-if="moveButton" @click="displaceItem(item, +1)">↓</button>
      </td>
    </tr>
  </table>
</template>

<script>
function modulo(a, n) {
  return ((a % n) + n) % n;
}

function sortArray(arr, cmpFn) {
  // TODO: This is VERY inefficient
  const sortedArr = Array.from(arr);
  sortedArr.sort(cmpFn);
  if (!sortedArr.every((x, i) => x === arr[i])) {
    arr.sort(cmpFn);
    return true;
  }
  return false;
}

function attrGetterFn(value) {
  if (value == null || value === "") return x => x;
  if (typeof value === "function") return value;

  const path = value.split(".");
  return x => {
    for (let attr of path) {
      x = x[attr];
    }
    return x;
  };
}

function sortFn(value) {
  if (value == null || value === false) return null;
  if (value === true) return (a, b) => (a > b ? +1 : a < b ? -1 : 0);
  if (typeof value === "function") return value;

  const attrGetter = attrGetterFn(value);
  return (a, b) => {
    return attrGetter(a) > attrGetter(b);
  };
}

export default {
  name: 'DataGrid',
  data() {
    return {
      internalSortCriteria: [],
      internalValue: [], // readonlyがtrueの時のみmodelValueのコピーとして使う
    };
  },
  props: {
    readonly: {
      // modelValueにcomputed由来の値を渡す場合は必ずtrueにする
      type: Boolean,
      default: true,
    },
    modelValue: {
      type: Array,
      required: true
    },
    headers: {
      type: Array,
      default() {
        return [];
      }
    },
    itemKey: {
      default: null,
      validator(val) {
        return (
          val == null || typeof val === "function" || typeof val === "string"
        );
      }
    },
    sort: {
      default: null,
      validator(val) {
        return (
          val == null ||
          typeof val === "boolean" ||
          typeof val === "function" ||
          typeof val === "string"
        );
      }
    },
    deleteButton: {
      type: Boolean,
      default: false
    },
    moveButton: {
      type: Boolean,
      default: false
    },
    sortCriteria: {
      type: Array,
      default() {
        return [];
      }
    }
  },
  computed: {
    itemKeyFn() {
      return attrGetterFn(this.itemKey);
    },
    globalSortFn() {
      return sortFn(this.sort);
    },
    headerSortFns() {
      return this.headers
        .map(x => [attrGetterFn(x.value), sortFn(x.sort)])
        .map(([attrGetter, sortFn]) =>
          sortFn == null ? null : (a, b) => sortFn(attrGetter(a), attrGetter(b))
        );
    },
    sortFn() {
      if (this.globalSortFn != null) return this.globalSortFn;

      const sortFns = [];
      for (let [headerId, reverse] of this.internalSortCriteria) {
        if (headerId == null) continue;
        const idx = this.headers.findIndex(
          x => x.id === headerId || x.text === headerId
        );
        if (idx === -1) continue;
        const headerSortFn = this.headerSortFns[idx];
        if (headerSortFn == null) continue;
        sortFns.push([headerSortFn, reverse]);
      }
      if (sortFns.length === 0) return null;
      return (a, b) => {
        for (let [f, reverse] of sortFns) {
          const ret = f(a, b);
          if (ret != 0) return reverse ? -ret : ret;
        }
        return 0;
      };
    }
  },
  watch: {
    modelValue: {
      handler(val, oldVal) {
        this.internalValue = Array.from(val);
        this.updateValue();
      },
      immediate: true,
      deep: true,
    },
    sort(val, oldVal) {
      this.updateValue();
    },
    sortCriteria: {
      handler(val, oldVal) {
        this.internalSortCriteria = val;
      },
      deep: true,
    },
    internalSortCriteria: {
      handler(val, oldVal) {
        this.$emit("update:sortCriteria", val);
        this.updateValue();
      },
      deep: true
    }
  },
  methods: {
    getAttr(obj, path) {
      return attrGetterFn(path)(obj);
    },
    updateValue() {
      if (this.sortFn != null) {
        let value;
        if(this.readonly)
          value = this.internalValue;
        else
          value = this.modelValue;
        if(sortArray(value, this.sortFn)) {
          // TODO: this.modelValue.sortを使っているのになぜかDOMが更新されないことがある
          // しかしこの方法ではコンポーネントの外では更新されない...
          // this.$forceUpdate();
          // this.modelValue = Array.from(this.modelValue);
        }
      }
    },
    deleteItem(item) {
      if(this.readonly) return;
      const idx = this.modelValue.indexOf(item);
      this.modelValue.splice(idx, 1);
    },
    displaceItem(item, relativeIndex) {
      if(this.readonly) return;
      const idx = this.modelValue.indexOf(item);
      const newIdx = modulo(idx + relativeIndex, this.modelValue.length);
      this.modelValue.splice(idx, 1);
      this.modelValue.splice(newIdx, 0, item);
      this.internalSortCriteria.length = 0;
    },
    toggleSort(headerId) {
      if (headerId == null) return;
      const idx = this.headers.findIndex(
        x => x.id === headerId || x.text === headerId
      );
      if (idx === -1) return;
      const sortFn = this.headerSortFns[idx];
      if (sortFn == null) return;
      if (
        this.internalSortCriteria.length === 1 &&
        this.internalSortCriteria[0][0] === headerId
      ) {
        const x = [headerId, !this.internalSortCriteria[0][1]];
        this.internalSortCriteria[0] = x;
      } else {
        this.internalSortCriteria.splice(0, this.internalSortCriteria.length);
        this.internalSortCriteria.push([headerId, false]);
      }
    }
  }
};
</script>
