<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,
        'selecting': !!selector
      }"
      @dragover.prevent
      @drop="managerDrop"
      @mousedown="viewPointerDown"
      :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"
          :selected="nodeSelection[n.id]?true:false"
          @nodePointerDown.prevent="nodePointerDown($event,i)"
          @socketPointerDown="socketPointerDown(n.id,...arguments)"
          @nodeDoubleClick="$emit('nodeInspect',n)"
        />
        <!-- mouse link-->
        <flow-link
          :pointer="true"
          v-if="pointerLink.active"
          v-bind="pointerLink.props"
        />
        <rect
          class="flow-selector"
          :class="{'flow-selector--selecting':(selector)?true:false}"
          v-bind="selector"/>

      </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>
      <div class="vertical_sep"/>
      <!--<button @click="sendDocumentRun">Run</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'
import utils from '@/utils/utils'

export default {
  name: 'FlowManager',
  components: {FlowNode, FlowLink, FlowPanZoom, SvgDefs},
  props: {
    '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: {}},
      selector: null,

      nodeSelection: {},
      nodeStatus: {}
    }
  },
  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 && !!this.dragging[node.id],
          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
        }
      }
    }
  },
  mounted () {
    this.$flowService.on('document', (v) => {
      if (!v || !v.data || !v.data.nodes) { return }
      const nodes = []

      for (let node of v.data.nodes) {
        if (this.dragging && !!this.dragging[node.id]) {
          continue
        }
        let localNode = this.nodeData.nodes.find(n => n.id === node.id)
        if (localNode) {
          Object.assign(localNode, node)
          nodes.push(localNode)
          continue
        }
        nodes.push(node)
      }
      if (this.dragging) { // Add our nodes
        for (let nid in this.dragging) {
          nodes.push(this.dragging[nid])
        }
      }
      this.nodeData = {
        nodes: nodes,
        links: v.data.links
      }
      this.$nextTick(() => {
        this.$forceUpdate()
      })
    })
    this.$flowService.on('nodeUpdate', (v) => {
      const nodes = v.data
      const nd = this.nodeData
      for (let nid in nodes) {
        const node = nodes[nid]
        const idx = nd.nodes.findIndex(n => n.id === node.id)
        if (idx === -1) { // new node
          nd.nodes.push(nodes[nid])
          continue
        }
        // if (!this.dragging || !this.dragging[node.id]) {
        this.$set(nd.nodes, idx, node) // new Node
        // }
      }
    })
    this.$flowService.on('nodeStart', (v) => {
      this.nodeStatus[v.id] = 'started'
    })
    this.$flowService.on('nodeFinish', (v) => {
      this.nodeStatus[v.id] = ''
    })

    this.$nextTick(() => {
      this.$forceUpdate()
    })
    document.addEventListener('keydown', this.keyDown)
    document.addEventListener('keyup', this.keyUp)
  },
  beoreDestroy () {
    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, y: 0, 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}
      }
      utils.createDrag({
        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
          }
        },
        drop: (ev) => {
          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')
            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, replace
          const existingInputI = this.nodeData.links.findIndex(l => l.to === link.to && l.in === link.in)
          if (existingInputI !== -1) {
            this.nodeData.links.splice(existingInputI, 1)
          }
          this.linkAdd(link)
        }})
    },

    nodePointerDown (ev, i) {
      document.activeElement && document.activeElement.blur()
      const 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.sendFlowEvent('nodeRemove', tnode)
        this.sendDocumentUpdate()
        return
      }
      if (ev.button !== 0) return // first button
      if (ev.shiftKey) {
        this.socketPointerDown(tnode.id, ev, {out: 0})
        return
      }
      // Switch selection
      if (!this.nodeSelection[tnode.id] && !ev.ctrlKey) this.nodeSelection = {}
      this.nodeSelection[tnode.id] = tnode
      // we can handle with nodeId and a search

      // put the list of nodes in last
      for (let nk in this.nodeSelection) {
        let ni = this.nodeData.nodes.findIndex(n => n.id === this.nodeSelection[nk].id)
        this.nodeData.nodes.splice(ni, 1)
        this.nodeData.nodes.push(this.nodeSelection[nk]) // put in last
      }
      let curP = this.transformedPoint(ev.x, ev.y)

      this.dragging = this.nodeSelection
      utils.createDrag({
        drag: (ev) => {
          if (this.nodeSelection === undefined) {
            console.error('Well something went wrong')
          }
          const dragP = this.transformedPoint(ev.x, ev.y)
          for (let n in this.nodeSelection) {
            this.nodeSelection[n].x += dragP.x - curP.x
            this.nodeSelection[n].y += dragP.y - curP.y
          }
          this.sendFlowEvent('nodeUpdate', this.nodeSelection)
          curP = dragP
        },
        drop: (ev) => {
          this.dragging = null
          this.sendDocumentUpdate()
        }
      })
    },
    nodeAdd (src, x = 100, y = 100) {
      const newNode = {
        id: utils.guid(),
        x: x,
        y: y,
        label: src,
        src: src
      }
      // Setup Props
      if (this.registry[src].props) {
        for (let k in this.registry[src].props) {
          newNode.prop || (newNode.prop = {})
          newNode.prop[k] = ''
        }
      }

      this.nodeData.nodes.push(newNode)
      let nu = {}
      this.sendFlowEvent('nodeUpdate', (nu[newNode.id] = newNode, nu))
      this.sendDocumentUpdate()
    },
    linkAdd (link) {
      this.nodeData.links.push(link)
      this.sendFlowEvent('linkUpdate', link)
      this.sendDocumentUpdate()
    },
    linkRemove (link) {
      const i = this.nodeData.links.findIndex(l => l === link)
      if (i === -1) return
      this.nodeData.links.splice(i, 1)
      this.sendFlowEvent('linkRemove', link)
      this.sendDocumentUpdate()
    },
    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)
    },
    viewPointerDown (ev) {
      if (ev.button !== 0) return
      ev.preventDefault()
      this.nodeSelection = {}
      const p = this.transformedPoint(ev.x, ev.y)
      this.selector = {x: p.x, y: p.y, width: 0, height: 0}
      utils.createDrag({
        drag: (evd) => {
          // transform again in case we changed zoom/pan
          const p = this.transformedPoint(ev.x, ev.y)
          const p2 = this.transformedPoint(evd.x, evd.y)
          const nwidth = p2.x - p.x
          const nheight = p2.y - p.y
          this.selector = {
            x: nwidth < 0 ? p2.x : p.x,
            y: nheight < 0 ? p2.y : p.y,
            width: nwidth < 0 ? -nwidth : nwidth,
            height: nheight < 0 ? -nheight : nheight
          }
        },
        drop: (ev) => {
          for (let n of this.nodeData.nodes) {
            if (n.x > this.selector.x && n.x < (this.selector.x + this.selector.width) &&
              n.y > this.selector.y && n.y < (this.selector.y + this.selector.height)
            ) {
              this.nodeSelection[n.id] = n
            }
          }
          this.selector = null
        }})
    },

    // service events
    sendFlowEvent (type, param) {
      this.$flowService[type](param)
    },
    sendDocumentUpdate (nodeData) {
      this.$flowService.documentUpdate(this.nodeData, this.$route.params.sessId)
    },
    sendDocumentRun () {
      console.log('Document running')
      this.$flowService.documentRun(this.nodeData, this.$route.params.sessId)
    },
    // 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))
    }
  }
}

</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;
}

.flow-selector {
  pointer-events:none;
  opacity:0;
}

.flow-selector.flow-selector--selecting {
  opacity:1;
}

</style>
