<template>
  <div
    class="itf-sortable"
    @keydown.enter="onKeydownEnter"
    @keydown.space="onKeydownEnter"
    @keydown.down="onKeydownDown"
    @keydown.right="onKeydownDown"
    @keydown.up="onKeydownUp"
    @keydown.left="onKeydownUp"
    @keydown.escape="onKeydownEscape"
    @keydown.tab="onKeydownEscape"
    @mousedown="onMouseDown"
    @click="onMouseClick"
    @dblclick="onMouseClick"
    @touchstart="onTouchStart"
    @touchcancel="onTouchCancel"
    @mouseup="onMouseUp"
    @touchend="onTouchEnd"
    @mouseover="onMouseOver"
    @mouseout="onMouseOut"
    @mousemove="onMouseMove"
    @touchmove="onTouchMove"
    @dropoutside="onDropOutside"
  >
    <auto-scroll
      ref="autoscroll"
      :disabled="!isSortingInProgress || !autoScroll"
      :parent-selector="autoScroll === 'window' ? null : autoScroll"
      class="autoscroll"
    >
      <div
        ref="originalList"
        class="itf-sortable-list itf-sortable-list-original"
        :class="listClass"
      >
        <!-- @slot Slot for typeahead -->
        <slot />
      </div>
      <ul
        ref="mockedList"
        class="itf-sortable-list itf-sortable-list-mocked hidden"
        :class="listClass"
      />
    </auto-scroll>
  </div>
</template>
<script>
import autoScroll from './AutoScroll.vue';
import { onDropOutside } from './utils/event-outside';
import OriginalSortableItemList from './sortable-item-list/original-sortable-item-list';
import MockedSortableItemList, { ATTRIBUTE_SKIP_SORTING } from './sortable-item-list/mocked-sortable-item-list';
import sortItemList from './utils/sort-item-list';
import vibrate from './utils/vibrate';
import stopEvent from './utils/stop-event';
import { AXIS } from './sortable-item-list/axis';
import './Sortable.scss';

/**
 * Sortable component to reorder LI elements in a UL item list.
 * Each sortable element should have attribute sortable="true" in case
 * if `require-sortable-attribute="true"` then sorted element
 * or its part should have `sortable` attribute
 * if `sortable-skip="true"` then sorting is ignored for that element
 */
export default {
  name: 'itfSortable',
  components: {
    autoScroll
  },
  props: {
    /**
       * Items to sort
       */
    value: {
      type: Array,
      default: /* istanbul ignore next */ () => []
    },
    /**
       * Axis to sort: x, y, xy
       */
    axis: {
      type: String,
      default: AXIS.xy,
      validator (value) {
        return [AXIS.x, AXIS.y, AXIS.xy].includes(value);
      }
    },
    /**
       * Sorting is not allowed if true
       */
    readOnly: {
      type: Boolean,
      default: false
    },
    /**
       * Auto scroll during sorting is available if set.
       * Value `window` manages scrolling for window,
       * else treated as an html selector for its parent to scroll.
       */
    autoScroll: {
      type: String,
      default: null
    },
    /**
       * Sorting is allowed if an element has attribute `sortable/data-sortable` = `true`
       */
    requireSortableAttribute: {
      type: Boolean,
      default: false
    },
    /**
       * Sortable list class name
       */
    listClass: {
      type: String,
      default: null
    },
    /**
       * Item cursor class name
       */
    cursorClass: {
      type: String,
      default: null
    },
    /**
       * Item cursor selector
       */
    cursorSelector: {
      type: String,
      default: null
    }
  },
  data () {
    return {
      originalItemList: new OriginalSortableItemList(),
      mockedItemList: new MockedSortableItemList(),
      isSortingInProgress: false
    };
  },
  watch: {
    readOnly (newValue) {
      if (newValue) {
        this.cancelSorting(null);
      }
    },
    axis (newValue) {
      this.mockedItemList.setAxis(newValue);
    }
  },
  mounted () {
    this.originalItemList.setContainer(this.$refs.originalList);
    this.mockedItemList.setContainer(this.$refs.mockedList);
    this.mockedItemList.setAxis(this.axis);
    this.onDropOutsideRelease = onDropOutside(this.$el, event =>
      this.onDropOutside(event)
    );
  },
  beforeDestroy () {
    this.onDropOutsideRelease();
  },
  methods: {
    onKeydownEnter (event) {
      if (this.isSortingInProgress) {
        this.finishSorting(true);
        return stopEvent(event);
      }
      if (this.isSortingAvailable(event.target)) {
        this.startSorting(event);
        setTimeout(() => this.scrollToCursorItem(), 100);
        return stopEvent(event);
      }
      return null;
    },
    onKeydownDown (event) {
      if (this.isSortingInProgress) {
        if (this.mockedItemList.moveDown()) {
          this.scrollToCursorItem();
          this.emitChangeEvent('keydown-down');
        }
        return stopEvent(event);
      }
      return null;
    },
    onKeydownUp (event) {
      if (this.isSortingInProgress) {
        this.mockedItemList.moveUp();
        this.scrollToCursorItem();
        this.emitChangeEvent('keydown-up');
        return stopEvent(event);
      }
      return null;
    },
    onKeydownEscape (event) {
      if (this.isSortingInProgress) {
        this.cancelSorting(this.mockedItemList.getFromPosition());
        return stopEvent(event);
      }
      return null;
    },
    onMouseDown (event) {
      if (!this.isSortingInProgress && this.isSortingAvailable(event.target)) {
        this.startSorting(event);
        return stopEvent(event);
      }
      return null;
    },
    onMouseClick () {
      this.cancelSorting(null);
    },
    onTouchStart (event) {
      if (this.isSortingAvailable(event.target)) {
        this.startSorting(event);
        if (this.isSortingInProgress) {
          vibrate();
          return stopEvent(event);
        }
      }
      return null;
    },
    onTouchCancel (event) {
      if (this.isSortingInProgress) {
        return stopEvent(event);
      }
      return null;
    },
    onMouseUp (event) {
      if (this.isSortingInProgress) {
        this.finishSorting();
        return stopEvent(event);
      }
      return null;
    },
    onTouchEnd (event) {
      if (this.isSortingInProgress) {
        this.finishSorting();
        vibrate();
        return stopEvent(event);
      }
      return null;
    },
    onMouseOver (event) {
      if (this.isSortingInProgress) {
        return stopEvent(event);
      }
      return null;
    },
    onMouseOut (event) {
      if (this.isSortingInProgress) {
        return stopEvent(event);
      }
      return null;
    },
    onMouseMove (event) {
      if (this.isSortingInProgress) {
        this.performSorting(event);
        this.emitChangeEvent('mousemove');
        return stopEvent(event);
      }
      return null;
    },
    onTouchMove (event) {
      if (this.isSortingInProgress) {
        this.performSorting(event);
        this.emitChangeEvent('touchmove');
        return stopEvent(event);
      }
      return null;
    },
    onDropOutside (event) {
      if (this.isSortingInProgress) {
        this.cancelSorting(null);
        return stopEvent(event);
      }
      return null;
    },
    scrollToCursorItem () {
      this.$refs.autoscroll.autoScroll(
        this.mockedItemList.getCursorPageX(),
        this.mockedItemList.getCursorPageY(),
        false
      );
    },
    startSorting (event) {
      const { target } = event;
      this.cancelSorting(null);
      if (
        !this.readOnly &&
        this.originalItemList.isSortable(target, this.requireSortableAttribute)
      ) {
        const itemElement = this.originalItemList.getSortItem(target);
        const position = this.originalItemList.getItemIndexByElement(
          itemElement
        );
        if (position >= 0) {
          /**
             * Start sorting
             * @param {number} position Start position to sort
             * @param {Event} event Event
             */
          this.$emit('sortable-start', position, event);
          this.mockSortingList(position, event);
          this.isSortingInProgress = true;
          this.changeEventSign = null;
        }
      }
    },
    performSorting (event) {
      if (this.isSortingInProgress) {
        this.mockedItemList.move(event);
      }
    },
    finishSorting (setFocus = false) {
      if (this.isSortingInProgress) {
        const from = this.mockedItemList.getFromPosition();
        const to = this.mockedItemList.getToPosition();
        const callback = itemList => sortItemList(itemList, from, to);
        if (this.mockedItemList.isSorted()) {
          this.mockedItemList.finishSorting();
          const items = callback(this.value);
          /**
             * Change model value
             */
          this.$emit('input', items);
          /**
             * Sort happened
             * @param {Function} callback Callback to sort an array with one argument.
             *        If sorting through v-model is not enough
             * @param {number} from From position
             * @param {number} to From position
             */
          this.$emit('sort', callback, from, to);
          this.$emit('sortable-finish');
        }
        const focusItemPosition = setFocus ? to : null;
        setTimeout(() => this.restoreOriginalList(focusItemPosition), 10);
        this.isSortingInProgress = false;
      }
    },
    mockSortingList (fromPosition, event) {
      this.originalItemList.hide();
      this.mockedItemList.reset();
      this.originalItemList.cloneTo(this.mockedItemList.getContainer());
      this.mockedItemList.createCursorItem(
        fromPosition,
        event,
        this.cursorClass,
        this.cursorSelector
      );
    },
    restoreOriginalList (positionToFocus) {
      this.originalItemList.restore(positionToFocus);
      this.mockedItemList.hide();
    },
    cancelSorting (positionToFocus) {
      if (this.isSortingInProgress) {
        this.restoreOriginalList(positionToFocus);
        this.isSortingInProgress = false;
        /**
           * Cancel sorting
           */
        this.$emit('sortable-cancel');
        /**
           * Finish sorting
           */
        this.$emit('sortable-finish');
      }
    },
    emitChangeEvent (source) {
      const sign = [
        this.mockedItemList.getFromPosition(),
        this.mockedItemList.getToPosition(),
        source
      ].join('!');
      if (this.changeEventSign !== sign) {
        /**
           * Change sorting
           * @param {number} from From position
           * @param {number} to To position
           * @param {string} source Source
           */
        this.$emit(
          'sortable-change',
          this.mockedItemList.getFromPosition(),
          this.mockedItemList.getToPosition(),
          source
        );
        this.changeEventSign = sign;
      }
    },
    isSortingAvailable (element) {
      const sortableSkip = element.closest(`[${ATTRIBUTE_SKIP_SORTING}]`);
      if (sortableSkip) {
        return false;
      }
      return !sortableSkip || !this.mockedItemList.isToSkip(sortableSkip);
    }
  }
};
</script>
