<template>
  <div class="flowchart-container"
       @mousemove="handleMove"
       @mouseup="handleUp"
       @mousedown="handleDown">

    <svg width="100%" height="100%">
      <flowchart-link v-bind.sync="link"
                      v-for="(link, index) in lines"
                      :key="`link${index}`"
                      @deleteLink="linkDelete(link.id)">
      </flowchart-link>
    </svg>

    <flowchart-node v-bind.sync="node"
                    v-for="(node, index) in scene.nodes"
                    :key="`node${index}`"
                    :options="nodeOptions"
                    @linkingStart="linkingStart(node.id)"
                    @linkingStop="linkingStop(node.id)"
                    @nodeSelected="nodeSelected(node.id, $event)"
                    @dataUpdated="dataUpdated(node.id, $event)"
    >

    </flowchart-node>

  </div>
</template>

<script>
import FlowchartLink from './FlowchartLink.vue';
import FlowchartNode from './FlowchartNode.vue';
import {getMousePosition} from '../assets/utilty/position';

export default {
  name: 'VueFlowchart',
  props: {
    scene: {
      type: Object,
      default() {
        return {
          centerX: 0,
          scale: 1,
          centerY: 0,
          nodes: [],
          links: [],
        }
      }
    }
  },
  data() {
    return {
      action: {
        linking: false,
        dragging: false,
        scrolling: false,
        selected: 0,
      },
      mouse: {
        x: 0,
        y: 0,
        lastX: 0,
        lastY: 0,
      },
      draggingLink: null,
      rootDivOffset: {
        top: 0,
        left: 0
      },
      dragOrigin: {
        x: 0,
        y: 0
      }
    };
  },
  components: {
    FlowchartLink,
    FlowchartNode,
  },
  computed: {
    nodeOptions() {
      return {
        centerY: this.scene.centerY,
        centerX: this.scene.centerX,
        scale: this.scene.scale,
        offsetTop: this.rootDivOffset.top,
        offsetLeft: this.rootDivOffset.left,
        selected: this.action.selected,
      }
    },
    lines() {
      const lines = this.scene.links.map((link) => {
        const fromNode = this.findNodeWithID(link.from)
        const toNode = this.findNodeWithID(link.to)
        let x, y, cy, cx, ex, ey;
        x = this.scene.centerX + fromNode.x;
        y = this.scene.centerY + fromNode.y;
        [cx, cy] = this.getPortPosition('bottom', x, y);
        x = this.scene.centerX + toNode.x;
        y = this.scene.centerY + toNode.y;
        [ex, ey] = this.getPortPosition('top', x, y);
        return {
          start: [cx, cy],
          end: [ex, ey],
          id: link.id,
        };
      })
      if (this.draggingLink) {
        let x, y, cy, cx;
        const fromNode = this.findNodeWithID(this.draggingLink.from)
        x = this.scene.centerX + fromNode.x;
        y = this.scene.centerY + fromNode.y;
        [cx, cy] = this.getPortPosition('bottom', x, y);
        // push temp dragging link, mouse cursor postion = link end postion 
        lines.push({
          start: [cx, cy],
          end: [this.draggingLink.mx, this.draggingLink.my],
        })
      }
      return lines;
    }
  },
  mounted() {
    this.rootDivOffset.top = this.$el ? this.$el.offsetTop : 0;
    this.rootDivOffset.left = this.$el ? this.$el.offsetLeft : 0;
  },
  methods: {
    findNodeWithID(id) {
      return this.scene.nodes.find((item) => {
        return id === item.id
      })
    },
    getPortPosition(type, x, y) {
      // Line start and end points set here
      if (type === 'top') {
        return [x + 100, y];
      } else if (type === 'bottom') {
        return [x + 100, y + 150];
      }
    },
    linkingStart(index) {
      this.action.linking = true;
      // Dirty fix, so that links won't glitch when being created
      const node = this.findNodeWithID(index);
      const [x, y] = this.getPortPosition('bottom', node.x + this.scene.centerX, node.y + this.scene.centerY);
      this.draggingLink = {
        from: index,
        mx: x, // Yes, those are magic numbers
        my: y,
      };
    },
    linkingStop(index) {
      // add new Link
      if (this.draggingLink && this.draggingLink.from !== index) {
        // check link existence
        const existed = this.scene.links.find((link) => {
          return link.from === this.draggingLink.from && link.to === index;
        })
        // check for conflicting
        const conflicting = this.scene.links.find((link) => { // This makes sure that only one link can go towards a node
          return link.to === index;
        })
        if (!existed && !conflicting) {
          let maxID = Math.max(0, ...this.scene.links.map((link) => {
            return link.id
          }))
          const newLink = {
            id: maxID + 1,
            from: this.draggingLink.from,
            to: index,
          };
          this.scene.links.push(newLink)
          this.$emit('linkAdded', newLink)
        }
      }
      this.draggingLink = null
    },
    linkDelete(id) {
      const deletedLink = this.scene.links.find((item) => {
        return item.id === id;
      });
      if (deletedLink) {
        this.scene.links = this.scene.links.filter((item) => {
          return item.id !== id;
        });
        this.$emit('linkBreak', deletedLink);
      }
    },
    nodeSelected(id, e) {
      this.action.dragging = id;

      const node = this.scene.nodes.find(node => node.id === id);
      this.dragOrigin.x = node.x;
      this.dragOrigin.y = node.y;

      this.action.selected = id;
      this.$emit('nodeClick', id);
      this.mouse.lastX = e.pageX || e.clientX + document.documentElement.scrollLeft
      this.mouse.lastY = e.pageY || e.clientY + document.documentElement.scrollTop
    },
    handleMove(e) {
      if (this.action.linking) {
        [this.mouse.x, this.mouse.y] = getMousePosition(this.$el, e);
        [this.draggingLink.mx, this.draggingLink.my] = [this.mouse.x, this.mouse.y];
      }
      if (this.action.dragging) {
        this.mouse.x = e.pageX || e.clientX + document.documentElement.scrollLeft
        this.mouse.y = e.pageY || e.clientY + document.documentElement.scrollTop
        let diffX = this.mouse.x - this.mouse.lastX;
        let diffY = this.mouse.y - this.mouse.lastY;

        this.mouse.lastX = this.mouse.x;
        this.mouse.lastY = this.mouse.y;
        this.moveSelectedNode(diffX, diffY);
      }
      if (this.action.scrolling) {
        [this.mouse.x, this.mouse.y] = getMousePosition(this.$el, e);
        let diffX = this.mouse.x - this.mouse.lastX;
        let diffY = this.mouse.y - this.mouse.lastY;

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

        this.scene.centerX += diffX;
        this.scene.centerY += diffY;

        // this.hasDragged = true
      }
    },
    handleUp(e) {
      const target = e.target || e.srcElement;
      if (this.$el.contains(target)) {
        if (typeof target.className !== 'string' || target.className.indexOf('node-input') < 0) {
          this.draggingLink = null;
        }
        if (typeof target.className === 'string' && target.className.indexOf('node-delete') > -1) {
          this.nodeDelete(this.action.dragging);
          this.action.dragging = null;
        }
      }

      if (this.action.dragging) {
        const node = this.scene.nodes.find(node => node.id === this.action.dragging);
        const moved = (this.dragOrigin.x !== node.x) || (this.dragOrigin.y !== node.y)
        if (moved) {
          this.$emit("nodeMoved", this.action.dragging);
        }
        this.action.dragging = null;
      }

      this.action.linking = false;
      this.action.scrolling = false;
    },
    handleDown(e) {
      const target = e.target || e.srcElement;
      // console.log('for scroll', target, e.keyCode, e.which)
      if ((target === this.$el || target.matches('svg, svg *')) && e.which === 1) {
        this.action.scrolling = true;
        [this.mouse.lastX, this.mouse.lastY] = getMousePosition(this.$el, e);
        this.action.selected = null; // deselectAll
      }
      this.$emit('canvasClick', e);
    },
    moveSelectedNode(dx, dy) {
      let index = this.scene.nodes.findIndex((item) => {
        return item.id === this.action.dragging
      });
      let left = this.scene.nodes[index].x + dx / this.scene.scale;
      let top = this.scene.nodes[index].y + dy / this.scene.scale;
      this.$set(this.scene.nodes, index, Object.assign(this.scene.nodes[index], {
        x: left,
        y: top,
      }));
    },
    nodeDelete(id) {
      const deleted_node = this.scene.nodes.find((node) => node.id === id);
      console.log(deleted_node);

      this.scene.nodes = this.scene.nodes.filter((node) => {
        return node.id !== id;
      })

      this.scene.links = this.scene.links.filter((link) => {
        return link.from !== id && link.to !== id
      });
      this.$emit('nodeDelete', deleted_node);
    },
    dataUpdated(id, newData) {
      this.$emit('nodeDataEdited', id, newData);
    }
  },
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.flowchart-container {
  margin: 0;
  background: #f3f3f3;
  position: relative;
  overflow: hidden;
  height: 100%;

  svg {
    cursor: grab;
  }
}
</style>
