import sys
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtCore import QPointF
from PyQt5.QtGui import QPainter,QPainterPath
from PyQt5 import QtGui
from PyQt5.QtCore import Qt,QSizeF
from .my_interpolation import my_interpolate
# from scipy.interpolate import interp1d
import time
from PyQt5.QtWidgets import QFileDialog,QMessageBox
from PyQt5.QtGui import QPixmap
from PyQt5 import QtPrintSupport
from PyQt5.QtPrintSupport import QPrintDialog, QPrinter
from PyQt5.QtGui import QFont, QFontDatabase

import os
import numpy as np
import traceback 

#from transaction_editor import TransactionEditor
#from agent_editor import AgentEditor

from .agent_editor import CodeEditor


class Label:
    """
    A label for a box
    """

    label_instances = {}

    def __new__(cls,x,y,name,subject,parent_box):
        # print("NEW", cls,x,y,name,subject)

        if name in cls.label_instances:
            if cls.label_instances[name] is not None:

                return cls.label_instances[name]

        return super().__new__(Label)



    def __init__(self,x,y,name,subject,parent_box):
        self.x = x
        self.y =  y
        self.box = parent_box
        self._name = name
        self.subject = subject

        self.__class__.label_instances[name] = self

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self,val):
        self.__class__.label_instances[val] = self
        self.__class__.label_instances[self._name] = None
        self._name = val


    def draw(self, qp, brush, position = None):

        dw = MyDrawWidget.instance
        zf = dw.zoom_factor

        if dw.hover_object == self:
            brush = QtGui.QBrush(QtGui.QColor(200, 200, 200, 255))
            qp.setPen(QtGui.QPen(QtGui.QColor(100, 200, 100 , 255),0))
        
        # set the name according to the checkbox settings
        if not dw.main_widget.SETTING_subject_labels: # checkBox_8.isChecked():
            name = self._name.replace("_"," ")
        else:
            name = self.subject.replace("_"," ")

        # check the actually dispalyed height and length
        fm = qp.fontMetrics()

        text_length = fm.boundingRect(name).width() + 2
        text_height = fm.boundingRect(name).height() + 2
    	
        if position is None:
            a = zf*(dw.gox + self.x)
            b = zf*(dw.goy + self.y)
            # print(self, "zf",zf, "goxy", dw.gox, dw.goy, a, b) < for debugging purposes
        else:
            a,b = position

        pw = MyDrawWidget.instance # self.box.parent_widget
        # draw highlighted background
        if brush is not None and dw.main_widget.SETTING_highlight_labels: # checkBox_4.isChecked():
            px = (int(a-.1))
            py = (int(b-text_height+1))

            qx = (int(a + text_length))
            qy = (int(b))

            bound_rect = QtCore.QRectF(QtCore.QPointF(px,py),QtCore.QPointF(qx,qy))
            qp.fillRect(bound_rect,brush)

        # draw text
        qp.drawText(int(a), int(b-1), name)


class Box:
    """
    A box (representing an agent)
    """
    def __init__(self, x, y, name, subject, ishelper=False, parent=None, parent_widget=None, is_support=False):

        self._x = x
        self._y = y

        self.x0 = x
        self.y0 = y

        self._name = name #.capitalize()
        self.subject = subject # if is a helper box, subject is the transaction subject
        self.label = Label(x+1,y-7,name,subject,parent_box = self)

        self.ishelper = ishelper
        self.is_support = is_support
        self.parent = parent
        self.n_connections = 0

        self.parent_widget = parent_widget

    @property
    def name(self):
        return self._name
    @name.setter
    def name(self,val):
        self._name = val
        self.label.name = val

    @property
    def x(self):
        if self.parent_widget is not None:
            return self.parent_widget.raster(self._x)
        
        return int(self._x)

    @property
    def y(self):
        if self.parent_widget is not None:
            return self.parent_widget.raster(self._y)

        return int(self._y)

    @x.setter
    def x(self,val):
        # print("new x ", self)
        dx = val - self._x
        self._x += dx
        self.label.x += dx

    @y.setter
    def y(self,val):
        dy = val - self._y
        self._y += dy
        self.label.y += dy

    def draw(self, qp, brush=None, active=True):

        dw = MyDrawWidget.instance
        zf = dw.zoom_factor

        if qp:
            if brush is None:
                brush = qp.brush()
            if dw.hover_object == self:
                brush = QtGui.QBrush(QtGui.QColor(200, 20, 20, 255))
                qp.setBrush(brush)

        ppaint = None # paint position (return arg)
        
        if self.ishelper:

            pw = MyDrawWidget.instance # self.parent_widget

            if active:
                handle_w = 3

                px = int(zf*(dw.gox + self.x - .5*handle_w))# - handle_w)
                py = int(zf*(dw.goy + self.y - .5*handle_w))# - handle_w)

            else:
                handle_w = 0 # 1

                px = int(zf*(dw.gox + self.x))# - handle_w)
                py = int(zf*(dw.goy + self.y))# - handle_w)

            #if pw is not None: 
            #    px = pw.raster(px) 
            #    py = pw.raster(py) 
            
            if handle_w > 0:
                if qp: qp.drawEllipse(QtCore.QPointF(px,py), 2* handle_w, 2* handle_w)
                ppaint = px, py 
        else:

            pw = self.parent_widget
            path = QPainterPath()

            w = 45
            x = self.x
            y = self.y
            x2 = self.x + w
            y2 = self.y+ w

            x1 = zf*(dw.gox +x)
            y1 = zf*(dw.goy +y)
            x2 = zf*(dw.gox +x2)
            y2 = zf*(dw.goy +y2)

            # rect with rounded corners
            round_w = 4

            # draw boxes
            if pw is not None:
                x1 = pw.raster(x1)
                y1 = pw.raster(y1) 

            if qp: path.addRoundedRect(QtCore.QRectF(x1,y1,zf*w,zf*w), zf*round_w, zf*round_w)
            if qp: qp.fillPath(path,brush.color())
            ppaint = x1, y1

        if qp: qp.setBrush(brush)
        return ppaint 


    def edit_agent(self, raise_notification=True):
        if not self.ishelper:
            #print("EDIT AGENT CODE", self.name)
            # print("INSTANCES",CodeEditor.instances,"\n\n")
            start = ""

            # workaround for old file versions
            if self.name.lower() in self.parent_widget.code_data:
                start = self.parent_widget.code_data[self.name.lower()]

            if self.name in self.parent_widget.code_data:
                print("found agent", self.name)

                start = self.parent_widget.code_data[self.name]
                print("start <-", start)

            if self.name not in CodeEditor.instances:
                print("need to create new code editor for", self.name)
                # dw = self.parent_widget.main_widget.tabWidget_2
                tw = self.parent_widget.main_widget.tabWidget

                pos = self.parent_widget.main_widget.pos()
                x = pos.x()
                y = pos.y()
                w = self.parent_widget.main_widget.frameGeometry().width()
                h = self.parent_widget.main_widget.frameGeometry().height()

                tab = QtWidgets.QWidget()

                scrollbar = QtWidgets.QScrollArea(widgetResizable=True)
                scrollbar.setWidget(tab)
                #tab.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,QtWidgets.QSizePolicy.Expanding))

                # print("New code editor in ",dw,"parent",dw.parent())
                if self.parent_widget.main_widget.current_file is None:
                    if raise_notification:
                        self.parent_widget.main_widget.notify("Please save the project before editing agents.",title="Save Project")
                else:
                    print("new Code Editor")
                    ce = CodeEditor(tab,self,start)
                    tw.addTab(tab, ce.search_name())
                    ce.tab = tab

                    tab_idx = tw.indexOf(tab)

                    #ce.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,QtWidgets.QSizePolicy.Expanding))
                    ce.setGeometry(2,2,self.parent_widget.frameGeometry().width()-2,self.parent_widget.frameGeometry().height()-2)
                    tw.setGeometry(0,0,self.parent_widget.frameGeometry().width(),self.parent_widget.frameGeometry().height())
                    #dw.dockLocationChanged.connect(ce.resizeEvent)

                    tw.setCurrentIndex(tab_idx)
                    ce.resize_frame()

            # activate current tab
            if self.name in CodeEditor.instances:
                current_tab = CodeEditor.instances[self.name].tab
                print("found in code editor instances. tab %s" % str(current_tab))

                tw = self.parent_widget.main_widget.tabWidget
                print("ok1")

                print("current_tab:", current_tab)
                print("index in tabWidget:", tw.indexOf(current_tab))
                print("tabWidget count:", tw.count())

                index = tw.indexOf(current_tab)
                if index == -1:
                    print("Error: current_tab not found in tabWidget")
                else:
                    try:
                        # tw.setCurrentWidget(current_tab)
                        print("ok2")
                        tw.setTabText(index, self.name)
                    except Exception as e:
                        print("Error updating tab widget:", str(e))
                
                print("ok2")
                tw.setTabText(tw.indexOf(current_tab),self.name)

                print("ok3")
                if start is not None and start != "":
                    CodeEditor.instances[self.name].textEdit.setPlainText(start)
                    self.parent_widget.code_data[self.name] = str(start)
    
        else:
            #print("CANNOT EDIT HELPER")
            pass 

    def overlaps(self,boxes):
        if self.ishelper:
            e = 8
        else:
            return False

        for box in boxes:

            if self.x - box.x < e:
                return True
            if self.y - box.y < e:
                return True

        return False

    def adjust_position(self, boxes):
        k = 0
        while self.overlaps(boxes) and k < 100:

            self.x = max(0, self.x0 + np.random.uniform(-45,45))
            self.y = max(0, self.y0 + np.random.uniform(-45,45))

            try:
                self.x = np.clip(self.x ,1,self.parent_widget.W - 45) # 799)
                self.y = np.clip(self.y ,1,self.parent_widget.H -45) # 699)
                # TODO fix parent_widget is None sometimes
            except:
                pass

            k+= 1

class Connector:
    def __init__(self, a, b, name="Connector", items=None, subject="(None)"):
        """
        creates a new connector

        :param a: a box (a) sender
        :param b: a box (b) receiver
        :param name: the label of the conncetor
        :param items: items affected by this connector (list of str)
        """

        self.a = a
        self.b = b
        self.curve = None

        if items is None:
            self.items = []
        else:
            self.items =items

        self.subject = subject

        #self.a.n_connections += 1
        #if self.a != self.b:
        #    self.b.n_connections += 1
        # ^moved to add_connection

        dx = self.b.x +25/2. - self.a.x #+ 25
        dy = self.b.y +25/2. - self.a.y #+ 25

        u = np.random.uniform(-1,1)
        v = np.random.uniform(-1,1)

        if a != b:
            #self.c = Box(self.a.x + 0.5*dx + u, self.a.y + 0.5*dy + v,name,subject,ishelper=True,parent=self)
            self.c = Box(self.a.x + 0.5*dx + u, self.a.y + 0.5*dy + v,name,subject,ishelper=True,parent=self)
            self.d = Box(self.a.x + 0.75*dx, self.a.y + 0.75*dy,name+"<support>",subject,ishelper=True,parent=self,is_support=True)

            #self.d = Box(self.a.x + 0.75*dx, self.a.y + 0.75*dy,name+"<support>",subject,ishelper=True,parent=self,is_support=True) # <- is support handle for better line interpolation
        else:
            self.c = Box(self.a.x + 5*u, self.a.y + 5*u ,name,subject,ishelper=True,parent=self)
            self.d = Box(self.a.x + 0.75*dx, self.a.y + 0.75*dy,name+"<support>",subject,ishelper=True,parent=self,is_support=True)

        self.a.ishelper = False
        self.b.ishelper = False

        # NOTE small connector boxes c,d are called 'helper' boxes. The
        # support the line drawn between the 'agent' boxes a, b


class MyDrawWidget(QtWidgets.QWidget):

    instance = None

    def __init__(self,parent_widget,main_widget):

        super().__init__(parent_widget)

        parent_widget.resizeEvent = self.resizeEvent
        # self.W = int(self.parent().frameGeometry().width()-1030) # 850
        # self.H = 750
        # self.setGeometry(0,0,self.W,self.H)
        self.startx = None 
        self.starty = None

        self.mouse_pos = QPointF(0,0)

        self.move(0,0)


        self.parent_widget = parent_widget
        self.main_widget = main_widget

        self.hover_object = None 

        self.resize_frame()
        self.__class__.instance = self

        self.boxes = []
        self.connectors = []

        self.old_positions = {}

        self.code_data = {}
        self.box_position_data = {}
        self.label_position_data = {}

        self.selected = None
        self.connected1 = None
        self.connected2 = None
        self._highlighted = None

        self.last_click = time.time()
        
        self.main_widget.zoomOutButton.pressed.connect(self.zoom_out)
        self.main_widget.zoomInButton.pressed.connect(self.zoom_in)

        # self.main_widget.graphEditButton.setEnabled(False)

        self.setMouseTracking(True)
        self.standard_cursor = QtCore.Qt.CrossCursor
        self.block_drag = False 
        
        # self.main_widget.setMouseTracking(True)
        self.W0 =  max(400,int(self.parent_widget.frameGeometry().width()-190)) # 850
        self.H0 =  max(1000,int(self.parent_widget.frameGeometry().height()-100)) # 750

        self.offx = 0
        self.offy = 0 # temporary offset when moving a box1

        self.gox = 0   # global offset x-axis
        self.goy = 0   # global offset y-axis
        self.go_startx = 0 # starting position when moving global offset
        self.go_starty = 0
        self.gox_start = 0
        self.goy_start = 0 # starting position before moving global offset

        self.maxx = self.W-50 # 800
        self.maxy = self.H-50 # 700

        self.mousex = 0
        self.mousey = 0

        self.interpol_method = "slinear" #"quadratic"
        self.mode = "move" # "select"

        self.zoom_factor = 0.5 # 1.0 # 1.0

        self.raster_size = 20.0 # 0.9
        self.show_raster = False

        self.show()


    def update_raster(self):
        # updates the raster settings

        # self.raster_size = 10.0 # self.main_widget.spinBoxRaster.value()  #
        # * 0.01
        self.show_raster = self.main_widget.SETTING_use_raster # checkBox_raster.isChecked()
        #print("raster", self.raster_size,self.show_raster)

        self.update()

    def resizeEvent(self, event):
        # print("RESIZE DRAW CANVAS")
        self.resize_frame()

    def swap_interpol_style(self):
        if self.interpol_method == "cubic":
            self.interpol_method = "quadratic"
        elif self.interpol_method == "quadratic":
            self.interpol_method = "slinear"
        elif self.interpol_method == "slinear":
            self.interpol_method = "cubic"
        self.main_widget.update()
    
    def wheelEvent(self,event):

        numDegrees = event.angleDelta().y()
        numSteps = numDegrees / 15
        if numSteps > 0:
            self.zoom_in()
        else:
            self.zoom_out()
        event.accept()

    def zoom_in(self):
        self.zoom_factor += 0.1

        if self.zoom_factor > 3.0:
            self.zoom_factor = 3.0


        self.update()

    def zoom_out(self):

        self.zoom_factor -= 0.1
        if self.zoom_factor < .3:
            self.zoom_factor = .3

        self.update()


    def resize_frame(self):
        # NOTE not used from version spotykach TODO think of re-introducing this when parent widget is resized

        #self.W = max(500,int(self.parent().frameGeometry().width()-970)) # 850
        #self.H = max(850,int(850+self.parent().frameGeometry().height()-986)) # 750
        self.W = int(self.parent_widget.frameGeometry().width()-10) # 850
        self.H = int(self.parent_widget.frameGeometry().height()-10) # 750

        self.maxx = self.W- 50 # 800
        self.maxy = self.H- 50 # 700
        # self.setGeometry(890,170,self.W,self.H)
        self.setGeometry(5,13,self.W,self.H)

    def export_pdf(self):
        success = False

        if self.main_widget.current_file is not None:
            try:
                filename = QFileDialog.getSaveFileName(self, 'Open file',os.path.dirname(os.path.abspath(self.main_widget.current_file)), "PDF Files (*.pdf)")[0]
                success = True
            except:
                pass
        if not success or self.main_widget.current_file is None:
            try:
                filename = QFileDialog.getSaveFileName(self, 'Open file',os.getcwd(), "PDF Files (*.pdf)")[0]
                success = True
            except Exception as e:
                self.main_widget.notify("Something went wrong while exporting the Graph as PDF:\n" + str(e), title="Error")

        if not success or filename == "" or filename is None:
            return

        # export current's frame painting to pdf
        printer = QPrinter()
        printer.setOutputFormat(QtPrintSupport.QPrinter.PdfFormat)
        printer.setOutputFileName(filename)
        printer.setOrientation(QtPrintSupport.QPrinter.Landscape)
        printer.setFullPage(False) # keep?
        marginw = 100
        marginh = 100
        printer.setPaperSize(QSizeF(self.width()-marginw,self.height()-marginh),QPrinter.Point)
        
        qp = QtGui.QPainter(printer) 
        try:
            self.paint(reference=printer, painter=qp, ratio=1.1)
        finally:
            qp.end()


    @property
    def highlighted(self):
        return self._highlighted

    @highlighted.setter
    def highlighted(self,val):
        if val is None:
            # self.main_widget.graphEditButton.setEnabled(False)
            self.main_widget.graphDeleteButton.setEnabled(False)

            self._highlighted = val
        else:
            if not val.ishelper: # is not a helper node (connection node)
                # self.main_widget.graphEditButton.setEnabled(True)
                # self.parent().graphDeleteButton.setEnabled(True)
                if val.n_connections == 0:
                    self.main_widget.graphDeleteButton.setEnabled(True)
                else:
                    print("Delete disabled because has still %i connections" % val.n_connections)
                    self.main_widget.graphDeleteButton.setEnabled(False)
            else:
                # print("Cannot delete (is helper)")

                if str(val.label.name) != "None":
                    # self.main_widget.graphEditButton.setEnabled(False)
                    self.main_widget.graphDeleteButton.setEnabled(False)

            self._highlighted = val

    def box_positions(self):
        pos_data = {}

        for box in self.boxes:
            pos_data[box.name] = {"x": int(box.x), "y": int(box.y)}

        return pos_data

    def label_positions(self):
        pos_data = {}

        for box in self.boxes:
            

            pos_data[box.name + "<label>"] = {"x": int(box.label.x), "y": int(box.label.y)}

        return pos_data


    def reposition(self):
        pos_data = self.old_positions # box_position_data
        # reposition all the boxes
        # print("reposition <-> ", pos_data)
        for box in self.boxes:
            if box.name in pos_data:

                # print("reposition",box.name,pos_data[box.name])
                box.x = pos_data[box.name][0] # ["x"]
                box.y = pos_data[box.name][1] # ["y"]
                box.x0 = box.x
                box.y0 = box.y

        for connector in self.connectors:
            for box in [connector.a, connector.b, connector.c, connector.d]:
                if box.name in pos_data:

                    # print("reposition",box.name,pos_data[box.name])
                    box.x = pos_data[box.name][0] # ["x"]
                    box.y = pos_data[box.name][1] # ["y"]
                    box.x0 = box.x
                    box.y0 = box.y

    def center_boxes(self):
        # qp = QtGui.QPainter(self)
        
        def pointrange():
            x = []
            y = []
            for connector in self.connectors:
                for box in [connector.a, connector.b, connector.c, connector.d]:
                    ppaint = box.draw(None) 
                    if ppaint:
                            px, py = ppaint 
                            x.append(px)
                            y.append(py)
            
            for box in self.boxes:
                ppaint  = box.draw(None )
                if ppaint:
                    px, py = ppaint 
                    x.append(px)
                    y.append(py)
            return x, y 
            
        # wx = int(1115*0.5)
        # wy = int(1080*0.5)
        min_x = -1 
        while min_x < 25:
            x, y = pointrange()
            min_x = np.min(x)
            self.gox += 5

    def reposition_labels(self):
        pos_data = self.label_position_data # label_position_data

        # reposition all the labels
        # print("reposition <-> ", pos_data)

        for box in self.boxes:
            # print("box", box)
            label_name = box.name + "<label>"

            #try:
            if label_name in pos_data:
                
                # print("reposition", label_name, pos_data[label_name])
                box.label.x = pos_data[label_name]["x"] # ["x"]
                box.label.y = pos_data[label_name]["y"] # ["y"]
                
                # except Exception as e:
                #    self.parent_widget.notify(str(e), title="Error")

        for label in Label.label_instances.values():

            # print("label", label.name)
            if label is not None:
                label_name = label.name + "<label>"

                if label_name in pos_data:
                    label.x = pos_data[label_name]["x"] # ["x"]
                    label.y = pos_data[label_name]["y"] # ["y"]

             

    def edit_agent(self, raise_notification=True):
        if self.highlighted is not None:
            self.highlighted.edit_agent(raise_notification=raise_notification)

    def remove_agent(self):
        # remove currently selected agent if it has no connections
        box = self.highlighted
        if box is not None and box.n_connections <= 0:
            if box in self.boxes:
                self.boxes.remove(box)
            if box.name in self.code_data:
                del self.code_data[box.name]

            if box.name in CodeEditor.instances:
                ce = CodeEditor.instances[box.name]
                tw = box.parent_widget.main_widget.tabWidget
                tw.removeTab(tw.indexOf(ce.tab))

            self.update()
        else:
            self.main_widget.notify("Box has still %i connection(s)! Cannot remove"%box.n_connections, title="Error")

    def check_exist(self,name):
        if name == "":
            return False

        for box in self.boxes:
            # TODO more efficient search for large number of boxes?
            if box.name == name:
                return True
        return False

    def add_agent(self, name):

        # mechanism to avoid overwriting...
        for box in self.boxes:
            if not box.ishelper and box.name == name:
                return box


        # actually a new agent?
        off = 200 # pixel offset for random sampling of agent position 

        if name not in self.old_positions:
            print("   > could not find", name, "in old positions")
            u = np.random.uniform(off+150,off+250)
            v = np.random.uniform(off+150,off+250)
            
            self.old_positions[name] = u,v
        else:
            u, v = self.old_positions[name]
            print("Move to old position", name, u, v)

        new_box = Box(u, v, name, subject=name, ishelper=False, parent_widget=self)
        print("box", new_box, u, v)
        self.boxes.append(new_box)
        
        # check label position 
        if name + "<label>" in self.old_positions:
            new_box.label.x = self.old_positions[name+"<label>"][0]
            new_box.label.y = self.old_positions[name+"<label>"][1]

        print("aDD _>",  )
        self.parent_widget.highlighted = self 
        new_box.edit_agent()
        self.update()

        return new_box

    def arrange_pretty(self):
        for box in self.boxes:
            box.adjust_position(self.boxes)

        self.update()

    def highlight_connector(self,name):
        for box in self.boxes:
            if box.ishelper and box.name == name or box.name.lower() == name.lower():
                self.highlighted = box

        self.update()


    def add_connection(self, box1, box2, name, items, subject="(None)"):


        new_conn = Connector(box1, box2, name, items, subject=subject)
        #print("NEW CONN", name)

        if name not in self.old_positions or (name + "<label>" not in self.old_positions):
            self.old_positions[name] = new_conn.c.x, new_conn.c.y 
            self.old_positions[name +"<label>"] =  new_conn.c.x, new_conn.c.y 
        else:
            new_conn.c.x = self.old_positions[name][0]
            new_conn.c.y = self.old_positions[name][1]

            new_conn.c.label.x = self.old_positions[name+"<label>"][0]
            new_conn.c.label.y = self.old_positions[name+"<label>"][1]
            
            #print("CONN C", self.old_positions[name+"<label>"])

        if name+"<support>" not in self.old_positions:
            self.old_positions[name+"<support>"] = new_conn.d.x, new_conn.d.y
        else:
            new_conn.d.x = self.old_positions[name+"<support>"][0]
            new_conn.d.y = self.old_positions[name+"<support>"][1]
            
            try:
                new_conn.d.label.x = self.old_positions[name+"<support><label>"][0]
                new_conn.d.label.y = self.old_positions[name+"<support><label>"][1]
            except:
                pass 
            
            #print("CONN D", self.old_positions[name+"<support><label>"])

        box1.n_connections += 1
        box2.n_connections += 1

        self.connectors.append(new_conn)
        self.boxes.append(new_conn.c)
        self.boxes.append(new_conn.d)

        return new_conn

    def clear(self,clearall=False):

        if clearall:
            boxes = [b for b in self.boxes]

        else:
            boxes = [b for b in self.boxes if b.ishelper]

        while len(boxes) > 0:
            b = boxes.pop()
            self.boxes.remove(b)
            self.old_positions[b.name] = (b.x, b.y)
            try:
                self.old_positions[b.name + "<label>"] = (b.x, b.y)
            except:
                pass

        while len(self.connectors) > 0:
            self.connectors.pop()


    def rename_connection(self,name,new_name):
        for box in self.boxes:
            if box.ishelper and box.name == name:
                box.name = new_name
                box.label.name = new_name
                self.update()
                self.old_positions[box.name] = (box.x,box.y)

            if box.ishelper and box.name == name+"<support>":
                box.name = new_name+"<support>"
                box.label.name = new_name + "<support>"
                self.update()
                self.old_positions[box.name] = (box.x, box.y)

    def remove_connection(self,name):
        self.update()

        self.highlighted = None
        self.selected = None
        #print("remove connection",name)

        for box in self.boxes:
            if box.ishelper and box.name == name:
               
                box.parent.a.n_connections -= 1
                box.parent.b.n_connections -= 1


                self.boxes.remove(box)
                self.connectors.remove(box.parent)

                self.old_positions[box.name] = (box.x, box.y)
                self.old_positions[box.parent.a.name] = (box.parent.a.x,box.parent.a.y)
                self.old_positions[box.parent.b.name] = (box.parent.b.x,box.parent.b.y)

                self.update()
                return

    def manhattan_lines(self,x0, y0, xf, yf, n=50):
    
        wx = abs(xf - x0) # abs(ip[-1][0] - ip[0][0])
        wy = abs(yf - y0) #  abs(ip[-1][1] - ip[0][1])

        eps = 1e-2
        if wx <= eps:
            x_coords = [x0  for i in range(n)]
            y_coords = list(np.linspace(y0,yf,n))
            return x_coords, y_coords
        
        if wy <= eps:
            y_coords = [y0 for i in range(n)]
            x_coords = list(np.linspace(x0,xf,n))
            return x_coords, y_coords
        
    
        len1 = int(0.75*n)
        len2 = int(0.25*n)

        if wx > wy:

            x_coords = list(np.linspace(x0,xf,len1)) + [xf]*len2
            y_coords = [y0]*len1 + list(np.linspace(y0,yf,len2))

        else:

            y_coords = list(np.linspace(y0,yf,len1)) + [yf]*len2
            x_coords = [x0]*len1 + list(np.linspace(x0,xf,len2))

        n = len(x_coords)


        return x_coords, y_coords

    def crop_angle(self,x):
        x = x % np.pi

        if 0 <= x < np.pi/2:
            x = 0.0

        elif np.pi/2 <= x < np.pi:
            x = np.pi/2

        elif -np.pi/2 <= x < 0 :
            x = -np.pi/2.0

        elif -np.pi <= x < -np.pi/2:
            x = -np.pi

        return x

    def draw_arrow_at(self, qp, interpolated_points, md,offset_text=15, reverse = False, dashed=False,dd=5):
        
        if dashed:
            return 
        
        if self.show_raster:
            dd = 5
            md = int(len(interpolated_points)/2.)
            # reverse = not reverse
        
        mid_point = interpolated_points[md]
        mid_point2 = interpolated_points[md+dd]
        while dd < len(interpolated_points) and (mid_point2[0] == mid_point[0] and mid_point2[1] == mid_point[1]):
            dd+= 1
            mid_point2 = interpolated_points[md+dd]
        
        mid_points3 = interpolated_points[md-offset_text+1]
        mid_points4 = interpolated_points[md-offset_text+1+2*dd]

        # for debugging:
        #qp.drawRect(int(mid_point[0]),int(mid_point[1]),8,8)
        #qp.drawRect(int(mid_point2[0]),int(mid_point2[1]),8,8)
        
        # start = interpolated_points[0] 
        # end = interpolated_points[-1]

        dy = mid_point[1] - mid_point2[1]
        dx = mid_point[0] - mid_point2[0]

        if reverse:
            dx = -dx
            dy = -dy

        alfa = np.arctan2(dy,dx)

        eps = 1e-10

        if abs(dy) < eps and abs(dx) >= 0:
            alfa = 0.0

        elif abs(dy) < eps and abs(dx) < 0:
            alfa = np.pi

        elif abs(dx) < eps and abs(dy) >= 0:
            alfa = -np.pi/2.

        elif abs(dx) < eps and abs(dy) < 0:
            alfa = np.pi/2.

        if self.show_raster:
            alfa = self.crop_angle(alfa)
        
        beta1 = alfa + np.pi/4
        beta2 = alfa - np.pi/4

        if abs(dy) > abs(dy):
            beta3 = alfa - np.pi/3
        else:
            beta3 = alfa - np.pi/3 + np.pi/2

        x2 = mid_point2[0]
        y2 = mid_point2[1]

        d = 10 # arrow width
        x3 = np.cos(beta1)*d + x2
        y3 = np.sin(beta1)*d + y2

        x4 = np.cos(beta2)*d + x2
        y4 = np.sin(beta2)*d + y2

        xt = np.cos(beta3)*3 + mid_points3[0]
        yt = np.sin(beta3)*3 + mid_points3[1]

        if self.show_raster:

            darr = 10 # arrow width
            eps = 1e-10
            if abs(dy) < eps and (dx) <= 0:
                x3 = x2 - d
                y3 = y2 + darr

                x4 = x2 - d
                y4 = y2 - darr

            elif abs(dy) < eps and (dx) > 0:
                x3 = x2 + d
                y3 = y2 + darr

                x4 = x2 + d
                y4 = y2 - darr


            elif abs(dx) < eps and (dy) >= 0:
                x3 = x2 - darr
                y3 = y2 + d

                x4 = x2 + darr
                y4 = y2 + d


            elif abs(dx) < eps and (dy) < 0:
                x3 = x2  - darr
                y3 = y2 - d

                x4 = x2 + darr
                y4 = y2 - d


        # draw arrow
        # if is_flow: # TODO think about behavior for non-flows
        if not dashed:
            if self.main_widget.SETTING_show_arrows: # checkBox.isChecked(): # not end_is_start and  ...

                # left side
                qp.drawLine(int(self.zoom_factor*(x2)),int(self.zoom_factor*(y2)), int(self.zoom_factor*(x3)), int(self.zoom_factor*(y3)))

                # right side
                qp.drawLine(int(self.zoom_factor*(x2)), int(self.zoom_factor*(y2)), int(self.zoom_factor*(x4)), int(self.zoom_factor*(y4)))

        return xt, yt


    def curved_connector(self,qp,x0,y0,xf,yf,xi,yi,xd,yd,dashed=False,is_flow=True,is_unidir=True):
        """
        draws a curved connector
        """


        # construct helper box_positions
        xi2 = 0.5*(xi-x0) + x0
        xi3 = 0.5*(xf-xi) + xi
        yi2 = 0.5*(yi-y0) + y0
        yi3 = 0.5*(yf-yi) + yi

        #qp.drawRect(xi2-5,yi2-5,10,10)
        #qp.drawRect(xi3-5,yi3-5,10,10)
        W = 25 # 25 # 12.5

        xi4 = xi
        yi4 = yi
        xi5 = xf
        yi5 = yf

        # first quadrant (bottom right)
        if xf >= xi and yf >= yi and abs(xi-xf) >W:
            xi4 = xi+W
            yi4 = yi
            xi5 = xf-W
            yi5 = yf

        # second quadrant (bottom left)
        if xf < xi and yf >= yi and abs(xi-xf) >W:
            xi4 = xi-W
            yi4 = yi
            xi5 = xf+W
            yi5 = yf

        # third quadrant (top left)
        if xf < xi and yf < yi and abs(xi-xf) >W:
            xi4 = xi-W
            yi4 = yi
            xi5 = xf+W
            yi5 = yf

        # fourth quadrant (top right)
        if xf >= xi and yf < yi and abs(xi-xf) >W:
            xi4 = xi+W
            yi4 = yi
            xi5 = xf-W
            yi5 = yf

        xi6 = x0
        yi6 = y0
        xi7 = xi
        yi7 = yi

        # first quadrant (bottom right)
        if xi >= x0 and yi >= y0 and abs(xi-x0) >W:
            xi6 = x0+W
            yi6 = y0
            xi7 = xi-W
            yi7 = yi

        # second quadrant (bottom left)
        if xi < x0 and yi >= y0 and abs(xi-x0) >W:
            xi6 = x0-W
            yi6 = y0
            xi7 = xi+W
            yi7 = yi

        # third quadrant (top left)
        if xi < x0 and yi < y0 and abs(xi-x0) >W:
            xi6 = x0-W
            yi6 = y0
            xi7 = xi+W
            yi7 = yi

        # fourth quadrant (top right)
        if xi >= x0 and yi < y0 and abs(xi-x0) >W:
            xi6 = x0+W
            yi6 = y0
            xi7 = xi-W
            yi7 = yi


        xi62 = 0.5*(xi6+xi2)
        yi62 = 0.5*(yi6+yi2)

        """
        if self.darkMode:
            pen = QtGui.QPen(QtGui.QColor(50, 50, 50 , 255),0) # ,2, Qt.SolidLine)
        else:
            pen = QtGui.QPen(QtGui.QColor(0, 0,0 , 255),0) # ,2, Qt.SolidLine)
        """
        #pen = QtGui.QPen(self.parent().theme_manager.get_color(1),0)
        
        # https://stackoverflow.com/questions/52014197/how-to-interpolate-a-2d-curve-in-python
        # define some points
        end_is_start = False
        if abs(x0-xf) < 0.0002 and abs(y0-yf) < 0.0002:
            end_is_start = True
            points2 = np.array([[x0,xi62,xi2,xi,xd],
                                [y0,yi62,yi2,yi,yd]]).T
        else:
            # points2 = np.array([[x0,xi6,xi62,xi2,xi,xd,xi3,xi35,xi5,xf],
            #              [y0,yi6,yi62,yi2,yi,yd,yi3,yi35,yi5,yf]]).T

            points2 = np.array([[x0,xi,xd,xf],
                                 [y0,yi,yd,yf]]).T

           
        points = points2 #  np.array(points2)
        assert len(points) > 0, "empty points"

        xt, yt = 0, 0
        # Linear length along the line:
        try:
            distance = np.cumsum( np.sqrt(np.sum( np.diff(points, axis=0)**2, axis=1 )) )
            distance = np.insert(distance, 0, 0)

            if len(distance)  > 0:
                distance = distance/distance[-1]
            
            # Interpolation for different methods:
            # interpolations_methods = ['slinear', 'quadratic', 'cubic']

            if not self.show_raster:
                if not end_is_start:
                    # method = "quadratic"
                    method  = self.interpol_method
                    
                    # for method in interpolations_methods:
                    interpolated_points =  my_interpolate(points, method = method)
                    # interpolated_points = interpol(alpha)

                else:
                    # method  = self.interpol_method
                    # alpha = np.linspace(0, 1, 60) #np.linspace(0, 1, 80)

                    c0x = 0.5*(xi-x0)
                    c0y = 0.5*(yi-y0)

                    rx = max(3,0.5 * np.sqrt((x0-xi)**2 + (y0-yi)**2) )
                    ry = max(3,0.5 * np.sqrt((xi-xd)**2 + (yi-yd)**2) )

                    t0 = np.arctan2((yi-y0) ,(xi-x0))

                    t = t0 + np.linspace(0,2*np.pi, 100)

                    x_coords = [rx * np.cos(ti) * np.cos(t0) - ry * np.sin(ti) * np.sin(t0) + c0x + x0 for ti in t]
                    y_coords = [rx * np.cos(ti) * np.sin(t0) + ry* np.sin(ti) * np.cos(t0) + c0y + y0 for ti in t]

                    interpolated_points = list(zip(x_coords,y_coords))


            else:
                method = "linear"
                interpolated_points =  my_interpolate(points, method = method, n=10)
                
                x0 = (interpolated_points[0][0])
                y0 = (interpolated_points[0][1])
                xf = (interpolated_points[-1][0])
                yf = (interpolated_points[-1][1])
                
                if not end_is_start:
                    
                    # construct a line
                    xc1, yc1 = self.manhattan_lines(x0,y0,xi,yi,111)
                    xc2, yc2 = self.manhattan_lines(xi,yi,xd,yd,111)
                    xc3, yc3 = self.manhattan_lines(xd,yd,xf,yf,111)

                    x_coords = xc1 + xc2 + xc3
                    y_coords = yc1 + yc2 + yc3
                else:
                    # construct a circle with center point x0, y0
                    # through xi,yi

                    c0x = 0.5*(xi-x0)
                    c0y = 0.5*(yi-y0)

                    rx = max(3,0.5 * np.sqrt((x0-xi)**2 + (y0-yi)**2) )
                    ry = max(3,0.5 * np.sqrt((xi-xd)**2 + (yi-yd)**2) )

                    t0 = np.arctan2((yi-y0) ,(xi-x0))

                    t = t0 + np.linspace(0,2*np.pi, 100)

                    x_coords = [rx * np.cos(ti) * np.cos(t0) - ry * np.sin(ti) * np.sin(t0) + c0x + x0 for ti in t]
                    y_coords = [rx * np.cos(ti) * np.sin(t0) + ry* np.sin(ti) * np.cos(t0) + c0y + y0 for ti in t]      

                interpolated_points = list(zip(x_coords,y_coords))


            if self.show_raster and not end_is_start:
                interpolated_points = [(self.raster(t[0]), self.raster(t[1])) for t in interpolated_points]

            # draw connector line as path
            n = len(interpolated_points)

            # print("interpolated_poitns",interpolated_points)
            # draw curved line
            offset_text = 20

            qp.setRenderHint(QPainter.Antialiasing)

            connPath = QPainterPath()
            startx = self.zoom_factor*(interpolated_points[0][0])
            starty = self.zoom_factor*(interpolated_points[0][1])

            connPath.moveTo(startx,starty)

            for i in range(1,int(n/2)-offset_text):
                #qp.drawLine(self.zoom_factor*(interpolated_points[i-1][0]),self.zoom_factor*(interpolated_points[i-1][1]),
                #            self.zoom_factor*(interpolated_points[i][0]),self.zoom_factor*(interpolated_points[i][1]))
                connPath.lineTo(self.zoom_factor*(interpolated_points[i][0]),self.zoom_factor*(interpolated_points[i][1]))

            for i in range(int(n/2)-offset_text,len(interpolated_points)):
                #qp.drawLine(self.zoom_factor*(interpolated_points[i-1][0]),self.zoom_factor*(interpolated_points[i-1][1]),
                #            self.zoom_factor*(interpolated_points[i][0]),self.zoom_factor*(interpolated_points[i][1]))
                connPath.lineTo(self.zoom_factor*(interpolated_points[i][0]),self.zoom_factor*(interpolated_points[i][1]))

            connPath.moveTo(startx,starty)
            connPath.closeSubpath()
            qp.drawPath(connPath)

            
            # draw arrow at midpoint
            md = int(n/2.)
            # rint("md", md)
            # interpolated_points = [(self.raster(p[0],50),self.raster(p[1],50)) for p in interpolated_points]
            draw_reverse = False

            if not end_is_start:
                xt, yt = self.draw_arrow_at(qp,interpolated_points,md,dashed=dashed)

            if not is_unidir: # double arrow for non-unidirectional
                if not end_is_start and self.main_widget.SETTING_show_arrows: # .checkBox.isChecked():
                    if not draw_reverse:

                        if self.main_widget.SETTING_show_raster: # checkBox_raster.isChecked():
                            md = int(n/2.)
                        else:
                            md = int(n/2. - 15)

                        xt, yt = self.draw_arrow_at(qp, interpolated_points,md, reverse=True,dashed=dashed)


            # interpolated_points = [(self.raster(t[0]), self.raster(t[1])) for t in points]


        except Exception as e:
            print("Error in Line Interpolation:",str(e))
            traceback.print_exc()
            xt = 0
            yt = 0

       
        # return position for label
        return xt,yt, interpolated_points

    def drawPreview(self,qp):
        # draw small preview window
        # param qp: a qpainter object
        if not self.main_widget.SETTING_show_preview: # checkBox_9.isChecked():
            return

        pen = QtGui.QPen(self.main_widget.theme_manager.get_color(2),1)
        qp.setPen(pen)

        # draw Origin
        #qp.drawLine(self.zoom_factor*(0.5*self.W0-5+self.gox),self.zoom_factor*(0.5*self.H0+self.goy),self.zoom_factor*(0.5*self.W0+5+self.gox),self.zoom_factor*(0.5*self.H0+self.goy))
        #qp.drawLine(self.zoom_factor*(0.5*self.W0+self.gox),self.zoom_factor*(0.5*self.H0-5+self.goy),self.zoom_factor*(0.5*self.W0+self.gox),self.zoom_factor*(0.5*self.H0+5+self.goy))

        w_frame = 80
        h_frame = 80
        distance = 25
        move_x = 10 # self.W/2 #  distance + 10
        move_y = 10 # -25

        brush = QtGui.QBrush(QtGui.QColor(80,80,80 , 100),1) # QtGui.QBrush(self.parent().theme_manager.get_color(3)) #7
        qp.fillRect(QtCore.QRectF(move_x-5,move_y-5,w_frame+10,h_frame+10),brush)

        if len(self.boxes) == 0:
            return

        x_min = min(0,-self.gox)
        x_max = max(self.W,self.W-self.gox)
        y_min = min(0,-self.goy)
        y_max = max(self.H,self.H-self.goy)
        x_vals = []
        y_vals = []

        for box in self.boxes:
            x_vals.append(box.x)
            y_vals.append(box.y)


        x_max = max(np.max(x_vals),-self.gox)
        y_max = max(np.max(y_vals),-self.goy)
        x_min = min(np.min(x_vals),-self.gox)
        y_min = min(np.min(y_vals),-self.goy)

        if x_max == x_min:
            x_max = x_min + 10
        if y_max == y_min:
            y_max = y_min + 10

        if x_max < self.W:
            x_max = self.W
        if y_max < self.H:
            y_max = self.H

        x_vals = [self.zoom_factor*(xi+self.gox)/10 for xi in x_vals]
        y_vals = [self.zoom_factor*(yi+self.goy)/10 for yi in y_vals]

        # origin
        x_vals.append(self.zoom_factor/10 *(self.gox + 0.5*self.W))
        y_vals.append(self.zoom_factor/10 *(self.goy + 0.5*self.H))

        # pointer
        x_vals.append(1/10 *(0.5*self.W))
        y_vals.append(1/10 *(0.5*self.H))

        mx = np.mean(x_vals)
        my = np.mean(y_vals)

        x_vals = [x-mx for x in x_vals]
        y_vals = [y-my for y in y_vals]

        dx = max(-np.min(x_vals),np.max(x_vals))
        dy = max(-np.min(y_vals),np.max(y_vals))

        x_vals = [0.5*w_frame*x/dx for x in x_vals]
        y_vals = [0.5*h_frame*y/dy for y in y_vals]

        x_vals = [x+0.5*w_frame for x in x_vals]
        y_vals = [y+0.5*h_frame for y in y_vals]

        x_vals = [move_x +int(distance/2)+x-2-10 for x in x_vals]
        y_vals = [move_y +int(distance/2)+y-2-10 for y in y_vals]

        # transform all points

        for i in range(len(x_vals)-2):
            x = int(x_vals[i])
            y = int(y_vals[i])
            if self.boxes[i].ishelper:
                    qp.drawRect(x,y,1,1)
            else:
                #if self.boxes[i] == self.highlighted:
                #    pen = QtGui.QPen(self.parent().theme_manager.get_color(3),1)
                #    brush = QtGui.QBrush(self.parent().theme_manager.get_color(3))
                #    qp.setPen(pen)

                #    qp.fillRect(QtCore.QRectF(x,y,4,4),brush)
                #else:
                qp.drawRect(x,y,4,4)
        
        # skip origin ...
        x = x_vals[-1]
        y = y_vals[-1]
        pen = QtGui.QPen(self.main_widget.theme_manager.get_color(4),1)
        qp.setPen(pen)
        ox = x
        oy =  y
        d = 5
        qp.drawLine(int(ox-d),int(oy),int(ox+d),int(oy))
        qp.drawLine(int(ox),int(oy-d),int(ox),int(oy+d))

        # draw Origin NOTE turned off for now, check in later versions
        # ox = (self.gox-x_min)/(x_max-x_min)*(w_frame-distance)
        # oy = (self.goy-y_min)/(y_max-y_min)*(h_frame-distance)
        # dx = 5
        # dy = 5
        # ox_transformed = move_x + self.W-w_frame+ox-2
        # oy_transformed = move_y + self.H-h_frame+oy-2

        # qp.drawLine(ox_transformed-dx,oy_transformed,ox_transformed+dx,oy_transformed)
        # qp.drawLine(ox_transformed,oy_transformed-dy,ox_transformed,oy_transformed+dy)

    def raster(self, x, n=19):
        if self.show_raster: # show rasterized
            # x_rastered = np.round(x,int(-np.log10(self.raster_size)))
            m = min(n,self.raster_size)
            x_rastered = (int(1.0 * x/m))*m  # np.round(x,int(-np.log10(self.raster_size)))
            return int(x_rastered)
        return int(x)
        
    def paint(self,reference, painter, ratio=1):

        qp = painter # QtGui.QPainter(reference)
        qp.scale(ratio, ratio)
        
        #qp.setRenderHint(QtGui.QPainter.SmoothPixmapTransform )
        qp.setRenderHint(QtGui.QPainter.HighQualityAntialiasing  )
        qp.setRenderHint(QtGui.QPainter.TextAntialiasing  )

        font = QFont("Segue UI", 12)
        #font.setBold(True)
        qp.setFont(font)

        for box in self.boxes:
            box.x = self.raster(box.x)
            box.y = self.raster(box.y)

        """
        if self.darkMode:
            pen = QtGui.QPen(QtGui.QColor(120, 120, 120, 255),1) # ,2, Qt.SolidLine)
        else:
            pen = QtGui.QPen(QtGui.QColor(0, 0, 0, 255),1) # ,2, Qt.SolidLine)
        """
        pen = QtGui.QPen(self.main_widget.theme_manager.get_color(2),1)
        qp.setPen(pen)

        if self.main_widget.SETTING_white_bg: # checkBox_7.isChecked(): # draw boundary rect with white background?
            br_white = QtGui.QBrush(QtGui.QColor(255, 255, 255),1)
            qp.fillRect(QtCore.QRectF(QtCore.QPointF(0,0),QtCore.QPointF(int(self.W-2),int(self.H-2))),br_white) # background
            if reference == self:
                qp.drawRect(0,0,self.W-2,self.H-4)
        else:
            if reference == self:
                qp.drawRect(0,0,self.W-2,self.H-4)


        # draw grid 
        if self.main_widget.SETTING_show_raster: # checkBox_raster_2.isChecked():
            for i in np.arange(0, int(self.W-2), self.raster_size):
                for j in np.arange(0, int(self.H-2),self.raster_size):
                    qp.drawRect(int(i)-1,int(j)-1,2,2)

        qp.setPen(pen)

        # filter_vals = set([]) 
        filter_vals =  set(self.main_widget.filter_gui) # [item.text() for item in self.main_widget.filter_gui])
        
        active_boxes = []
        active_connectors = []
        
        # try to retrieve mouse 
        m_pos = self.mouse_pos 
        mx, my = m_pos.x()/self.zoom_factor, m_pos.y()/self.zoom_factor
        # print("mouse_pos", mx, my)
    
        # build 'active connectors' 
        for conn in self.connectors:

            # connectors near the mouse        
            dx = 3
            dy = 3
            w = 20

            if (conn.curve is not None) and self.hit_conn(mx, my, conn.curve, w):
                active_connectors.append(conn)
                active_boxes.append(conn.a)
                active_boxes.append(conn.b)
                active_boxes.append(conn.c)
                active_boxes.append(conn.d)

                #print("*", conn, "close")
            # print(conn, "not close", conn.c.x, conn.c.y)
            
            # selected connectors 
            no_selection = len(filter_vals) == 0
            is_selected = len(filter_vals) != 0 and len(list(set(conn.items) & filter_vals)) > 0 
            if no_selection or is_selected:
                if conn not in active_connectors:
                    if self.highlighted is None:
                        active_connectors.append(conn)
                    else:
                        if conn.a == self.highlighted or conn.b == self.highlighted or conn.c == self.highlighted:
                            active_connectors.append(conn)

            active_connectors = list(set(active_connectors))

        for conn in self.connectors:

            is_unidir = True
            is_flow = False

            if conn.c == self.highlighted:

                pen = QtGui.QPen(self.main_widget.theme_manager.get_color(4),2.0) # ,2, Qt.SolidLine)

                is_unidir = True
                for i,data in enumerate(self.main_widget.entry_data): # TODO make this more efficient

                    # print("Connector data", data)
                    if data["shortname"] == conn.c.name:

                        if str(data["uni-directional"]) == "False":
                            is_unidir = False

                        break
            else:
                # pen = QtGui.QPen(QtGui.QColor(0, 0, 0, 200),1) # ,2, Qt.SolidLine)

                """
                if self.darkMode:
                    pen = QtGui.QPen(QtGui.QColor(120, 120, 120, 255),1) # ,2, Qt.SolidLine)
                else:
                    pen = QtGui.QPen(QtGui.QColor(0, 0,0 , 75),0) # ,2, Qt.SolidLine)
                """

                pen = QtGui.QPen(self.main_widget.theme_manager.get_color(2),1.5)

                if conn in active_connectors:

                    # is actually a flow?
                    is_flow = False
                    is_unidir = True

                    for i, data in enumerate(self.main_widget.entry_data): # TODO make this more efficient
                        # print("connector data", data)
                        if data["shortname"] == conn.c.name:

                            if str(data["log transaction"]) == "True":
                                is_flow = True

                            if str(data["uni-directional"]) == "False":
                                is_unidir = False

                            break;

                    if is_flow:
                        pen = QtGui.QPen(self.main_widget.theme_manager.get_color(2),1.5)
                    else:
                        pen = QtGui.QPen(self.main_widget.theme_manager.get_color(4),1.5)

                    pen.setStyle(Qt.SolidLine)
                    pen.setWidthF(1.5)

                else:
                    pen.setStyle(Qt.DashLine)
                    #pen.setDashOffset(4.0)
                    pen.setDashPattern([1,4])
                    pen.setWidthF(1.1)

                    is_flow = False
                    is_unidir = True

            qp.setPen(pen)

            # apply filter: does the connector contain any of the filter_vals?

            show = True
            offset_box = 25

            if conn in active_connectors:
                # draw only if filter applies

                tx,ty, interpoints = self.curved_connector(qp,self.gox + int(conn.a.x+offset_box),
                                                 self.goy + int(conn.a.y+offset_box),
                                                 self.gox + int(conn.b.x+offset_box),
                                                 self.goy + int(conn.b.y+offset_box),
                                                 # self.gox + int(conn.c.x+5), self.goy + int(conn.c.y+5),
                                                 # self.gox + int(conn.d.x+5), self.goy + int(conn.d.y+5),
                                                 self.gox + int(conn.c.x), self.goy + int(conn.c.y),
                                                 self.gox + int(conn.d.x), self.goy + int(conn.d.y),
                                                 is_flow=is_flow,
                                                 is_unidir=is_unidir,
                                              )
                conn.curve = interpoints 

                # self.curved_connector(qp,int(conn.a.x), int(conn.a.y),int(conn.b.x), int(conn.b.y),int(conn.c.x), int(conn.c.y))
                
                if conn.c == self.highlighted:
                    pen = QtGui.QPen(self.main_widget.theme_manager.get_color(6),1.5)
                    qp.setPen(pen)
                else:
                    pen = QtGui.QPen(self.main_widget.theme_manager.get_color(1),1.5)
                    qp.setPen(pen)


                if self.main_widget.SETTING_text_at_handle: # checkBox_5.isChecked(): # draw at handles
                    if self.main_widget.SETTING_show_labels: # checkBox_2.isChecked():


                        if self.highlighted == conn.c:
                            brush = QtGui.QBrush(self.main_widget.theme_manager.get_color(4),1)
                        else:
                            brush = QtGui.QBrush(self.main_widget.theme_manager.get_color(3),1)

                        fontsize = 8

                        try:
                            fontsize = int(self.main_widget.SETTING_fontsize_2) #spinBoxFontSize_2.value())
                        except Exception as e:
                            #print(str(e))
                            pass 
                        qp.setFont(QFont("Arial", fontsize))

                        conn.c.label.draw(qp,brush)

                       
                
                else: # do not draw at handles
                    if self.main_widget.SETTING_text_at_handle: # .checkBox_2.isChecked():
                        if self.highlighted == conn.c:
                            brush = QtGui.QBrush(self.main_widget.theme_manager.get_color(4),1)
                        else:
                            brush = QtGui.QBrush(self.main_widget.theme_manager.get_color(3),1)


                        if conn.a.x != conn.b.x and conn.a.y != conn.b.y:
                            # label ends and starts at different box

                            a = self.zoom_factor*(tx)
                            b = self.zoom_factor*(ty)

                            a2 = self.zoom_factor*(tx+5)
                            b2 = self.zoom_factor*(ty+5)


                        else: # label ends and starts at same box
                            a = self.zoom_factor*(self.gox +conn.c.x)
                            b = self.zoom_factor*(self.goy + conn.c.y)

                            a2 = self.zoom_factor*(self.gox +conn.d.label.x)
                            b2 = self.zoom_factor*(self.goy + conn.d.label.y)

                        fontsize = 8

                        try:
                            fontsize = int(self.main_widget.SETTING_fontsize_2) # .spinBoxFontSize_2.value())
                            
                        except Exception as e:
                            #print(str(e))
                            pass 

                        qp.setFont(QFont("Arial", fontsize))


                        conn.c.label.draw(qp, brush, position = (a2,b2))

                        # TODO add conn.curve here 
                        """
                        DEPRICATED
                        TODO remove in future versions

                        # draw agent labels


                        if self.main_widget.checkBox_4.isChecked():
                            # text_length = 5.1*len(conn.c.name)

                            fm = qp.fontMetrics()
                            text_length = fm.boundingRect(conn.c.name.replace("_"," ")).width() + 2
                            text_height = fm.boundingRect(conn.c.name.replace("_"," ")).height() + 2

                            qp.fillRect(QtCore.QRectF(QtCore.QPointF(int(a-.1),int(b-text_height+1)),QtCore.QPointF(int(a+text_length),int(b))),brush)
                            # qp.fillRect(QtCore.QRectF(QtCore.QPointF(int(a2-.1),b2-10),QtCore.QPointF(int(a2+5.1*len(conn.d.name)),b2+10)),brush)


                        qp.drawText(int(a),int(b)-1,conn_label_name)

                        # draw second connector
                        """

            else:

                tx,ty, interpoints = self.curved_connector(qp,self.gox + int(conn.a.x+offset_box),
                                                 self.goy + int(conn.a.y+offset_box),
                                                 self.gox + int(conn.b.x+offset_box),
                                                 self.goy + int(conn.b.y+offset_box),
                                                 self.gox + int(conn.c.x+5), self.goy + int(conn.c.y+5),
                                                 self.gox + int(conn.d.x+5), self.goy + int(conn.d.y+5),
                                                 is_flow=is_flow,
                                                 is_unidir=is_unidir
                                              )

                conn.curve = interpoints


        pen = QtGui.QPen(self.main_widget.theme_manager.get_color(2),1)
        pen.setStyle(Qt.SolidLine)

        # pen = QtGui.QPen(QtGui.QColor(0, 0,0 , 200),1) # ,2, Qt.SolidLine)
        qp.setPen(pen)

        # print("self.boxes",[b.name for b in self.boxes])

        """
        FOR DEBUGGING LABELS
        for box in self.boxes:
            if box.name != "":
                z = self.zoom_factor
                a = z*(box.label.x + box.label.offx +self.gox )
                b = z*(box.label.y + box.label.offy -11+self.goy ) # self.zoom_factor*(y-7+self.goy)

                w = z* 25
                w2 = z* 15
                qp.drawRect(QtCore.QRectF(a,b,w,w2))
        """

        for box in self.boxes:

            x = box.x
            y = box.y
        
            
            if box == self.selected:
                br = QtGui.QBrush(QtGui.QColor(34, 34, 230, 255))
            elif box == self.connected1:
                br = QtGui.QBrush(QtGui.QColor(100, 10, 10, 255))
            elif box.ishelper and box != self.highlighted:

                """
                if self.darkMode:
                    br = QtGui.QBrush(QtGui.QColor(255, 255, 255))
                else:
                    br = QtGui.QBrush(QtGui.QColor(74, 138, 217, 255))
                """
                br = QtGui.QBrush(self.main_widget.theme_manager.get_color(0),1)


            elif box == self.highlighted:
                br = QtGui.QBrush(self.main_widget.theme_manager.get_color(5))
            
            else:
                """
                if self.darkMode:
                    br = QtGui.QBrush(QtGui.QColor(20, 20, 20, 255))
                else:
                    br = QtGui.QBrush(QtGui.QColor(200, 200, 200, 255))
                """
                br = QtGui.QBrush(self.main_widget.theme_manager.get_color(0),1)

            x = int(x)
            y = int(y)

            if box.ishelper:
                pen = QtGui.QPen(self.main_widget.theme_manager.get_color(6),1)

                if box.is_support:
                    br = QtGui.QBrush(self.main_widget.theme_manager.get_color(7),1)
                else:
                    br = QtGui.QBrush(self.main_widget.theme_manager.get_color(1),1)

                qp.setPen(pen)
                # qp.drawText(x,y-7,box.name)

            else:
                #qp.drawText(x,y-3,box.name.capitalize())

                # draw agent labels


                #a = self.zoom_factor*(x+self.gox)
                #b = self.zoom_factor*(y-11+self.goy) # self.zoom_factor*(y-7+self.goy)
                a = self.zoom_factor*(x+self.gox )
                b = self.zoom_factor*(y-11+self.goy ) # self.zoom_factor*(y-7+self.goy)

                pen = QtGui.QPen(self.main_widget.theme_manager.get_color(6),1)
                qp.setPen(pen)


                # draw agent labels (box labels)
                fontsize = 12

                try:
                    fontsize = int(self.main_widget.SETTING_fontsize) # spinBoxFontSize.value())
                    
                except Exception as e:
                    # print(str(e))
                    pass 

                qp.setFont(QFont("Arial", fontsize))


                if self.main_widget.SETTING_highlight_labels: # checkBox_4.isChecked():

                    br_label = QtGui.QBrush(self.main_widget.theme_manager.get_color(3),1)
                else:
                    br_label = None

                
                box.label.draw(qp,br_label)


            qp.setBrush(br)

            if box.ishelper:

                x2 = x + 6
                y2 = y + 6

                pen = QtGui.QPen(QtGui.QColor(0, 0, 0 , 0),0) # ,2, Qt.SolidLine)
                    
                
                qp.setPen(pen)

                # qp.drawRect(QtCore.QRectF(QtCore.QPointF(x,y),QtCore.QPointF(x2,y2)))

                if box.is_support:
                    show_box = self.main_widget.SETTING_show_support # checkBox_6.isChecked()
                else:
                    show_box = self.main_widget.SETTING_show_handles #checkBox_3.isChecked()

                if show_box:
                    if box in active_boxes:

                        """
                        if box.is_support:
                            qp.drawEllipse(QtCore.QPointF(self.zoom_factor*(self.gox + x+4),self.zoom_factor*(self.goy + y+4)),6, 6)
                        else:
                            qp.drawEllipse(QtCore.QPointF(self.zoom_factor*(self.gox + x+4),self.zoom_factor*(self.goy + y+4)),6, 6)
                        """
                        box.draw(qp,active=True)

                    else:

                        """
                        if box.is_support:
                            qp.drawEllipse(QtCore.QPointF(self.zoom_factor*(self.gox + x+4),self.zoom_factor*(self.goy + y+4)),2, 2)
                        else:
                            qp.drawEllipse(QtCore.QPointF(self.zoom_factor*(self.gox + x+4),self.zoom_factor*(self.goy + y+4)),2, 2)
                        """
                        box.draw(qp,active=False)

                """
                if self.darkMode:
                    pen = QtGui.QPen(QtGui.QColor(120, 120, 120 , 200),0) # ,2, Qt.SolidLine
                else:
                    pen = QtGui.QPen(QtGui.QColor(0, 0, 0 , 200),0) # ,2, Qt.SolidLine)
                """
                pen = QtGui.QPen(self.main_widget.theme_manager.get_color(2),1)
                qp.setPen(pen)

            else:

                #W = 45

                #x2 = x + W
                #y2 = y + W

                pen = QtGui.QPen(QtGui.QColor(0, 0, 100 , 0),0) # ,2, Qt.SolidLine)
                #br = QtGui.QBrush(QtGui.QColor(0, 0, 100, 255))
                qp.setPen(pen)
                # qp.setBrush(br)


                if box in active_boxes:
                    box.draw(qp,brush=br,active=True)
                else:
                    box.draw(qp,brush=br,active=False)

                """
                if self.darkMode:
                    pen = QtGui.QPen(QtGui.QColor(120,120,120 , 255),0) # ,2, Qt.SolidLine)
                else:
                    pen = QtGui.QPen(QtGui.QColor(0, 0,0 , 255),0) # ,2, Qt.SolidLine)
                """

                pen = QtGui.QPen(self.main_widget.theme_manager.get_color(3),1)
                qp.setPen(pen)
        self.drawPreview(qp)
        
        # return qp

    def paintEvent(self, event):
        qp = QtGui.QPainter(self)
        try:
            qp.save()                     # balance
            self.paint(self, painter=qp)
        except Exception as e:
            print("EXCEPTION:", str(e))
        finally:
            qp.restore()
            qp.end()
            
    def hit_conn(self, x, y, path, w):
        # path_x, path_y = path 
        for px, py in path:
            # Calculate the Euclidean distance
            distance = ((x - px) ** 2 + (y - py) ** 2) ** 0.5
            if distance <= w:
                return True
        return False

    def hit_box(self,x,y,xb,yb,wx,wy):
        """
        check if (x,y) lies within the rectangle
        (xb, yb), (xb, yb+wy), ...., (xb+wx, yb+wy)
        """
        if wx < 30:
            cond1 = self.zoom_factor*(xb+self.gox - wx)  < x*self.zoom_factor  < self.zoom_factor*(xb+ self.gox + wx)
            cond2 = self.zoom_factor*(yb+self.goy - wy) < y*self.zoom_factor  < self.zoom_factor*(yb + wy + self.goy)
        else:
            cond1 = self.zoom_factor*(xb+self.gox)  < x*self.zoom_factor  < self.zoom_factor*(xb+ self.gox + wx)
            cond2 = self.zoom_factor*(yb+self.goy) < y*self.zoom_factor  < self.zoom_factor*(yb + wy + self.goy)
        return cond1 and cond2

    # Handle double left-click event
    def mouseDoubleClickEvent(self, event):
        
        self.main_widget.update_graphics_data(self.boxes,self.connectors)

        if event.button() == QtCore.Qt.LeftButton:
            self.mode = "select" 
            print("double select")
            self.block_drag = True
                
            self.update_selections(event)

    def mousePressEvent(self, event):
        print("MOUSE PRESS DRAW WIDGET", "mode", self.mode) 
        self.main_widget.update_graphics_data(self.boxes,self.connectors)
        
        #self.mode = "drag"
        #print("drag")

        x, y = event.pos().x(), event.pos().y()
        self.startx = x 
        self.starty = y
        
        if event.buttons() & QtCore.Qt.LeftButton:# ok 
            
            if self.highlighted is None:
                if not self.block_drag:
                    self.update_selections(event)

            if self.hover_object is not None:
                print("dragging", self.hover_object)

                self.mode="drag"
                self.dragbox = self.hover_object
                self.dragbox.x0 = self.dragbox.x 
                self.dragbox.y0 = self.dragbox.y 
                
            else:
                self.mode = "select"
        
        
        elif event.buttons() & QtCore.Qt.RightButton: # ok 
            self.mode = "move" 

            print("click", x, y)
            self.gox_start = self.gox
            self.goy_start = self.goy 
        
        
        else:
            self.mode = "move" 
    

    def update_selections(self, event):

        time_delay = time.time() - self.last_click
        self.last_click = time.time()

        x,y = event.pos().x(), event.pos().y()

        x = int(x) #/self.zoom_factor)
        y = int(y) # /self.zoom_factor)

        

        # check if any box was selected
        box_hit = False
        self.selected = None

        # check for labels
        for box in self.boxes:

            if self.mode != "move" and self.hit_box(x/self.zoom_factor, y/self.zoom_factor, box.label.x , box.label.y , 25, 15):
                if (self.hover_object is not None) and (self.hover_object == box.label):
                    self.mode = "drag_label"
                    self.selected = box.label
                    print("selected", box.name)
        
        # check for box movements
        for box in self.boxes:
            if box.ishelper:
                w = 20
            else:
                w = 50

            if self.mode != "move" and self.hit_box(x/self.zoom_factor, y/self.zoom_factor, box.x, box.y, w, w):
                print("box hit", box.name, box.x, box.y, "mode", self.mode)
                # self.mode = "drag"
                
                box_hit = True
                if self.mode == "drag":
                    self.selected = box
                    print("selected for dragging", box, "at", box.x, box.y)
                    self.dragbox = box 
                    return 
                
                elif self.mode == "select":
                    if time_delay < 0.02: # double click on agent to edit
                        print(" ....double clicked")
                        if self.highlighted is None:
                            self.highlighted = box
                        
                        self.edit_agent(raise_notification=False)
                        self.highlighted = None
                        return
                    else:
                        if self.highlighted == box:
                            self.highlighted = None
                            #print("unhighlight", box)
                            self.block_drag = False 
                            self.update()
                        else:
                            #print("highlight",box)
                            self.highlighted = box
                            self.block_drag = True 

                    # self.mode = "drag"
                    if box.ishelper and self.highlighted == box:
                        self.main_widget.transaction_select_where(box.name)
                    self.update()
                
                elif self.mode == "edit":                    
                    self.mode = "select"
                
                elif self.mode == "delete":
                    raise RuntimeError("Delete mode not supported")
                
                elif self.mode == "connect":
                    self.selected = None

                    if self.connected1 is None:
                        self.connected1 = box
                        self.update()

                    elif self.connected2 is None and not box.ishelper:
                        self.connected2 = box
                        con = Connector(self.connected1,self.connected2)
                        self.connectors.append(con)
                        self.boxes.append(con.c)
                        self.boxes.append(con.d)
                        self.connected1 = None
                        self.connected2 = None
                        # self.mode = "drag"
        
        if not box_hit and event.buttons() & QtCore.Qt.RightButton: # ok 
            #print("Move")
            self.mode = "move"
        
    def mouseMoveEvent(self,event):
        """
        if event.buttons() & QtCore.Qt.LeftButton: # ok
                            self.mode = "drag"
        """

        x, y = event.pos().x(), event.pos().y()
        self.mouse_pos = event.pos()
       
        try:
            x = int(x) # /self.zoom_factor)
            y = int(y) # /self.zoom_factor)
            #self.mousex = x
            #self.mousey = y

            if self.mode == "move" and self.startx is not None:
                
                dx = (x - self.startx)/ self.zoom_factor
                dy = (y - self.starty)/self.zoom_factor
                # print("move", dx,dy)

                self.gox = self.gox_start + dx #self.raster(self.gox_start + dx)
                self.goy = self.goy_start + dy #self.raster(self.goy_start + dy)
                assert not np.isnan(self.gox)
                assert not np.isnan(self.goy)

            elif self.mode == "drag_label" and (self.selected is not None):
                
                dx = (x - self.startx)/ self.zoom_factor
                dy = (y - self.starty)/self.zoom_factor

                self.selected.x += dx 
                self.selected.y += dy
                
            if self.mode == "drag":
                dx = (x - self.startx)/ self.zoom_factor
                dy = (y - self.starty)/self.zoom_factor

                box = self.dragbox
                # print("drag", dx,dy, "-->", box.x, box.y, "to", box.x + dx, box.y + dy) < for debugging
                box.x = box.x0 + dx #self.raster(self.gox_start + dx)
                box.y = box.y0 + dy #self.raster(self.goy_start + dy)
                
            else:
                # print("other", x, y)
                hovering = False
                hoverbox = None 
                for box in self.boxes:
                    
                    if box.ishelper:
                        w = 20
                        w2x, w2y = 35, 15
                        cond = self.zoom_factor*(box.x+self.gox - w)  < x  < self.zoom_factor*(box.x+self.gox + w) and self.zoom_factor*(box.y+self.goy - w) < y  < self.zoom_factor*(box.y + w+self.goy)
                        cond2 = self.zoom_factor*(box.label.x+self.gox)  < x  < self.zoom_factor*(box.label.x+self.gox + w2x) and self.zoom_factor*(box.label.y+self.goy) < (y+6)  < self.zoom_factor*(box.label.y + w2y+self.goy)

                    else:
                        w = 50
                        w2x, w2y = 35, 15
                        cond = self.zoom_factor*(box.x+self.gox)  < x  < self.zoom_factor*(box.x+self.gox + w) and self.zoom_factor*(box.y+self.goy ) < y  < self.zoom_factor*(box.y + w+self.goy)
                        cond2 = self.zoom_factor*(box.label.x+self.gox)  < x  < self.zoom_factor*(box.label.x+self.gox + w2x) and self.zoom_factor*(box.label.y+self.goy) < (y+6)  < self.zoom_factor*(box.label.y + w2y+self.goy)

                    if cond :

                        self.setCursor(Qt.OpenHandCursor)
                        hovering = True
                        hoverbox = box 

                    elif cond2:
                        
                        self.setCursor(Qt.OpenHandCursor)
                        hovering = True 
                        hoverbox = box.label 

                        print("hover", hoverbox)

                if not hovering:
                    self.setCursor(self.standard_cursor)
                    self.hover_object = None
                else:
                    self.hover_object = hoverbox  
        except Exception as e:
            print("EXCEPTION", str(e))
        
        self.update()
        
    def mouseReleaseEvent(self, event):
        
        # print("release")
        self.block_drag = False    

        if self.mode == "drag_label":
            if self.selected is not None:
                
                self.old_positions[self.selected.name+"<label>"] = (self.selected.x, self.selected.y)
                self.label_position_data[self.selected.name+"<label>"] = {"x":self.selected.x, "y":self.selected.y}
                
                self.mode = "select"

        elif self.mode == "drag":
             
            x, y = event.pos().x(), event.pos().y()

            x = int(x) # /self.zoom_factor) # self.raster(int(x/self.zoom_factor),m)
            y = int(y) # /self.zoom_factor) # self.raster(int(y/self.zoom_factor),m)

            self.mousex = x
            self.mousey = y
            
            selected = self.dragbox # self.highlighted 

            if selected is not None:

                # dx = self.selected 
                print(">>> move ", selected.name, "to", selected.x, selected.y)
                self.old_positions[selected.name] = (selected.x, selected.y)
                try:
                    self.old_positions[selected.label.name + "<label>"] = (selected.label.x, selected.label.y)
                    self.label_position_data[selected.name+"<label>"] = {"x":selected.label.x, "y":selected.label.y}
                except:
                    pass
                
                self.selected = None
            
            self.mode = "move"

            self.update()

        elif self.mode == "move":
            print("MOVE RELEASE")
            x, y = event.pos().x(), event.pos().y()
            dx = x - self.startx 
            dy = y - self.starty 
            

        self.mode = None
        self.selected = None 