<template>
  <div class="flow-container">
    <svg
      xmlns="http://www.w3.org/2000/svg"
      xmlns:xlink="http://www.w3.org/1999/xlink"
      ref="svg"
      class="flow-view view"
      :class="{
        'flow-linking':linking || stickySockets,
        'activity':dragging || pointerLink.active ,
        'flow-detail': detailed
      }"
      @dragover.prevent
      @drop="managerDrop"
      :width="width"
      :height="height">
      <svg-defs/>

      <flow-pan-zoom
        ref="panzoom"
        v-model="panzoom">

        <!-- links below nodes -->
        <flow-link
          v-for="(link,i) in nodeData.links"
          :key="i"
          v-bind="linkProps(link)"
          @click="linkRemove(link)"
        />
        <!-- nodes -->
        <flow-node
          ref="nodes"
          v-for="(n,i) of nodeData.nodes"
          v-bind="nodeProps(n)"
          :key="'node' + n.id"
          :id="n.id"
          @nodePointerDown.prevent="nodeDragStart($event,i)"
          @socketPointerDown="socketPointerDown(n.id,...arguments)"
          @nodeDoubleClick="$emit('nodeInspect',n.id)"
        />
        <!-- mouse link-->
        <flow-link
          :pointer="true"
          v-if="pointerLink.active"
          v-bind="pointerLink.props"
        />

      </flow-pan-zoom>
    </svg>
    <div class="flow-container__control">
      <button @click="$emit('funcsPanelToggle')">Panel</button>
      <button @click="stickySockets=!stickySockets"> {{ stickySockets? 'Hide':'Show' }} sockets </button>
      <button @click="detailed=!detailed"> {{ detailed? 'Hide':'Show' }} detail </button>
      <button v-if="panzoom.x!=0 || panzoom.y!=0 || panzoom.zoom!=1" @click="panzoomReset">Reset view</button>
    </div>
    <div class="flow-container__info">
      x:{{ panzoom.x.toFixed(2) }} y:{{ panzoom.y.toFixed(2) }} scale:{{ panzoom.zoom.toFixed(2) }}
      nodes: {{ nodeData.nodes.length }}
      links: {{ nodeData.links.length }}
    </div>

  </div>
</template>
<script>
import FlowNode from './node'
import FlowLink from './link'
import FlowPanZoom from './panzoom'
import SvgDefs from './svgdefwrapper.vue'

export default {
  name: 'FlowManager',
  components: {FlowNode, FlowLink, FlowPanZoom, SvgDefs},
  props: {
    'value': {type: Object, default: () => {}},
    'registry': {type: Object, default: () => {}},
    'width': {type: String, default: '800px'},
    'height': {type: String, default: '600px'}
  },
  data () {
    // const cloned = JSON.parse(JSON.stringify(this.value)) // initial?
    return {
      panzoom: { x: 0, y: 0, zoom: 1 },
      nodeData: { nodes: [], links: [] },
      dragging: null,

      linking: false,
      stickySockets: false,
      detailed: false,
      pointerLink: {active: false, props: {}, src: {}}
    }
  },
  computed: {
    nodeProps () {
      return (node) => {
        let match = {}
        if (this.pointerLink.active && this.pointerLink.src.nodeId !== node.id) {
          if (this.pointerLink.src.in !== undefined) {
            match = {out: this.pointerLink.src.type}
          } else {
            match = {in: this.pointerLink.src.type}
          }
        }
        const nodeClass = this.registry[node.src]
        return {
          transform: `translate(${node.x} ${node.y})`,
          id: node.id,
          label: node.label,
          inputs: nodeClass.inputs,
          output: nodeClass.output,
          match: match,
          dragging: this.dragging === node,
          nodeStyle: nodeClass.style
        }
      }
    },
    linkProps () {
      return (link) => {
        if (!this.$refs.nodes) return
        // For size .x .y
        const nodeFrom = this.nodeData.nodes.find(n => n.id === link.from)
        const nodeTo = this.nodeData.nodes.find(n => n.id === link.to)

        const refFrom = this.$refs.nodes.find(n => n.id === link.from)
        const refTo = this.$refs.nodes.find(n => n.id === link.to)

        const fromOutput = refFrom.outputPos(0) // only 1 output
        const toInput = refTo.inputPos(link.in)

        return {
          x1: nodeFrom.x + fromOutput.x,
          y1: nodeFrom.y + fromOutput.y,
          x2: nodeTo.x + toInput.x,
          y2: nodeTo.y + toInput.y
        }
      }
    }
  },
  watch: {
    value: {
      handler (val) {
        const clone = JSON.parse(JSON.stringify(this.value)) // deepClone
        const nodes = []
        for (let n of clone.nodes) {
          if (this.dragging && this.dragging.id === n.id) {
            continue
          }
          nodes.push(n)
        }
        if (this.dragging) {
          nodes.push(this.dragging)
        }
        this.nodeData = {
          nodes: nodes,
          links: clone.links
        }
        this.$nextTick(() => {
          this.$forceUpdate()
        })
      },
      deep: true
    }
  },
  mounted () {
    this.$nextTick(() => {
      this.$forceUpdate()
    })
    document.addEventListener('keydown', this.keyDown)
    document.addEventListener('keyup', this.keyUp)
  },
  beforeDestroy () {
    document.removeEventListener('keydown', this.keyDown)
    document.removeEventListener('keyup', this.keyUp)
  },
  methods: {
    keyDown (ev) {
      if (ev.shiftKey) {
        this.linking = true
      }
    },
    keyUp (ev) {
      if (!ev.shiftKey) {
        this.linking = false
      }
    },
    panzoomReset () {
      this.panzoom.x = 0
      this.panzoom.y = 0
      this.panzoom.zoom = 1
    },
    // XXX: Shrink this function
    // and create some LinkAdd method
    socketPointerDown (nodeId, ev, socket) {
      if (ev.button !== 0) return
      const nodeRef = this.$refs.nodes.find(n => n.id === nodeId)
      const node = this.nodeData.nodes.find(n => n.id === nodeId)

      const isInput = socket.in !== undefined
      const socketPos = isInput ? nodeRef.inputPos(socket.in) : nodeRef.outputPos(socket.out)

      // const p = this.transformedPoint(ev.clientX, ev.clientY)
      this.pointerLink.props = {
        x1: node.x + socketPos.x,
        y1: node.y + socketPos.y,
        x2: node.x + socketPos.x,
        y2: node.y + socketPos.y
      }

      this.pointerLink.active = true
      if (isInput) {
        this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].inputs[socket.in], in: socket.in}
      } else {
        this.pointerLink.src = {nodeId: nodeId, type: this.registry[node.src].output, out: 0}
      }

      // What socket is this
      // Create a temporary link
      const drag = (ev) => {
        const p = this.transformedPoint(ev.clientX, ev.clientY)
        if (isInput) {
          this.pointerLink.props.x1 = p.x
          this.pointerLink.props.y1 = p.y
        } else {
          this.pointerLink.props.x2 = p.x
          this.pointerLink.props.y2 = p.y
        }
      }
      const drop = (ev) => {
        document.removeEventListener('mousemove', drag)
        document.removeEventListener('mouseup', drop)
        this.pointerLink.active = false

        const targetNodeId = ev.target.getAttribute('data-nodeid')
        const targetIn = ev.target.getAttribute('data-in')
        const targetOut = ev.target.getAttribute('data-out')

        // Not a node or same node
        if (targetNodeId === undefined || targetNodeId === nodeId) {
          console.error('LINK: target is not a socket or is node')
          return
        }
        let link
        // target is input
        if (targetIn && !isInput) {
          link = {
            from: nodeId,
            to: targetNodeId,
            in: parseInt(targetIn)
          }
        } else if (targetOut && isInput) {
          link = {
            from: targetNodeId,
            to: nodeId,
            in: socket.in
          }
        }
        // No link
        if (!link) {
          console.error('LINK: input same direction (in->in/out->out)')
          return
        }
        const nodeFrom = this.nodeData.nodes.find(n => n.id === link.from)
        const nodeTo = this.nodeData.nodes.find(n => n.id === link.to)

        const output = this.registry[nodeFrom.src].output
        const input = this.registry[nodeTo.src].inputs[link.in]
        // Type checking
        if (!(output === 'any' || output == input || input === 'any')) {
          console.error('LINK: Invalid type')
          return
        }

        // Input already exists
        const existingInputI = this.nodeData.links.findIndex(l => l.to === link.to && l.in === link.in)
        if (existingInputI !== -1) {
          this.nodeData.links.splice(existingInputI, 1)
          // console.error('LINK: already has input')
          // return
        }
        this.linkAdd(link)
      }
      document.addEventListener('mousemove', drag)
      document.addEventListener('mouseup', drop)
    },
    nodeDoubleClick (ev, i) {
      this.nodeModalTarget = i
      this.nodeModal = true
    },
    nodeDragStart (ev, i) {
      document.activeElement && document.activeElement.blur()
      var tnode = this.nodeData.nodes[i]
      if (ev.button === 1) {
        // remove related links
        this.nodeData.links = this.nodeData.links.filter(l => l.from !== tnode.id && l.to !== tnode.id)
        this.nodeData.nodes.splice(i, 1)
        this.$emit('input', this.nodeData)
        this.$emit('nodeRemove', tnode)
        this.$emit('shouldPersist')
        return
      }
      if (ev.button !== 0) return // first button
      if (ev.shiftKey) {
        this.socketPointerDown(tnode.id, ev, {out: 0})
        return
      }
      // we can handle with nodeId and a search
      this.nodeData.nodes.splice(i, 1)
      this.nodeData.nodes.push(tnode) // put in last

      this.dragging = tnode
      // transform CTM
      const delta = this.transformedPoint(ev.clientX, ev.clientY)
      delta.x -= tnode.x
      delta.y -= tnode.y

      const drag = (ev) => {
        const point = this.transformedPoint(ev.clientX, ev.clientY)
        tnode.x = point.x - delta.x
        tnode.y = point.y - delta.y
        // Bad possibly
        this.$emit('input', this.nodeData)
        this.$emit('nodeUpdate', tnode)
      }
      const drop = (ev) => {
        document.removeEventListener('mousemove', drag)
        document.removeEventListener('mouseup', drop)
        this.dragging = null

        this.$emit('input', this.nodeData)
        this.$emit('nodeUpdate', tnode)
        this.$emit('shouldPersist')
      }
      document.addEventListener('mousemove', drag)
      document.addEventListener('mouseup', drop)
    },
    nodeAdd (src, x = 100, y = 100) {
      const newNode = {
        id: guid(),
        x: x,
        y: y,
        label: src,
        src: src
      }
      this.nodeData.nodes.push(newNode)
      this.$emit('input', this.nodeData)
      this.$emit('nodeUpdate', newNode)
      this.$emit('shouldPersist')
    },
    linkAdd (link) {
      this.nodeData.links.push(link)
      this.$emit('input', this.nodeData)
      this.$emit('linkUpdate', link)
      this.$emit('shouldPersist')
    },
    linkRemove (link) {
      const i = this.nodeData.links.findIndex(l => l === link)
      if (i === -1) return

      this.nodeData.links.splice(i, 1)
      this.$emit('input', this.nodeData)
      this.$emit('linkRemove', link)
      this.$emit('shouldPersist')
    },
    managerDrop (ev) {
      ev.preventDefault()
      const reg = ev.dataTransfer.getData('text')
      if (this.registry[reg] === undefined) {
        console.error('Registry: Drop src not found in registry')
        return
      }

      const pt = this.transformedPoint(ev.x, ev.y)
      this.nodeAdd(reg, pt.x, pt.y)
    },
    // HELPERS depending on svg ref
    createSVGPoint (x, y) {
      const p = this.$refs.svg.createSVGPoint()
      p.x = x; p.y = y
      return p
    },
    transformedPoint (x, y, abs) {
      const svgRect = this.$refs.svg.getBoundingClientRect()
      if (!abs) {
        x -= svgRect.left
        y -= svgRect.top
      }
      return this.$refs.panzoom.transformedPoint(this.createSVGPoint(x, y))
    }
  }
}
// utils
function guid () {
  function s4 () {
    return Math.floor((1 + Math.random()) * 0x10000)
      .toString(16)
      .substring(1)
  }
  return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
    s4() + '-' + s4() + s4() + s4()
}
</script>
<style>
.flow-container {
  display:flex;
  flex-flow:row;
  position:relative;
}

.flow-container__control {
  position:absolute;
  top: 20px;
  left: 20px;
  display:flex;
  justify-content: center;
  align-items: center;
  color: #FFF;
}
.flow-container__control button {
  display:flex;
  justify-content: center;
  margin:0;
  padding:14px;
  color:#333;
}
.flow-container__info {
  position:absolute;
  bottom:10px;
  left:10px;
  padding:2px;
  font-size:9px;
}
.flow-view {
  border:none;
  position:relative;
  fill:transparent;
}

</style>
