<template>
  <div>

    <div
      ref="item"
      :style="mergedStyle"
      class="vue-grid-layout">

      <div
        v-if="$slots['grid-header']"
        class="vue-grid-header">
        <!-- @slot Grid-Header bar for GridItem -->
        <slot name="grid-header" />
      </div>

      <!-- default slot for main layout, no name needed -->
      <slot />

    </div>
    <modal
      @close="closeModal"
      v-show="isModalVisible"
      v-if="modalConfig.model && modalConfig.schema"
      :is-modal-visible="isModalVisible"
      :facet-id="modalConfig.facetId"
      :expanding="modalConfig.expanding"
      :model="modalConfig.model"
      :schema="modalConfig.schema" />
  </div>
</template>
<script>
import Vue from 'vue';

import {
  bottom,
  compact,
  getLayoutItem,
  moveElement,
  validateLayout
} from '../utils/utils';

import GridItem from './GridItem.vue';
import Modal from './Modal';
import store from '../store';

// For smoother scrolling, 3rd party lib.
const elementResizeDetectorMaker = require('element-resize-detector');

export default {
  name: 'GridLayout',
  provide () {
    return {
      eventBus: null
    };
  },
  components: {
    GridItem,
    Modal
  },
  props: {
    /**
     * If true, the container height swells and contracts to fit contents
     */
    autoSize: {
      type: Boolean,
      default: true
    },
    /**
     * Arbitrary number that is divided by viewport width, and that's the column.
     */
    colNum: {
      type: Number,
      default: 12
    },
    /**
     * Pixel height of each row.
     */
    rowHeight: {
      type: Number,
      default: 150
    },
    /**
     * Maximum Number of Rows
     * Default to Infinity
     */
    maxRows: {
      type: Number,
      default: Infinity
    },
    /**
     * Array that is the overall margins for the main layout object.
     */
    margin: {
      type: Array,
      default () {
        return [10, 10];
      }
    },
    /**
     * Boolean if all facets are draggable.
     */
    isDraggable: {
      type: Boolean,
      default: true
    },
    /**
     * Boolean if all facets are resizable.
     */
    isResizable: {
      type: Boolean,
      default: true
    },
    /**
     * todo remove this?
     * Figure out how this is used.
     */
    useCssTransforms: {
      type: Boolean,
      default: true
    },
    /**
     * todo remove this - move into store?
     */
    verticalCompact: {
      type: Boolean,
      default: true
    },
    /**
     * The structure and content of the layout grid.
     *
     * Array of Objects that contain the facets; and the layout items for each facet.
     */
    layout: {
      type: Array,
      required: true
    }
  },
  data () {
    return {
      isModalVisible: false,
      modalConfig: {
        model: null,
        schema: null
      },
      width: null,
      mergedStyle: {},
      lastLayoutLength: 0,
      isDragging: false,
      placeholder: {
        x: 0,
        y: 0,
        w: 0,
        h: 0,
        i: -1
      }
    };
  },
  watch: {
    /**
     * When width changes the eventBus $emits updateWidth with new Width, then updates Height.
     * @event updateWidth - Event with new Width
     */
    width () {
      this.$nextTick(function updateWidth () {
        this.eventBus.$emit('updateWidth', this.width);
        this.updateHeight();
      });
    },
    layout () {
      this.layoutUpdate();
    },
    /**
     * When Column Numbers Change it emits the new Column Number
     * @event setColNum
     */
    colNum (val) {
      this.eventBus.$emit('setColNum', val);
    },
    /**
     * When Row Height changes it emits the new Row Height Number
     * @event setRowHeight
     */
    rowHeight () {
      this.eventBus.$emit('setRowHeight', this.rowHeight);
    },
    /**
     * If you change the draggable aspect this will emit the new boolean
     * @event setDraggable
     */
    isDraggable () {
      this.eventBus.$emit('setDraggable', this.isDraggable);
    },
    /**
     * If you change the resizable aspect this will emit the new boolean
     * @event setResizable
     */
    isResizable () {
      this.eventBus.$emit('setResizable', this.isResizable);
    }
  },
  created () {
    const self = this;

    this.$on('openModal', function (modalConfig) {
      this.modalConfig = modalConfig;
      this.isModalVisible = true;
    })

    this.$on('close', function () {
      this.isModalVisible = false;
    })

    /**
     * Accessible references of functions for removing in beforeDestroy
     * @todo This should be passed in as a prop; and avoid all the events spawning.
     */
    self.resizeEventHandler = function resizeEventHandler (eventType, id, x, y, h, w) {
      self.resizeEvent(eventType, id, x, y, h, w);
    };
    /**
     * @todo This should be passed in as a prop; and avoid all the events spawning.
     */
    self.dragEventHandler = function dragEventHandler (eventType, id, x, y, h, w) {
      self.dragEvent(eventType, id, x, y, h, w);
    };

    /**
     * Creating separate Vue for EventBus holding.
     * Attaches to root, used as bypass for all internal events.
     * @event resizeEvent
     * @event dragEvent
     */
    self._provided.eventBus = new Vue();
    self.eventBus = self._provided.eventBus;
    self.eventBus.$on('resizeEvent', self.resizeEventHandler);
    self.eventBus.$on('dragEvent', self.dragEventHandler);
  },
  beforeDestroy () {
    const self = this;
    // Remove listeners
    this.eventBus.$off('resizeEvent', self.resizeEventHandler);
    this.eventBus.$off('dragEvent', self.dragEventHandler);
    window.removeEventListener('resize', self.onWindowResize);
  },
  mounted () {
    this.$nextTick(function mounted () {
      const self = this;
      validateLayout(this.layout);
      this.$nextTick(() => {
        if (self.width === null) {
          self.onWindowResize();
          window.addEventListener('resize', self.onWindowResize);
        }
        compact(self.layout, self.verticalCompact);

        self.updateHeight();
        self.$nextTick(() => {
          // <- For ultra performance.
          const erd = elementResizeDetectorMaker({
            strategy: 'scroll'
          });
          erd.listenTo(self.$refs.item, () => {
            self.onWindowResize();
          });
        });
      });
      window.onload = function onload () {
        if (self.width === null) {
          self.onWindowResize();
          window.addEventListener('resize', self.onWindowResize);
        }
        compact(self.layout, self.verticalCompact);

        self.updateHeight();
        self.$nextTick(() => {
          // <- For ultra performance.
          const erd = elementResizeDetectorMaker({
            strategy: 'scroll'
          });
          erd.listenTo(self.$refs.item, () => {
            self.onWindowResize();
          });
        });
      };
    });
  },
  methods: {
    /**
     * @name closeModal
     * @description Closes the Modal
     */
    closeModal () {
      this.isModalVisible = false;
    },
    /*
     * This catches the event being passed up.
     *
     * This is done so the whole layout object can be modified
     * (as resizing the facet can force changes to other facets)
     *
     * @todo investigate moving layout into prop; and having facet or STORE do
     * the proper mutation.
     */
    layoutUpdate () {
      if (this.layout !== undefined) {
        if (this.layout.length !== this.lastLayoutLength) {
          this.lastLayoutLength = this.layout.length;
        }
        /**
         * @todo check on this; this vertically compacts the layout,
         * then re-emits the width; and updates the height.
         */
        compact(this.layout, this.verticalCompact);
        this.eventBus.$emit('updateWidth', this.width);
        this.updateHeight();
      }
    },
    /**
     * This updates the 'mergedStyle' variable and updates the height.
     */
    updateHeight () {
      this.mergedStyle = {
        height: this.containerHeight()
      };
    },
    /**
     * Checks this.$refs; which is the HTML element in the DOM.
     *
     * If it's rendered it gives the offsetWidth.
     */
    onWindowResize () {
      if (
        this.$refs !== null &&
          this.$refs.item !== null &&
          this.$refs.item !== undefined
      ) {
        this.width = this.$refs.item.offsetWidth;
      }
    },
    /**
     * This sets the css style based on the layout, rowHeight, margin.
     * @returns String
     */
    containerHeight () {
      return (`${bottom(this.layout) * (this.rowHeight + (this.margin[1]) + this.margin[1])}px`);
    },
    /**
     * dragEvent
     */
    dragEvent (eventName, id, x, y, h, w) {
      if (eventName === 'dragmove' || eventName === 'dragstart') {
        this.placeholder.id = id;
        this.placeholder.x = x;
        this.placeholder.y = y;
        this.placeholder.w = w;
        this.placeholder.h = h;
        this.$nextTick(function isDragging () {
          this.isDragging = true;
        });
        this.eventBus.$emit('updateWidth', this.width);
      } else {
        this.$nextTick(function isDragging () {
          this.isDragging = false;
        });
      }
      let l = getLayoutItem(this.layout, id);
      // GetLayoutItem sometimes returns null object
      if (l === undefined || l === null) {
        l = { x: 0, y: 0 };
      }
      l.x = x;
      l.y = y;
      // Move the element to the dragged location.
      this.layout = moveElement(this.layout, l, x, y, true);
      compact(this.layout, this.verticalCompact);
      // needed because vue can't detect changes on array element properties
      this.eventBus.$emit('compact');
      this.updateHeight();
      // if (eventName === 'dragend') this.$emit('layout-updated', this.layout);
      store.dispatch('layouts/updateFacet', this.layout);
    },
    /**
     * resizeEvent
     */
    resizeEvent (eventName, id, x, y, h, w) {
      if (eventName === 'resizestart' || eventName === 'resizemove') {
        this.placeholder.id = id;
        this.placeholder.i = id;
        this.placeholder.x = x;
        this.placeholder.y = y;
        this.placeholder.w = w;
        this.placeholder.h = h;
        this.$nextTick(function isDragging () {
          this.isDragging = true;
        });
        this.eventBus.$emit('updateWidth', this.width);
      } else {
        this.$nextTick(function isDragging () {
          this.isDragging = false;
        });
      }
      let l = getLayoutItem(this.layout, id);
      // GetLayoutItem sometimes return null object
      if (l === undefined || l === null) {
        l = { h: 0, w: 0 };
      }
      l.h = h;
      l.w = w;
      compact(this.layout, this.verticalCompact);
      this.eventBus.$emit('compact');
      this.updateHeight();
      if (eventName === 'resizeend') this.$emit('layout-updated', this.layout);

      // Passing modified facet to store.
      const updatedFacet = this.layout.find(facet => facet.id === parseInt(id, 10));
      store.dispatch('layouts/updateFacet', updatedFacet);
    }
  }
};
</script>
<style>
  @import "/docs/bulma.css";

  .vue-resizable-handle {
    z-index: 5000;
    position: absolute;
    width: 20px !important;
    height: 20px !important;
    bottom: 0;
    right: 0;
    /* background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4=');
    */
    background: url('/docs/resize.png');
    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-draggable-handle {
    position: absolute;
    width: 20px;
    height: 20px;
    top: 0;
    left: 0;
    background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><circle cx='5' cy='5' r='5' fill='#999999'/></svg>") no-repeat;
    background-position: bottom right;
    padding: 0 8px 8px 0;
    background-repeat: no-repeat;
    background-origin: content-box;
    box-sizing: border-box;
    cursor: pointer;
  }

  .columns {
    -moz-columns: 120px;
    -webkit-columns: 120px;
    columns: 120px;
  }

  .vue-grid-layout {
    background: rgba(252, 225, 214, 0.5);
    position: relative;
    transition: height 200ms ease;
  }

  fieldset {
    border: 0;
  }
</style>
