import tkinter as tk
from enum import IntEnum

from typing import Optional, List, Dict, Iterator, Type, Tuple

from nodepasta.node import Node, IOPort, Link, LinkAddr
from nodepasta.nodegraph import NodeGraph
from nodepasta.utils import Vec
from nodepasta.errors import NodeGraphError
from nodepasta.argtypes import NodeArgValue
from nodepasta.tk.tk_arg_handlers import TKArgHandler, DEF_HANDLERS, TKNotFoundHandler


# region Helper Classes

class _PortIO(IntEnum):
    IN = 0
    OUT = 1


class _NodeRef:
    def __init__(self, node: Node, nodeTag: str):
        self.node = node
        self.iPorts: List[_PortRef] = []
        self.oPorts: List[_PortRef] = []
        self.args: Dict[str, NodeArgValue] = {}
        self.argCanvasID = -1
        self.blockCanvasID = -1
        self.nodeTag = nodeTag
        self.numPorts = 0
        self.textTag = f'{nodeTag}_text'
        self.argWidth = 0
        self.argHeight = 0
        self.blockWidth = 0

    @property
    def pos(self) -> Vec:
        return self.node.pos

    @pos.setter
    def pos(self, v: Vec):
        self.node.pos = v

    def __iter__(self) -> Iterator[Link]:
        return iter(self.node)

    def __str__(self):
        return f"Ref({self.node}, pos: {self.node})"


class _VarPortRef:
    def __init__(self, varIdx: int, parent: '_PortRef'):
        self.canvasID = -1
        self.textCanvasID = -1
        self.varIdx = varIdx
        self.parent = parent
        self.links: List[Optional[_LinkRef]] = []
        if self.parent.io == _PortIO.IN:
            self.links.append(None)

    def __str__(self):
        return f'{self.parent}.{self.varIdx}'


class _PortRef:
    def __init__(self, node: _NodeRef, port: IOPort, idx: int, io: _PortIO, typeStr: str):
        self.nodeRef = node
        self.port = port
        self.textCanvasID = -1
        self.idx = idx
        self.varPorts: List[_VarPortRef] = []
        self.io = io
        self.typeStr = typeStr
        self.btnWindowID = -1

    def __str__(self):
        return f'{self.nodeRef.node}:{self.io.name}:{self.idx}'


class _LinkRef:
    def __init__(self, canvasID: int, parent: _NodeRef, child: _NodeRef, outPort: _VarPortRef, inPort: _VarPortRef,
                 link: Link):
        self.canvasID = canvasID
        self.parent = parent
        self.child = child
        self.oPort = outPort
        self.iPort = inPort
        self.link = link


# endregion

class _TKNode(tk.Frame):
    def __init__(self, parent):
        super().__init__(parent)


# region Consts

PORT_VERT_OFFSET = 10
PORT_HORZ_OFFSET = 0
PORT_TEXT_OFFSET = 10

PORT_SIZE = 10
HALF_PORT = PORT_SIZE / 2
PORT_BLOCK_H_OFFSET = 10

IO_HEIGHT = PORT_SIZE * 2

LINK_WIDTH = 4

PORT_TAG = 'port'
PORT_BLOCK_COLOR = "grey70"
NODE_HVR_TAG = "node"
DEF_PORT_COLOR = "grey50"
LINK_COLOR = "grey70"

DIALOG_TAG = 'dialog'
DIALOG_HEIGHT = 150

VAR_PORT_BTN_SIZE = 10


# endregion


class TKNodeGraph(tk.Frame):
    def __init__(self, parent, nodeGraph: NodeGraph, registerDefArgHandlers: bool = True):
        super().__init__(parent)

        self.nodeGraph = nodeGraph

        self.portColors: Dict[str, str] = {}

        self.rowconfigure(0, weight=3)
        self.rowconfigure(1, weight=0)
        self.rowconfigure(2, weight=0)
        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=0)

        self._nodeCanvas = tk.Canvas(self)
        self._nodeCanvas.grid(row=0, column=0, stick='nesw')

        v_scroll = tk.Scrollbar(self, orient=tk.VERTICAL, command=self._nodeCanvas.yview)
        self._nodeCanvas.configure(yscrollcommand=v_scroll.set)
        v_scroll.grid(row=0, column=1, sticky='nse')

        h_scroll = tk.Scrollbar(self, orient=tk.HORIZONTAL, command=self._nodeCanvas.xview)
        self._nodeCanvas.configure(xscrollcommand=h_scroll.set)
        h_scroll.grid(row=1, column=0, sticky='ews')

        infoPanel = tk.LabelFrame(self, text="Info")
        infoPanel.grid(row=2, column=0, stick='nesw')

        self._infoVar = tk.StringVar()
        self._infoLabel = tk.Label(infoPanel, textvariable=self._infoVar)
        self._infoLabel.grid(row=0, column=0, sticky='nesw')

        self._bindPortEvents()
        self._bindNodeEvents()
        self._bindKeyEvents()

        self._draggingPort = False
        self._draggingNode = False
        self._dragStart = Vec()
        self._curDragCID = 0
        self._curNode = None
        self._curPortID = 0

        self._lowestNode = None

        # Node ID -> PosNode
        self._idToNode: Dict[int, _NodeRef] = {}
        # Node Canvas ID -> PosNode
        self._canvToNodeRef: Dict[int, _NodeRef] = {}

        # Port Canvas ID -> PortRef
        self._canvToPortRef: Dict[int, _VarPortRef] = {}

        # Link ID -> LinkRef
        self._idToLink: Dict[int, _LinkRef] = {}
        # Link Canvas ID -> LinkRef
        self._canvToLinkRef: Dict[int, _LinkRef] = {}

        if registerDefArgHandlers:
            self._argTypeHandlers: Dict[str, TKArgHandler] = DEF_HANDLERS
        else:
            self._argTypeHandlers: Dict[str, TKArgHandler] = {}

        self._PIXEL = tk.PhotoImage(width=1, height=1)

        # Do last?
        if len(self.nodeGraph) > 0:
            self.reloadGraph()

    # region Setup Funcs
    def _bindNodeEvents(self):
        self._nodeCanvas.tag_bind(NODE_HVR_TAG, "<Enter>", self._hoverNode)
        self._nodeCanvas.tag_bind(NODE_HVR_TAG, "<Leave>", self._unhoverNode)
        self._nodeCanvas.tag_bind(NODE_HVR_TAG, "<B1-Motion>", self._startNodeDrag)
        self._nodeCanvas.tag_bind(NODE_HVR_TAG, "<Control-ButtonPress-3>", self._deleteNode)

    def _bindPortEvents(self):
        self._nodeCanvas.tag_bind(PORT_TAG, "<Enter>", self._hoverPortEvent)
        self._nodeCanvas.tag_bind(PORT_TAG, "<Leave>", self._unhoverPortEvent)
        self._nodeCanvas.tag_bind(PORT_TAG, "<B1-Motion>", self._startPortDrag)
        self._nodeCanvas.bind("<ButtonRelease-1>", self._releasedDrag)

    def _bindKeyEvents(self):
        self._nodeCanvas.bind('<Control-ButtonPress-1>', self._addNewNodeDialog)
        # TODO self.bind_all('<Control-KeyPress-r>', self._resetPan)
        self._nodeCanvas.bind('<ButtonPress-3>', self._startPan)
        self._nodeCanvas.bind('<B3-Motion>', self._panCanvas)
        self.bind('<KeyPress-Escape>', self._cancel)

    # endregion

    # region Info Funcs
    def setInfoMessage(self, msg: str):
        self._infoLabel.configure(fg="black")
        self._infoVar.set(msg)

    def setErrorMessage(self, msg: str):
        self._infoLabel.configure(fg="red")
        self._infoVar.set(msg)

    # endregion

    # region NodeGraph Funcs

    def unloadArgs(self) -> Dict[int, Dict[str, any]]:
        # Node ID -> Dict[argName, value]
        out = {}

        for node in self._idToNode.values():
            args = {}
            for name, argVal in node.args.items():
                args[name] = argVal.get()
            out[node.node.nodeID] = args

        return out

    def reloadGraph(self):
        """
        Reloads the graph visuals and updates nodes
        :return: None
        """
        for node in self.nodeGraph:
            try:
                nodeRef = self._idToNode[node.nodeID]
                # Make sure that the ref gets updated if reloaded
                nodeRef.node = node
                self._updateNode(nodeRef)
            except KeyError:
                self._makeNewNode(node)

        for node in self.nodeGraph:
            for link in node:
                try:
                    linkRef = self._idToLink[link.linkID]
                    self._updateLink(linkRef)
                except KeyError:
                    self._makeNewLink(link)

    def reset(self):
        self._idToNode = {}
        self._idToLink = {}
        self._canvToNodeRef = {}
        self._canvToLinkRef = {}
        self._canvToPortRef = {}
        self._lowestNode = None
        self._nodeCanvas.delete('all')

    # endregion

    def _current(self) -> Optional[int]:
        try:
            return self._nodeCanvas.find_withtag(tk.CURRENT)[0]
        except IndexError:
            return None

    def setPortTypeColor(self, typeStr: str, color: str):
        self.portColors[typeStr] = color

    def setPos(self, n: Node, pos: Vec):
        self._idToNode[n.nodeID].pos = pos

    def _canvCoord(self, e) -> Vec:
        return Vec(self._nodeCanvas.canvasx(e.x), self._nodeCanvas.canvasy(e.y))

    # region Hover Events
    def _hoverPortEvent(self, _):
        portID = self._current()
        self._hoverPort(portID)

    def _unhoverPortEvent(self, _):
        portID = self._current()
        self._unhoverPort(portID)

    def _hoverPort(self, portID):
        self._nodeCanvas.itemconfigure(portID, fill='white')

    def _unhoverPort(self, portID):
        color = self._getPortColor(portID)
        self._nodeCanvas.itemconfigure(portID, fill=color)

    def _hoverNode(self, _):
        c = self._current()
        if c in self._canvToNodeRef:
            self._nodeCanvas.itemconfigure(c, outline="white")

    def _unhoverNode(self, _):
        c = self._current()
        if c in self._canvToNodeRef:
            self._nodeCanvas.itemconfigure(c, outline="black")

    # endregion

    # region Drag Events
    def _startPan(self, e):
        self._nodeCanvas.scan_mark(e.x, e.y)

    def _panCanvas(self, e):
        if self._dragStart is None:
            return

        self._nodeCanvas.scan_dragto(e.x, e.y, 1)

    def _startPortDrag(self, e):
        canvPos = self._canvCoord(e)
        if not self._draggingPort:
            self._draggingPort = True
            portID = self._current()

            varPortRef = self._canvToPortRef[portID]

            self._dragStart = None

            if varPortRef.parent.io == _PortIO.IN:
                try:
                    link = varPortRef.links[0]
                except IndexError:
                    link = None

                if link is not None:
                    self._dragStart = self._portCoords(link.oPort.canvasID)
                    self._removeLink(link)
                    self._curPortID = link.oPort.canvasID

            if self._dragStart is None:
                self._dragStart = self._portCoords(portID)
                self._curPortID = portID

            self._curDragCID = self._nodeCanvas.create_line(self._dragStart.x, self._dragStart.y, canvPos.x, canvPos.y,
                                                            fill=LINK_COLOR, width=LINK_WIDTH)

            self._nodeCanvas.lower(self._curDragCID, self._lowestNode)
        else:
            self._nodeCanvas.coords(self._curDragCID, self._dragStart.x, self._dragStart.y, canvPos.x, canvPos.y)

    def _startNodeDrag(self, e):
        canvPos = self._canvCoord(e)
        if not self._draggingNode:
            self._draggingNode = True
            self._curDragCID = self._current()
            self._curNode = self._canvToNodeRef[self._curDragCID]
            self._dragStart = self._curNode.pos - canvPos

        self._curNode.pos = canvPos + self._dragStart
        self._updateNode(self._curNode)

        for iport in self._curNode.iPorts:
            for varport in iport.varPorts:
                for link in varport.links:
                    if link is not None:
                        self._updateLink(link)
        for oport in self._curNode.oPorts:
            for varport in oport.varPorts:
                for link in varport.links:
                    if link is not None:
                        self._updateLink(link)

    def _releaseLinkDrag(self):
        self._draggingPort = False
        c = self._current()
        self._nodeCanvas.delete(self._curDragCID)
        try:
            varportRef1 = self._canvToPortRef[c]
            varportRef2 = self._canvToPortRef[self._curPortID]

            portRef1 = varportRef1.parent
            portRef2 = varportRef2.parent

            # TODO remove?
            self.setInfoMessage(f'{portRef1} -> {portRef2}')

            if c != self._curPortID and portRef1.io != portRef2.io:

                if portRef1.io == _PortIO.IN:
                    child = portRef1.nodeRef.node
                    parent = portRef2.nodeRef.node

                    inIdx = portRef1.idx
                    inVarIdx = varportRef1.varIdx
                    outIdx = portRef2.idx
                    outVarIdx = varportRef2.varIdx
                else:
                    child = portRef2.nodeRef.node
                    parent = portRef1.nodeRef.node

                    inIdx = portRef2.idx
                    inVarIdx = varportRef2.varIdx
                    outIdx = portRef1.idx
                    outVarIdx = varportRef1.varIdx

                try:
                    addr = LinkAddr(outIdx, outVarIdx, inIdx, inVarIdx)
                    link, removed = self.nodeGraph.makeLink(parent, child, addr)

                    if removed is not None:
                        linkRef = self._idToLink[removed.linkID]
                        self._nodeCanvas.delete(linkRef.canvasID)

                    self._makeNewLink(link)
                except NodeGraphError as err:
                    self.setErrorMessage(str(err))

        except KeyError:
            pass

    def _releaseNodeDrag(self):
        self._draggingNode = False

    def _releasedDrag(self, _):
        if self._draggingPort:
            self._releaseLinkDrag()
        if self._draggingNode:
            self._releaseNodeDrag()

    # endregion

    # region Port Utils

    def _portCoords(self, portID: int) -> Vec:
        # noinspection PyTypeChecker
        c = self._nodeCanvas.coords(portID)

        x = (c[0] + c[2]) / 2
        y = (c[1] + c[3]) / 2
        # noinspection PyTypeChecker
        return Vec(x, y)

    def _getTypeColor(self, typeStr: str) -> str:
        try:
            return self.portColors[typeStr]
        except KeyError:
            pass

        self.portColors[typeStr] = DEF_PORT_COLOR
        return DEF_PORT_COLOR

    def _getPortColor(self, portID: int) -> str:
        return self._getTypeColor(self._canvToPortRef[portID].parent.typeStr)

    def _drawPort(self, x, y, fill: str, nodeTag: str) -> int:
        a = x - HALF_PORT
        b = y - HALF_PORT
        return self._nodeCanvas.create_oval(a, b, a + PORT_SIZE, b + PORT_SIZE, fill=fill, tags=[PORT_TAG, nodeTag])

    def _movePort(self, portCID, x: int, y: int):
        self._nodeCanvas.coords(portCID, x - HALF_PORT, y - HALF_PORT, x + HALF_PORT, y + HALF_PORT)

    # endregion

    def _updateNode(self, n: _NodeRef):
        # noinspection PyTypeChecker
        x, y = self._nodeCanvas.coords(n.argCanvasID)
        self._nodeCanvas.move(n.nodeTag, n.pos.x - x, n.pos.y - y)

        self._arrangePortBlock(n)

        for port in n.iPorts:
            for varport in port.varPorts:
                color = self._getTypeColor(port.typeStr)
                self._nodeCanvas.itemconfigure(varport.canvasID, fill=color)

        for port in n.oPorts:
            for varport in port.varPorts:
                color = self._getTypeColor(port.typeStr)
                self._nodeCanvas.itemconfigure(varport.canvasID, fill=color)

    def _makeNewNode(self, n: Node, pos: Optional[Vec] = None):
        nodeTag = str(n)

        if pos is None:
            pos = n.pos
        else:
            n.pos = pos

        nodeFrame = tk.LabelFrame(self._nodeCanvas)
        tk.Label(nodeFrame, text=f'{n.NODETYPE}:{n.nodeID}').grid(row=0, column=0, sticky='nesw')
        argCanvasID = self._nodeCanvas.create_window(pos.x, pos.y, anchor='nw', window=nodeFrame,
                                                     tags=[NODE_HVR_TAG, nodeTag])

        nodeRef = _NodeRef(n, nodeTag)
        nodeRef.argCanvasID = argCanvasID

        self._idToNode[n.nodeID] = nodeRef

        for idx, arg in enumerate(n.args.values()):
            argFrame = tk.Frame(nodeFrame)
            try:
                argValue = self._argTypeHandlers[arg.argType].draw(argFrame, arg)
            except KeyError:
                argValue = TKNotFoundHandler.draw(argFrame, arg)
            argFrame.grid(row=idx + 1, column=0, sticky='nesw')
            nodeRef.args[arg.name] = argValue

        self._nodeCanvas.update()
        bounds = self._nodeCanvas.bbox(argCanvasID)
        nodeRef.argWidth = max(nodeRef.argWidth, abs(bounds[2] - bounds[0]))
        nodeRef.argHeight = max(nodeRef.argHeight, abs(bounds[3] - bounds[1]))

        self._makePortBlock(nodeRef)

    def _makeNewVarPort(self, varPortRef: _VarPortRef):
        nodeRef = varPortRef.parent.nodeRef

        textAnchor = 'w' if varPortRef.parent.io == _PortIO.IN else 'e'
        color = self._getTypeColor(varPortRef.parent.typeStr)

        portID = self._drawPort(0, 0, fill=color, nodeTag=nodeRef.nodeTag)
        if varPortRef.parent.port.variable:
            text = varPortRef.varIdx
        else:
            text = varPortRef.parent.port.name

        # noinspection PyTypeChecker
        textID = self._nodeCanvas.create_text(0, 0, text=text, anchor=textAnchor,
                                              tags=[nodeRef.nodeTag, nodeRef.textTag])

        self._nodeCanvas.update()
        bounds = self._nodeCanvas.bbox(textID)
        nodeRef.blockWidth = max(nodeRef.blockWidth, abs(bounds[2] - bounds[0]))

        varPortRef.canvasID = portID
        varPortRef.textCanvasID = textID
        self._canvToPortRef[portID] = varPortRef

    def _drawPorts(self, iterator: Iterator[IOPort], nodeRef: _NodeRef, textAnchor: str,
                   portDir: _PortIO) -> Tuple[List[_PortRef], int, int]:
        out = []
        pcount = 0
        vpcount = 0
        for idx, p in enumerate(iterator):
            vp = []
            portRef = _PortRef(nodeRef, p, idx, portDir, p.typeStr)
            pcount += 1
            for varIdx in range(p.cnt):
                varPortRef = _VarPortRef(varIdx, portRef)
                self._makeNewVarPort(varPortRef)
                vp.append(varPortRef)
                if p.variable:
                    vpcount += 1
            portRef.varPorts = vp
            out.append(portRef)
            if p.variable:
                # noinspection PyTypeChecker
                portRef.textCanvasID = self._nodeCanvas.create_text(0, 0, text=p.name, anchor=textAnchor,
                                                                    tags=[nodeRef.nodeTag, nodeRef.textTag])

        return out, pcount, vpcount

    def _addVarPort(self, portref: _PortRef):
        if not portref.port.variable:
            raise NodeGraphError("TKNodeGraph._addVarPort()", "DEV: Cannot add varport to non variable port")

        varPortRef = _VarPortRef(portref.port.cnt, portref)
        portref.port.addvarport()
        portref.nodeRef.numPorts += 1
        portref.varPorts.append(varPortRef)
        self._makeNewVarPort(varPortRef)
        self._updateNode(portref.nodeRef)

    def _remVarPort(self, portref: _PortRef):
        if not portref.port.variable:
            raise NodeGraphError("TKNodeGraph._remVarPort()", "DEV: Cannot rem varport on non variable port")
        if portref.port.cnt > 1:
            for link in portref.varPorts[-1].links:
                if link is not None:
                    self._removeLink(link)
            portref.port.remvarport()
            portref.nodeRef.numPorts -= 1
            varport = portref.varPorts.pop()
            self._nodeCanvas.delete(varport.textCanvasID, varport.canvasID)

            del self._canvToPortRef[varport.canvasID]

            self._updateNode(portref.nodeRef)

    def _makeVarBtns(self, port: _PortRef, x, y, tag):

        btnFrame = tk.Frame(self._nodeCanvas)

        addBtn = tk.Button(btnFrame, text="+", command=lambda e=port: self._addVarPort(e),
                           image=self._PIXEL,
                           compound='center',
                           padx=0, pady=0,
                           width=VAR_PORT_BTN_SIZE, height=VAR_PORT_BTN_SIZE)
        addBtn.grid(row=0, column=0,
                    ipadx=0, ipady=0,
                    )

        remBtn = tk.Button(btnFrame, text='-', command=lambda e=port: self._remVarPort(e),
                           image=self._PIXEL, compound='center',
                           padx=0, pady=0,
                           width=VAR_PORT_BTN_SIZE, height=VAR_PORT_BTN_SIZE)

        remBtn.grid(row=0, column=1)

        port.btnWindowID = self._nodeCanvas.create_window(x, y, window=btnFrame, anchor="w",
                                                          tags=[tag])

    def _makePortBlock(self, nodeRef: _NodeRef):
        if self._lowestNode is None:
            self._lowestNode = nodeRef.argCanvasID

        n = nodeRef.node

        nodeRef.iPorts, icnt, ivpcnt = self._drawPorts(n.inputs, nodeRef, "w", _PortIO.IN)
        nodeRef.oPorts, ocnt, ovpcnt = self._drawPorts(n.outputs, nodeRef, "e", _PortIO.OUT)

        if len(nodeRef.iPorts) > 0:
            lowestPort = nodeRef.iPorts[0].varPorts[0].canvasID
        else:
            lowestPort = nodeRef.oPorts[0].varPorts[0].canvasID

        numPorts = icnt + ivpcnt + ocnt + ovpcnt
        nodeRef.numPorts = numPorts

        rectCanvasID = self._nodeCanvas.create_rectangle(0, 0, 1,
                                                         1, fill=PORT_BLOCK_COLOR,
                                                         tags=[NODE_HVR_TAG, nodeRef.nodeTag])
        nodeRef.blockCanvasID = rectCanvasID
        self._canvToNodeRef[rectCanvasID] = nodeRef

        self._nodeCanvas.lower(rectCanvasID, lowestPort)

        self._arrangePortBlock(nodeRef)

    def _arrangePortBlock(self, nodeRef: _NodeRef):

        centerX = int(nodeRef.node.pos.x + (nodeRef.argWidth / 2))
        bottomY = int(nodeRef.node.pos.y + nodeRef.argHeight)

        ioWidth = max(nodeRef.argWidth, nodeRef.blockWidth + PORT_BLOCK_H_OFFSET)

        halfWidth = int(ioWidth / 2)
        leftX = centerX - halfWidth
        rightX = centerX + halfWidth
        ioHeight = nodeRef.numPorts * IO_HEIGHT

        self._nodeCanvas.coords(nodeRef.blockCanvasID, leftX, bottomY, rightX, bottomY + ioHeight)

        usableHeight = ioHeight - (2 * PORT_VERT_OFFSET)

        try:
            deltaY = usableHeight / (nodeRef.numPorts - 1)
        except ZeroDivisionError:
            deltaY = 0

        curY = bottomY + PORT_VERT_OFFSET
        iPortX = leftX - PORT_HORZ_OFFSET
        oPortX = rightX + PORT_HORZ_OFFSET

        def _align(ports: List[_PortRef], x, textOffset):
            nonlocal curY
            for p in ports:
                if p.port.variable:
                    self._nodeCanvas.coords(p.textCanvasID, x + textOffset, curY)
                    bounds = self._nodeCanvas.bbox(p.textCanvasID)
                    # TOFIX output port btn location
                    btnX = max(bounds[0], bounds[2]) + 10
                    if p.btnWindowID == -1:
                        self._makeVarBtns(p, btnX, curY, nodeRef.nodeTag)
                    else:
                        self._nodeCanvas.coords(p.btnWindowID, btnX, curY)
                    curY += deltaY
                for vp in p.varPorts:
                    self._movePort(vp.canvasID, x, curY)
                    self._nodeCanvas.coords(vp.textCanvasID, x + textOffset, curY)
                    curY += deltaY

        _align(nodeRef.oPorts, oPortX, -PORT_TEXT_OFFSET)
        _align(nodeRef.iPorts, iPortX, PORT_TEXT_OFFSET)

    def _makeNewLink(self, link: Link):
        parent = self._idToNode[link.parent.nodeID]
        child = self._idToNode[link.child.nodeID]

        outPort = parent.oPorts[link.addr.outIdx].varPorts[link.addr.outVarIdx]
        inPort = child.iPorts[link.addr.inIdx].varPorts[link.addr.inVarIdx]

        end1 = self._portCoords(outPort.canvasID)
        end2 = self._portCoords(inPort.canvasID)

        canvasID = self._nodeCanvas.create_line(end1.x, end1.y, end2.x, end2.y, fill=LINK_COLOR, width=LINK_WIDTH)
        self._nodeCanvas.lower(canvasID, self._lowestNode)

        linkRef = _LinkRef(canvasID, parent, child, outPort, inPort, link)
        self._idToLink[link.linkID] = linkRef
        self._canvToLinkRef[canvasID] = linkRef

        outPort.links.append(linkRef)
        inPort.links[0] = linkRef

    def _updateLink(self, linkRef: _LinkRef):
        end1 = self._portCoords(linkRef.oPort.canvasID)
        end2 = self._portCoords(linkRef.iPort.canvasID)

        self._nodeCanvas.coords(linkRef.canvasID, end1.x, end1.y, end2.x, end2.y)

    def _removeLink(self, linkRef: _LinkRef):
        self.nodeGraph.unlink(linkRef.link)
        linkRef.oPort.links.remove(linkRef)
        linkRef.iPort.links[0] = None

        self._nodeCanvas.delete(linkRef.canvasID)
        del self._canvToLinkRef[linkRef.canvasID]

    def _addNewNode(self, nodeType: Type[Node], pos: Vec):
        self._nodeCanvas.delete(DIALOG_TAG)
        node = nodeType()
        node.pos = pos
        self.nodeGraph.addNode(node)
        self._makeNewNode(node)

    def _addNewNodeDialog(self, e):
        self._nodeCanvas.delete(DIALOG_TAG)
        pos = Vec(self._nodeCanvas.canvasx(e.x), self._nodeCanvas.canvasy(e.y))
        # TODO bind click outside of frame to cancel

        mainDialogFrame = tk.LabelFrame(self._nodeCanvas)
        tk.Label(mainDialogFrame, text='Add Node').grid(row=0, column=0, columnspan=2, sticky='new')
        dialogCanvas = tk.Canvas(mainDialogFrame, height=DIALOG_HEIGHT, borderwidth=0)
        dialogCanvas.grid(row=1, column=0)

        sb = tk.Scrollbar(mainDialogFrame, orient=tk.VERTICAL, command=dialogCanvas.yview)
        sb.grid(row=1, column=1, sticky='nse')
        dialogCanvas.configure(yscrollcommand=sb.set)

        dialogFrame = tk.Frame(dialogCanvas)
        for idx, nodeType in enumerate(self.nodeGraph.nodeTypes()):
            btn = tk.Button(dialogFrame,
                            text=nodeType.__name__,
                            command=lambda x=nodeType: self._addNewNode(x, pos)
                            )
            btn.grid(row=idx + 1, column=0, sticky='nesw')
            btn.bind('<MouseWheel>', lambda event: dialogCanvas.yview_scroll(-event.delta, 'units'))

        dialogCanvas.create_window(0, 0, anchor='nw', window=dialogFrame)
        dialogCanvas.update()
        bbox = dialogCanvas.bbox('all')
        dialogCanvas.configure(scrollregion=bbox, width=abs(bbox[0] - bbox[2]))

        self._nodeCanvas.create_window(pos.x, pos.y, anchor='center', window=mainDialogFrame, tags=[DIALOG_TAG])

    def _cancel(self, _):
        self._nodeCanvas.delete(DIALOG_TAG)

    def _deleteNode(self, _):
        cur = self._current()
        if cur is not None:
            try:
                node = self._canvToNodeRef[cur]
            except KeyError:
                return

            for port in node.iPorts:
                for varport in port.varPorts:
                    for link in varport.links:
                        if link is not None:
                            self._removeLink(link)
            for port in node.oPorts:
                for varport in port.varPorts:
                    for link in varport.links:
                        if link is not None:
                            self._removeLink(link)

            self.nodeGraph.removeNode(node.node)
            self._nodeCanvas.delete(node.nodeTag)
