<template>
  <div>
    <div
      ref="item"
      :class="{
        'vue-resizable' : resizable,
        'resizing' : isResizing,
        'vue-draggable-dragging' : isDragging,
        'cssTransforms' : useCssTransforms,
        'disable-userselect': isDragging
      }"
      :style="style"
      class="vue-grid-item">
      <!-- @slot Header bar for GridItem -->
      <div
        v-if="$slots['header']"
        class="grid-item-header level">
        <div class="level-left">
          <slot name="header" />
        </div>
        <div class="level-right">
          <font-awesome-icon
            v-if="model && schema"
            icon="list"
            size="xs"
            @click="showModal()"
            class="facet-control" />
          <font-awesome-icon
            v-if="isDraggable"
            icon="arrows-alt"
            size="xs"
            class="facet-control-move facet-control" />
          <font-awesome-icon
            v-if="model.maximizable"
            icon="expand-arrows-alt"
            size="xs"
            @click="expandItem(id)"
            class="facet-control" />
          <font-awesome-icon
            v-if="model.removeable"
            icon="times-circle"
            size="xs"
            @click="removeItem(id)"
            class="facet-control" />
        </div>
      </div>

      <div class="">
        <div class="">
          <!-- @slot Left Column of GridItem Body -->
          <div
            v-if="$slots['body-left']"
            class="grid-item-body-left column">
            <slot
              $ref="body-left"
              name="body-left"/>
          </div>
          <div class="">
            <div>
              <!-- @slot Main/Default GridItem area -->
              <slot class="grid-item-body-main"/>

              <!-- @slot Bottom half of GridItem Body -->
              <div v-if="$slots['body-bottom']">
                <slot name="body-bottom"/>
              </div>
            </div>
          </div>

          <div
            v-if="$slots['body-right']"
            class="grid-item-body-right">
            <!-- @slot Right Column of GridItem Body -->
            <slot name="body-right"/>
          </div>
        </div>
      </div>

      <div
        v-if="$slots['footer']"
        class="grid-item-footer">
        <!-- @slot Footer section; goes under all the body -->
        <slot name="footer"/>
      </div>
      <span
        v-if="isResizable"
        ref="handle"
        :class="resizableHandleClass" />
    </div>
  </div>
</template>
<script>
import { setTopLeft, setTransform } from '../utils/utils';
import { getControlPosition, createCoreData } from '../utils/draggableUtils';
import resizeImg from './resize.png'
import store from '../store';

const interact = require('interactjs');

export default {
  name: 'GridItem',
  props: {
    /**
     * Boolean flag for the facet to allow draggable handle.
     * Non-required; defaults to true.
     */
    isDraggable: {
      type: Boolean,
      required: false,
      default: true
    },
    /**
     * Boolean option for facet if it's resizable via bottom right handle.
     * Non-required; defaults to true.
     */
    isResizable: {
      type: Boolean,
      required: false,
      default: true
    },
    /**
     * Number that sets the Minimum Height. (@todo Find what this is.  pixels?  Row?)actions: facet:
     * Non-required; defaults to 1
     */
    minH: {
      type: Number,
      required: false,
      default: 1
    },
    /**
     * Number that sets the Minimum Width. (@todo Find what this is.  pixels?  Row?)
     * Non-required; defaults to 1
     */
    minW: {
      type: Number,
      required: false,
      default: 1
    },
    /**
     * Number that sets the Maximum Height. (@todo Find what this is.  pixels?  Row?)
     * Non-required; defaults to Infinity
     */
    maxH: {
      type: Number,
      required: false,
      default: Infinity
    },
    /**
     * Number that sets the Maximum Width. (@todo Find what this is.  pixels?  Row?)
     * Non-required; defaults to Infinity
     */
    maxW: {
      type: Number,
      required: false,
      default: Infinity
    },
    /**
     * Number that is the 'column' starting point of facet.
     * Required, no default.
     */
    x: {
      type: Number,
      required: true,
      default: 1
    },
    /**
     * Number that is the 'row' starting point of facet.
     * Required, no default.
     */
    y: {
      type: Number,
      required: true,
      default: 1
    },
    /**
     * Number that is the width of the facet; in 'columns'
     * Required, no default.
     */
    w: {
      type: Number,
      required: true,
      default: 1
    },
    /**
     * Number that is the height of the facet; in 'row'
     * Required, no default.
     */
    h: {
      type: Number,
      required: true,
      default: 1
    },
    /**
     * The content of the facet.
     * Required, no default
     */
    id: {
      type: [Number],
      required: true,
      default: 1
    },
    /**
     * The content of the facet.
     * Required, no default
     */
    i: {
      type: [String, Number, Object, Array],
      required: true,
      default: ''
    },
    /**
     * @todo FIgure out what this is.
     */
    dragIgnoreFrom: {
      type: String,
      required: false,
      default: 'a, button'
    },
    /**
     * @todo FIgure out what this is.
     */
    dragAllowFrom: {
      type: String,
      required: false,
      default: '.facet-control-move'
    },
    /**
     * @todo FIgure out what this is.
     */
    resizeIgnoreFrom: {
      type: String,
      required: false,
      default: 'a, button'
    },
    /**
     * the data model for the facet
     */
    model: {
      type: Object,
      required: false
    },
    /**
     * The schema defines the config form.
     */
    schema: {
      type: Object,
      required: false
    }
  },
  inject: ['eventBus'],
  data () {
    return {
      isModalVisible: false,
      resizeImg: resizeImg,
      cols: 1,
      containerWidth: 100,
      rowHeight: 30,
      margin: [10, 10],
      maxRows: Infinity,
      draggable: null,
      resizable: null,
      expanding: null,
      useCssTransforms: true,

      isDragging: false,
      dragging: null,
      isResizing: false,
      resizing: null,
      lastX: NaN,
      lastY: NaN,
      lastW: NaN,
      lastH: NaN,
      style: {},

      dragEventSet: false,
      resizeEventSet: false,

      previousW: null,
      previousH: null,
      previousX: null,
      previousY: null,
      innerX: this.x,
      innerY: this.y,
      innerW: this.w,
      innerH: this.h
    };
  },
  computed: {
    /**
     * This just dynamically sets the resize handle.
     */
    resizableHandleClass () {
      return 'vue-resizable-handle';
    }
  },
  watch: {
    isDraggable () {
      this.draggable = this.isDraggable;
    },
    draggable () {
      const self = this;
      if (this.interactObj === null || this.interactObj === undefined) {
        this.interactObj = interact(this.$refs.item);
      }
      if (this.draggable) {
        const opts = {
          ignoreFrom: this.dragIgnoreFrom,
          allowFrom: this.dragAllowFrom
        };
        this.interactObj.draggable(opts);
        if (!this.dragEventSet) {
          this.dragEventSet = true;
          this.interactObj.on('dragstart dragmove dragend', (event) => {
            self.handleDrag(event);
          });
        }
      } else {
        this.interactObj.draggable({
          enabled: false
        });
      }
    },
    isResizable () {
      this.resizable = this.isResizable;
    },
    resizable () {
      const self = this;
      if (this.interactObj === null || this.interactObj === undefined) {
        this.interactObj = interact(this.$refs.item);
      }
      if (this.resizable) {
        const opts = {
          preserveAspectRatio: false,
          edges: {
            left: false,
            right: `.${this.resizableHandleClass}`,
            bottom: `.${this.resizableHandleClass}`,
            top: false
          },
          ignoreFrom: this.resizeIgnoreFrom
        };

        this.interactObj.resizable(opts);

        if (!this.resizeEventSet) {
          this.resizeEventSet = true;
          this.interactObj.on('resizestart resizemove resizeend', (event) => {
            self.handleResize(event);
          });
        }
      } else {
        this.interactObj.resizable({
          enabled: false
        });
      }
    },
    /**
     * When the rowHeight changes it updates the computed style component.
     */
    rowHeight () {
      this.createStyle();
    },
    /**
     * When the cols changes it updates the computed style component.
     */
    cols () {
      this.createStyle();
    },
    /**
     * When the containerWidth changes it updates the computed style component.
     */
    containerWidth () {
      this.createStyle();
    },
    /**
     * When x changes it updates the computed style component.
     */
    x (newVal) {
      this.innerX = newVal;
      this.createStyle();
    },
    /**
     * When y changes it updates the computed style component.
     */
    y (newVal) {
      this.innerY = newVal;
      this.createStyle();
    },
    /**
     * When h changes it updates the computed style component.
     */
    h (newVal) {
      this.innerH = newVal;
      this.createStyle();
    },
    /**
     * When w changes it updates the computed style component.
     */
    w (newVal) {
      this.innerW = newVal;
      this.createStyle();
    }
  },
  created () {
    const self = this;

    /**
     * Accessible references of functions for removing in beforeDestroy
     * @param {number} width - the incoming new width to call updateWidth()
     */
    self.updateWidthHandler = function updateWidthHandler (width) {
      self.updateWidth(width);
    };

    /**
     * Exposing method to shrink/redraw grid via compact() function
     * @param {Object} layout
     */
    self.compactHandler = function compactHandler (layout) {
      self.compact(layout);
    };

    self.setDraggableHandler = function setDraggableHandler (isDraggable) {
      if (self.isDraggable === null) {
        self.draggable = isDraggable;
      }
    };

    self.setResizableHandler = function setResizableHandler (isResizable) {
      if (self.isResizable === null) {
        self.resizable = isResizable;
      }
    };

    self.setRowHeightHandler = function setRowHeightHandler (rowHeight) {
      self.rowHeight = rowHeight;
    };

    self.setColNum = (colNum) => {
      self.cols = parseInt(colNum, 10);
    };
    if (this.eventBus) {
      this.eventBus.$on('updateWidth', self.updateWidthHandler);
      this.eventBus.$on('compact', self.compactHandler);
      this.eventBus.$on('setDraggable', self.setDraggableHandler);
      this.eventBus.$on('setResizable', self.setResizableHandler);
      this.eventBus.$on('setRowHeight', self.setRowHeightHandler);
      this.eventBus.$on('setColNum', self.setColNum);
    }
  },
  beforeDestroy () {
    const self = this;
    // Remove listeners
    this.eventBus.$off('updateWidth', self.updateWidthHandler);
    this.eventBus.$off('compact', self.compactHandler);
    this.eventBus.$off('setDraggable', self.setDraggableHandler);
    this.eventBus.$off('setResizable', self.setResizableHandler);
    this.eventBus.$off('setRowHeight', self.setRowHeightHandler);
    this.eventBus.$off('setColNum', self.setColNum);
  },
  mounted () {
    // @todo - this looks at this.$parent, should pass in?
    this.cols = this.$parent.colNum;
    this.rowHeight = this.$parent.rowHeight;
    this.containerWidth = this.$parent.width !== null ? this.$parent.width : 100;
    this.margin = this.$parent.margin !== undefined ? this.$parent.margin : [10, 10];
    this.maxRows = this.$parent.maxRows;

    if (this.isDraggable === null) {
      this.draggable = this.$parent.isDraggable;
    } else {
      this.draggable = this.isDraggable;
    }

    if (this.isResizable === null) {
      this.resizable = this.$parent.isResizable;
    } else {
      this.resizable = this.isResizable;
    }

    this.useCssTransforms = this.$parent.useCssTransforms;
    this.createStyle();
  },
  methods: {
    /**
     * @name expandItem
     * @description This will expand the modal full screen
     * @param {Number} id - The Facet ID; to pass in to find the content.
     * @todo redisplay the content pieces in the modal slot.
     */
    expandItem (id) {
      this.expanding = true;
      this.isModalVisible = true;
    },
    /**
     * @name showModal
     * @description Shows the Modal.
     * Need to $emit the data up to the GridLayout, so we can open the modal OUTSIDE the grid-layout container.
     */
    showModal () {
      this.expanding = false;
      let modalConfig = {
        isModalVisible: true,
        facetId: this.id,
        model: this.model,
        schema: this.schema
      };
      this.$parent.$emit('openModal', modalConfig);
    },
    /**
     * @name closeModal
     * @description Closes the Modal
     */
    closeModal () {
      this.isModalVisible = false;
    },
    /**
     * @name removeItem
     * @description
     *  Dispatch to the layouts/removeFacets store the facet ID.
     * @since 0.1.11
     * @param {Number} id - the Facet ID; which is the passed in facetId prop.
     */
    removeItem: function (id) {
      store.dispatch('layouts/removeFacet', id);
    },
    /**
     * @name createStyle
     * @description
     *  This creates the positioning style for the facet.
     */
    createStyle () {
      // Checks to see if the facet will size to the right outside the grid.
      if (this.x + this.w > this.cols) {
        this.innerX = 0;
        this.innerW = this.w > this.cols ? this.cols : this.w;
      } else {
        this.innerX = this.x;
        this.innerW = this.w;
      }
      // Calls the `calcPosition` function to return an array of numbers.
      const pos = this.calcPosition(
        this.innerX,
        this.innerY,
        this.innerW,
        this.innerH
      );

      if (this.isDragging) {
        pos.top = this.dragging.top;
        pos.left = this.dragging.left;
      }
      if (this.isResizing) {
        pos.width = this.resizing.width;
        pos.height = this.resizing.height;
      }
      let style;
      // CSS Transforms support (default)
      if (this.useCssTransforms) {
        style = setTransform(pos.top, pos.left, pos.width, pos.height);
      } else {
        // top,left (slow)
        style = setTopLeft(pos.top, pos.left, pos.width, pos.height);
      }
      this.style = style;
    },
    /**
     * @name handleResize
     * @description The Resizing Function that is called when the facet is resized.
     * @param {Object} event - the passed in event.
     */
    handleResize (event) {
      // Get the current drag point from the event. This is used as the offset.
      const position = getControlPosition(event);
      const { x, y } = position;
      const newSize = { width: 0, height: 0 };
      let calcPos;
      let coreEvent;

      switch (event.type) {
        default:
          // for eslint
          break;
        case 'resizestart':
          this.previousW = this.innerW;
          this.previousH = this.innerH;
          calcPos = this.calcPosition(
            this.innerX,
            this.innerY,
            this.innerW,
            this.innerH,
          );
          newSize.width = calcPos.width;
          newSize.height = calcPos.height;
          this.resizing = newSize;
          this.isResizing = true;
          break;
        case 'resizemove':
          coreEvent = createCoreData(this.lastW, this.lastH, x, y);
          newSize.width = this.resizing.width + coreEvent.deltaX;
          newSize.height = this.resizing.height + coreEvent.deltaY;

          this.resizing = newSize;
          break;
        case 'resizeend':
          calcPos = this.calcPosition(
            this.innerX,
            this.innerY,
            this.innerW,
            this.innerH
          );
          newSize.width = calcPos.width;
          newSize.height = calcPos.height;

          this.resizing = null;
          this.isResizing = false;
          break;
      }

      // Get new WH
      const pos = this.calcWH(newSize.height, newSize.width);
      if (pos.w < this.minW) {
        pos.w = this.minW;
      }
      if (pos.w > this.maxW) {
        pos.w = this.maxW;
      }
      if (pos.h < this.minH) {
        pos.h = this.minH;
      }
      if (pos.h > this.maxH) {
        pos.h = this.maxH;
      }

      if (pos.h < 1) {
        pos.h = 1;
      }
      if (pos.w < 1) {
        pos.w = 1;
      }

      this.lastW = x;
      this.lastH = y;

      if (this.innerW !== pos.w || this.innerH !== pos.h) {
        this.$emit(
          'resize',
          this.i,
          pos.h,
          pos.w,
          newSize.height,
          newSize.width
        );
      }
      if (event.type === 'resizeend' && (this.previousW !== this.innerW || this.previousH !== this.innerH)) {
        this.$emit(
          'resized',
          this.i,
          pos.h,
          pos.w,
          newSize.height,
          newSize.width
        );
      }
      this.eventBus.$emit(
        'resizeEvent',
        event.type,
        this.i,
        this.innerX,
        this.innerY,
        pos.h,
        pos.w
      );
    },
    handleDrag (event) {
      let parentRect; // parent bounding box
      let clientRect; // this bounding box
      let coreEvent;

      const position = getControlPosition(event);
      const newPosition = {
        top: 0,
        left: 0
      };
      const { x, y } = position;

      // Quick check to see if it's in middle of resizing
      if (this.isResizing) {
        return;
      }

      switch (event.type) {
        default:
          // for eslint
          break;
        case 'dragstart':
          this.previousX = this.innerX;
          this.previousY = this.innerY;

          parentRect = event.target.offsetParent.getBoundingClientRect();
          clientRect = event.target.getBoundingClientRect();
          newPosition.left = clientRect.left - parentRect.left;
          newPosition.top = clientRect.top - parentRect.top;
          this.dragging = newPosition;
          this.isDragging = true;
          break;
        case 'dragend':
          if (!this.isDragging) return;
          parentRect = event.target.offsetParent.getBoundingClientRect();
          clientRect = event.target.getBoundingClientRect();
          newPosition.left = clientRect.left - parentRect.left;
          newPosition.top = clientRect.top - parentRect.top;
          this.dragging = null;
          this.isDragging = false;
          break;
        case 'dragmove':
          coreEvent = createCoreData(this.lastX, this.lastY, x, y);
          newPosition.left = this.dragging.left + coreEvent.deltaX;
          newPosition.top = this.dragging.top + coreEvent.deltaY;
          this.dragging = newPosition;
          break;
      }

      const pos = this.calcXY(newPosition.top, newPosition.left);

      this.lastX = x;
      this.lastY = y;

      if (this.innerX !== pos.x || this.innerY !== pos.y) {
        this.$emit('move', this.i, pos.x, pos.y);
      }
      if (event.type === 'dragend' && (this.previousX !== this.innerX || this.previousY !== this.innerY)) {
        this.$emit('moved', this.i, pos.x, pos.y);
      }
      this.eventBus.$emit(
        'dragEvent',
        event.type,
        this.i,
        pos.x,
        pos.y,
        this.innerH,
        this.innerW
      );
    },
    /**
     * @name calcPosition
     * @description Calculates the position of the facet
     * @param {Number} x - the X Position
     * @param {Number} y - the Y Position
     * @param {Number} w - the Number of Columns across the Layout the facet goes.
     * @param {Number} h - the Number of Rows across the Layout the facet goes.
     * @returns {Object} - The {left, top, width, height } Object
     */
    calcPosition (x, y, w, h) {
      const colWidth = this.calcColWidth();
      const out = {
        left: Math.round((colWidth * x) + ((x + 1) * this.margin[0])),
        top: Math.round((this.rowHeight * y) + ((y + 1) * this.margin[1])),
        // 0 * Infinity === NaN, which causes problems with resize constriants;
        // Fix this if it occurs.
        // Note we do it here rather than later because Math.round(Infinity) causes deopt
        width: w === Infinity ? w : Math.round((colWidth * w) + (Math.max(0, w - 1) * this.margin[0])),
        height: h === Infinity ? h : Math.round((this.rowHeight * h) + (Math.max(0, h - 1) * this.margin[1]))
      };
      return out;
    },
    /**
     * Translate x and y coordinates from pixels to grid units.
     * @param  {Number} top  Top position (relative to parent) in pixels.
     * @param  {Number} left Left position (relative to parent) in pixels.
     * @return {Object} x and y in grid units.
     */
    calcXY (top, left) {
      const colWidth = this.calcColWidth();

      let x = Math.round((left - this.margin[0]) / (colWidth + this.margin[0]));
      let y = Math.round((top - this.margin[1]) / (this.rowHeight + this.margin[1]));

      // Capping
      x = Math.max(Math.min(x, this.cols - this.innerW), 0);
      y = Math.max(Math.min(y, this.maxRows - this.innerH), 0);

      return { x, y };
    },
    /**
     * Generates column width.
     * - ContainerWidth - margin, * cols+1 / cols
     * @returns {Number}
     */
    calcColWidth () {
      let newWidth = (this.containerWidth - (this.margin[0] * (this.cols + 1))) / this.cols;
      if (this.containerWidth > 50) {
        store.dispatch('layouts/updateColumnWidth', newWidth);
        return newWidth;
      }
    },

    /**
     * Given a height and width in pixel values, calculate grid units.
     * @param  {Number} height Height in pixels.
     * @param  {Number} width  Width in pixels.
     * @return {Object} w, h as grid units.
     */
    calcWH (height, width) {
      const colWidth = this.calcColWidth();

      let w = Math.round((width + this.margin[0]) / (colWidth + this.margin[0]));
      let h = Math.round((height + this.margin[1]) / (this.rowHeight + this.margin[1]));

      // Capping
      w = Math.max(Math.min(w, this.cols - this.innerX), 0);
      h = Math.max(Math.min(h, this.maxRows - this.innerY), 0);
      return { w, h };
    },
    updateWidth (width, colNum) {
      this.containerWidth = width;
      if (colNum !== undefined && colNum !== null) {
        this.cols = colNum;
      }
    },
    /**
     * Method to shrink vertical facets; updating the grid.
     */
    compact () {
      this.createStyle();
    }
  }
};
</script>
<style type="text/css">
  .vue-grid-item {
    background: rgb(249, 255, 245);
  }
  .vue-grid-item:not(.vue-grid-placeholder) {
    border: 1px solid #5f442d;
  }

  .vue-grid-item.resizing {
    opacity: 0.9;
  }

  .vue-grid-item.static {
    background: #cce;
  }

  .vue-grid-item .no-drag {
    height: 100%;
    width: 100%;
  }

  .vue-grid-item .minMax {
    font-size: 12px;
  }

  .vue-grid-item .add {
    cursor: pointer;
  }

  .vue-grid-item {
    transition: all 200ms ease;
    transition-property: left, top, right;
  }

  .vue-grid-item.cssTransforms {
    transition-property: transform;
    left: 0;
    right: auto;
  }

  .vue-grid-item.resizing {
    opacity: 0.6;
    z-index: 3;
  }

  .vue-grid-item.vue-draggable-dragging {
    transition: none;
    z-index: 3;
  }

  .vue-grid-item .columns {
    margin-left: 0;
    margin-right: 0;
    margin-top: 0;
  }

  .vue-grid-item .columns:last-child {
    margin-bottom: 0;
  }

  .vue-grid-item.vue-grid-placeholder {
    background: red;
    opacity: 0.2;
    transition-duration: 100ms;
    z-index: 2;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    -o-user-select: none;
    user-select: none;
  }

  .vue-grid-item > .vue-resizable-handle {
    position: absolute;
    width: 20px;
    height: 20px;
    bottom: 0;
    right: 0;
    background-position: bottom right;
    padding: 0 3px 3px 0;
    background-repeat: no-repeat;
    background-origin: content-box;
    box-sizing: border-box;
    cursor: se-resize;
  }

  .vue-grid-item.disable-userselect {
    user-select: none;
  }

  .grid-item-header {
    padding: 5px 10px;
  }

  .grid-item-header .facet-control {
    margin-left: 5px;
    cursor: pointer;
  }

  .grid-item-header .facet-control-move {
    cursor: move;
  }
</style>
