#!/usr/local/bin/python3.13
#
# Copyright 2012-2016 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
#

from argparse import ArgumentParser

import sys
import time
import re
import signal
import random
import math
import operator
import thrift

from gnuradio.ctrlport.GNURadioControlPortClient import GNURadioControlPortClient
from PyQt5 import QtCore, Qt

try:
    import networkx as nx
    import matplotlib
    matplotlib.use("QT5Agg")
    import matplotlib.pyplot as plt
    from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas

    try:
        from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
    except ImportError as e:
        print(e)
        print(sys.argv[0], "could not load QTAgg backend.")
        sys.exit(1)

    from matplotlib.figure import Figure
except ImportError:
    print(sys.argv[0], "requires networkx and matplotlib.",
          "Please check that they are installed and try again.")
    sys.exit(1)

import itertools

from gnuradio import gr, ctrlport
from gnuradio.ctrlport.GrDataPlotter import *

from networkx.drawing.nx_agraph import graphviz_layout


class MAINWindow(Qt.QMainWindow):
    def minimumSizeHint(self):
        return Qt.QSize(800, 600)

    def __init__(self, radioclient):

        super(MAINWindow, self).__init__()
        self.plots = []
        self.knobprops = []

        self.mdiArea = Qt.QMdiArea()
        self.mdiArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
        self.mdiArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
        self.setCentralWidget(self.mdiArea)

        self.mdiArea.subWindowActivated.connect(self.updateMenus)
        self.windowMapper = QtCore.QSignalMapper(self)
        self.windowMapper.mapped[Qt.QWidget].connect(self.setActiveSubWindow)

        self.createActions()
        self.createMenus()
        self.createToolBars()
        self.createStatusBar()
        self.updateMenus()

        self.setWindowTitle("GNU Radio Performance Monitor")
        self.setUnifiedTitleAndToolBarOnMac(True)

        self.newCon(radioclient)

        icon = Qt.QIcon(ctrlport.__path__[0] + "/icon.png")
        self.setWindowIcon(icon)

    def newSubWindow(self, window, title):
        child = window
        child.setWindowTitle(title)
        self.mdiArea.addSubWindow(child)
        child.show()
        self.mdiArea.currentSubWindow().showMaximized()

    def newCon(self, radioclient=None):
        child = MForm(radioclient, self)
        if(child.radioclient is not None):
            child.setWindowTitle(str(child.radio))
            self.mdiArea.addSubWindow(child)
            child.show()
            self.mdiArea.currentSubWindow().showMaximized()

    def setActiveSubWindow(self, window):
        if window:
            self.mdiArea.setActiveSubWindow(window)

    def createActions(self):
        self.newConAct = Qt.QAction("&New Connection",
                                    self, shortcut=Qt.QKeySequence.New,
                                    statusTip="Create a new file", triggered=lambda x: self.newCon(None))

        self.exitAct = Qt.QAction("E&xit", self, shortcut="Ctrl+Q",
                                  statusTip="Exit the application",
                                  triggered=Qt.qApp.closeAllWindows)

        self.closeAct = Qt.QAction("Cl&ose", self, shortcut="Ctrl+F4",
                                   statusTip="Close the active window",
                                   triggered=self.mdiArea.closeActiveSubWindow)

        self.closeAllAct = Qt.QAction("Close &All", self,
                                      statusTip="Close all the windows",
                                      triggered=self.mdiArea.closeAllSubWindows)

        qks = Qt.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_T)
        self.tileAct = Qt.QAction("&Tile", self,
                                  statusTip="Tile the windows",
                                  triggered=self.mdiArea.tileSubWindows,
                                  shortcut=qks)

        qks = Qt.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_C)
        self.cascadeAct = Qt.QAction("&Cascade", self,
                                     statusTip="Cascade the windows", shortcut=qks,
                                     triggered=self.mdiArea.cascadeSubWindows)

        self.nextAct = Qt.QAction("Ne&xt", self,
                                  shortcut=Qt.QKeySequence.NextChild,
                                  statusTip="Move the focus to the next window",
                                  triggered=self.mdiArea.activateNextSubWindow)

        self.previousAct = Qt.QAction("Pre&vious", self,
                                      shortcut=Qt.QKeySequence.PreviousChild,
                                      statusTip="Move the focus to the previous window",
                                      triggered=self.mdiArea.activatePreviousSubWindow)

        self.separatorAct = Qt.QAction(self)
        self.separatorAct.setSeparator(True)

        self.aboutAct = Qt.QAction("&About", self,
                                   statusTip="Show the application's About box",
                                   triggered=self.about)

        self.aboutQtAct = Qt.QAction("About &Qt", self,
                                     statusTip="Show the Qt library's About box",
                                     triggered=Qt.qApp.aboutQt)

    def createMenus(self):
        self.fileMenu = self.menuBar().addMenu("&File")
        self.fileMenu.addAction(self.newConAct)
        self.fileMenu.addSeparator()
        self.fileMenu.addAction(self.exitAct)

        self.windowMenu = self.menuBar().addMenu("&Window")
        self.updateWindowMenu()
        self.windowMenu.aboutToShow.connect(self.updateWindowMenu)

        self.menuBar().addSeparator()

        self.helpMenu = self.menuBar().addMenu("&Help")
        self.helpMenu.addAction(self.aboutAct)
        self.helpMenu.addAction(self.aboutQtAct)

    def createToolBars(self):
        self.fileToolBar = self.addToolBar("File")
        self.fileToolBar.addAction(self.newConAct)

        self.fileToolBar = self.addToolBar("Window")
        self.fileToolBar.addAction(self.tileAct)
        self.fileToolBar.addAction(self.cascadeAct)

    def createStatusBar(self):
        self.statusBar().showMessage("Ready")

    def activeMdiChild(self):
        activeSubWindow = self.mdiArea.activeSubWindow()
        if activeSubWindow:
            return activeSubWindow.widget()
        return None

    def updateMenus(self):
        hasMdiChild = (self.activeMdiChild() is not None)
        self.closeAct.setEnabled(hasMdiChild)
        self.closeAllAct.setEnabled(hasMdiChild)
        self.tileAct.setEnabled(hasMdiChild)
        self.cascadeAct.setEnabled(hasMdiChild)
        self.nextAct.setEnabled(hasMdiChild)
        self.previousAct.setEnabled(hasMdiChild)
        self.separatorAct.setVisible(hasMdiChild)

    def updateWindowMenu(self):
        self.windowMenu.clear()
        self.windowMenu.addAction(self.closeAct)
        self.windowMenu.addAction(self.closeAllAct)
        self.windowMenu.addSeparator()
        self.windowMenu.addAction(self.tileAct)
        self.windowMenu.addAction(self.cascadeAct)
        self.windowMenu.addSeparator()
        self.windowMenu.addAction(self.nextAct)
        self.windowMenu.addAction(self.previousAct)
        self.windowMenu.addAction(self.separatorAct)

    def about(self):
        about_info = \
            '''Copyright 2012 Free Software Foundation, Inc.\n
This program is part of GNU Radio.\n
GNU Radio is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version.\n
GNU Radio is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n
You should have received a copy of the GNU General Public License along with GNU Radio; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Boston, MA 02110-1301, USA.'''

        Qt.QMessageBox.about(None, "gr-perf-monitorx", about_info)


class ConInfoDialog(Qt.QDialog):
    def __init__(self, parent=None):
        super(ConInfoDialog, self).__init__(parent)

        self.gridLayout = Qt.QGridLayout(self)

        self.host = Qt.QLineEdit(self)
        self.port = Qt.QLineEdit(self)
        self.host.setText("localhost")
        self.port.setText("43243")

        self.buttonBox = Qt.QDialogButtonBox(Qt.QDialogButtonBox.Ok |
                                             Qt.QDialogButtonBox.Cancel)

        self.gridLayout.addWidget(self.host)
        self.gridLayout.addWidget(self.port)
        self.gridLayout.addWidget(self.buttonBox)

        self.buttonBox.accepted.connect(self.accept)
        self.buttonBox.rejected.connect(self.reject)

    def accept(self):
        self.done(1)

    def reject(self):
        self.done(0)


class DataTable(Qt.QWidget):
    def update(self):
        print("update")

    def closeEvent(self, event):
        self.timer = None

    def __init__(self, radioclient, G):
        Qt.QWidget.__init__(self)

        self.layout = Qt.QVBoxLayout(self)
        self.hlayout = Qt.QHBoxLayout()
        self.layout.addLayout(self.hlayout)

        self.G = G
        self.radioclient = radioclient

        self._keymap = None

        self.disp = None

        # Create a combobox to set the type of statistic we want.
        self._statistic = "Instantaneous"
        self._statistics_table = {"Instantaneous": "",
                                  "Average": "avg ",
                                  "Variance": "var "}
        self.stattype = Qt.QComboBox()
        self.stattype.addItem("Instantaneous")
        self.stattype.addItem("Average")
        self.stattype.addItem("Variance")
        self.stattype.setMaximumWidth(200)
        self.hlayout.addWidget(self.stattype)
        self.stattype.currentIndexChanged.connect(self.stat_changed)

        # Create a checkbox to toggle sorting of graphs
        self._sort = False
        self.checksort = Qt.QCheckBox("Sort")
        self.checksort.setCheckState(self._sort)
        self.hlayout.addWidget(self.checksort)
        self.checksort.stateChanged.connect(self.checksort_changed)

        # set up table
        self.perfTable = Qt.QTableWidget()
        self.perfTable.setColumnCount(2)
        self.perfTable.verticalHeader().hide()
        self.perfTable.setHorizontalHeaderLabels(
            ["Block Name", "Percent Runtime"])
        self.perfTable.horizontalHeader().setStretchLastSection(True)
        self.perfTable.setSortingEnabled(True)
        nodes = self.G.nodes(data=True)

        # set up plot
        self.f = plt.figure(figsize=(10, 8), dpi=90)
        self.sp = self.f.add_subplot(111)
        self.sp.autoscale_view(True, True, True)
        self.sp.set_autoscale_on(True)

        # set up tabs
        self.tabber = Qt.QTabWidget()
        self.layout.addWidget(self.tabber)
        self.tabber.addTab(self.perfTable, "Table View")
        self.tabber.addTab(self.f.canvas, "Graph View")

        # set up timer
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.update)
        self.timer.start(500)

        for i, node in enumerate(nodes):
            self.perfTable.setItem(i, 0,
                                   Qt.QTableWidgetItem(node[0]))

    def table_update(self, data):
        for k in data.keys():
            weight = data[k]
            existing = self.perfTable.findItems(
                str(k), QtCore.Qt.MatchFixedString)
            if(len(existing) == 0):
                i = self.perfTable.rowCount()
                self.perfTable.setRowCount(i + 1)
                self.perfTable.setItem(i, 0, Qt.QTableWidgetItem(str(k)))
                self.perfTable.setItem(i, 1, Qt.QTableWidgetItem(str(weight)))
            else:
                self.perfTable.setItem(self.perfTable.row(
                    existing[0]), 1, Qt.QTableWidgetItem(str(weight)))

    def stat_changed(self, index):
        self._statistic = str(self.stattype.currentText())

    def checksort_changed(self, state):
        self._sort = state > 0


class DataTableBuffers(DataTable):
    def __init__(self, radioclient, G):
        super(DataTableBuffers, self).__init__(radioclient, G)
        self.perfTable.setHorizontalHeaderLabels(
            ["Block Name", "Percent Buffer Full"])

    def update(self):
        nodes = self.G.nodes()

        # get buffer fullness for all blocks
        kl = list(map(lambda x: "%s::%soutput %% full" %
                      (x, self._statistics_table[self._statistic]),
                      nodes))
        try:
            buf_knobs = self.radioclient.getKnobs(kl)
        except Exception as e:
            sys.stderr.write(
                "gr-perf-monitorx: lost connection ({0}).\n".format(e))
            self.parentWidget().mdiArea().removeSubWindow(self.parentWidget())
            self.close()
            return

        # strip values out of ctrlport response
        buffer_fullness = dict(zip(
            map(lambda x: x.split("::")[0], buf_knobs.keys()),
            map(lambda x: x.value, buf_knobs.values())))

        blockport_fullness = {}
        for blk in buffer_fullness:
            bdata = buffer_fullness[blk]
            if bdata:
                for port in range(0, len(bdata)):
                    blockport_fullness["%s:%d" % (blk, port)] = bdata[port]

        if(self.perfTable.isVisible()):
            self.table_update(blockport_fullness)

        else:
            if(self._sort):
                sorted_fullness = sorted(blockport_fullness.items(),
                                         key=operator.itemgetter(1))
                self._keymap = list(
                    map(operator.itemgetter(0), sorted_fullness))
            else:
                if self._keymap:
                    sorted_fullness = len(self._keymap) * ['', ]
                    for b in blockport_fullness:
                        sorted_fullness[self._keymap.index(b)] = (
                            b, blockport_fullness[b])
                else:
                    sorted_fullness = blockport_fullness.items()

            if(not self.disp):
                self.disp = self.sp.bar(range(0, len(sorted_fullness)),
                                        list(
                                            map(lambda x: x[1], sorted_fullness)),
                                        alpha=0.5, align='edge')
                self.sp.set_ylabel("% Buffers Full")
                self.sp.set_xticks(
                    list(map(lambda x: x + 0.5, range(0, len(sorted_fullness)))))
                self.sp.set_xticklabels(list(map(lambda x: "  " + x, map(lambda x: x[0], sorted_fullness))),
                                        rotation="vertical", verticalalignment="bottom")
            else:
                self.sp.set_xticklabels(list(map(lambda x: "  " + x, map(lambda x: x[0], sorted_fullness))),
                                        rotation="vertical", verticalalignment="bottom")
                for r, w in zip(self.disp, sorted_fullness):
                    r.set_height(w[1])

            self.f.canvas.draw()


class DataTableRuntimes(DataTable):
    def __init__(self, radioclient, G):
        super(DataTableRuntimes, self).__init__(radioclient, G)

    def update(self):
        nodes = self.G.nodes()

        # get work time for all blocks
        kl = list(map(lambda x: "%s::%swork time" %
                      (x, self._statistics_table[self._statistic]),
                      nodes))

        try:
            wrk_knobs = self.radioclient.getKnobs(kl)

        except Exception as e:
            sys.stderr.write(
                "gr-perf-monitorx: lost connection ({0}).\n".format(e))
            self.parentWidget().mdiArea().removeSubWindow(self.parentWidget())
            self.close()
            return

        # strip values out of ctrlport response
        total_work = sum(map(lambda x: x.value, wrk_knobs.values()))
        if(total_work == 0):
            total_work = 1
        work_times = dict(zip(
            map(lambda x: x.split("::")[0], wrk_knobs.keys()),
            map(lambda x: 1e-10 + x.value / total_work, wrk_knobs.values())))

        # update table view
        if(self.perfTable.isVisible()):
            self.table_update(work_times)

        else:
            if(self._sort):
                sorted_work = sorted(work_times.items(),
                                     key=operator.itemgetter(1))
                self._keymap = list(map(operator.itemgetter(0), sorted_work))
            else:
                if self._keymap:
                    sorted_work = len(list(self._keymap)) * ['', ]
                    for b in work_times:
                        sorted_work[self._keymap.index(b)] = (b, work_times[b])
                else:
                    sorted_work = work_times.items()

            if(not self.disp):
                self.disp = self.sp.bar(range(0, len(sorted_work)),
                                        list(map(lambda x: x[1], sorted_work)),
                                        alpha=0.5, align='edge')
                self.sp.set_ylabel("% Runtime")
                self.sp.set_xticks(
                    list(map(lambda x: x + 0.5, range(0, len(sorted_work)))))
                self.sp.set_xticklabels(list(map(lambda x: "  " + x[0], sorted_work)),
                                        rotation="vertical", verticalalignment="bottom")
            else:
                self.sp.set_xticklabels(list(map(lambda x: "  " + x[0], sorted_work)),
                                        rotation="vertical", verticalalignment="bottom")
                for r, w in zip(self.disp, sorted_work):
                    r.set_height(w[1])

            self.f.canvas.draw()


class MForm(Qt.QWidget):
    def update(self):
        try:
            try:
                # update current clock type
                self.prevent_clock_change = True
                kl1 = None
                if(self.clockKey == None):
                    kl1 = self.radioclient.getRe([".*perfcounter_clock"])
                else:
                    kl1 = self.radioclient.getKnobs([self.clockKey])
                self.clockKey = list(kl1.keys())[0]
                self.currClock = kl1[self.clockKey].value
                self.clockSelIdx = list(
                    self.clocks.values()).index(self.currClock)
                self.clockSel.setCurrentIndex(self.clockSelIdx)
                self.prevent_clock_change = False
            except Exception as e:
                print(e)
                print("WARNING: Failed to get current clock setting!")

            nodes_stream = self.G_stream.nodes()
            nodes_msg = self.G_msg.nodes()

            # get current buffer depths of all output buffers
            kl = list(map(lambda x: "%s::%soutput %% full" %
                          (x, self._statistics_table[self._statistic]),
                          nodes_stream))

            st = time.time()
            buf_knobs = self.radioclient.getKnobs(kl)
            td1 = time.time() - st

            # strip values out of ctrlport response
            buf_vals = dict(zip(
                map(lambda x: x.split("::")[0], buf_knobs.keys()),
                map(lambda x: x.value, buf_knobs.values())))

            # get work time for all blocks
            kl = list(map(lambda x: "%s::%swork time" %
                          (x, self._statistics_table[self._statistic]),
                          nodes_stream))
            st = time.time()
            wrk_knobs = self.radioclient.getKnobs(kl)
            td2 = time.time() - st

            # strip values out of ctrlport response
            total_work = sum(map(lambda x: x.value, wrk_knobs.values()))
            if(total_work == 0):
                total_work = 1
            work_times = dict(zip(
                map(lambda x: x.split("::")[0], wrk_knobs.keys()),
                map(lambda x: x.value / total_work, wrk_knobs.values())))
            work_times_padded = dict(zip(
                self.G.nodes(),
                [0.1] * len(self.G.nodes())))
            work_times_padded.update(work_times)

            for n in nodes_stream:
                # ne is the list of edges away from this node!
                ne = self.G.edges([n], True)
                # for e in ne: # iterate over edges from this block
                for e in ne:  # iterate over edges from this block
                    # get the right output buffer/port weight for each edge
                    sourceport = e[2]["sourceport"]
                    if(e[2]["type"] == "stream"):
                        newweight = buf_vals[n][sourceport]
                        e[2]["weight"] = newweight

            for n in nodes_msg:
                ne = self.G.edges([n], True)
                for e in ne:  # iterate over edges from this block
                    sourceport = e[2]["sourceport"]
                    if(e[2]["type"] == "msg"):
                        newweight = 0.01
                        e[2]["weight"] = newweight

            # set updated weights
            #self.node_weights = map(lambda x: 20+2000*work_times[x], nodes_stream)
            self.node_weights = list(
                map(lambda x: 20 + 2000 * work_times_padded[x], self.G.nodes()))
            self.edge_weights = list(
                map(lambda x: 100.0 * x[2]["weight"], self.G.edges(data=True)))

            # draw graph updates
            if(self.do_update):
                self.drawGraph()
            else:
                self.updateGraph()

            latency = td1 + td2
            self.parent.statusBar().showMessage("Current GNU Radio Control Port Query Latency: %f ms" %
                                                (latency * 1000))

        except Exception as e:
            sys.stderr.write(
                "gr-perf-monitorx: lost connection ({0}).\n".format(e))
            if(type(self.parent) is MAINWindow):
                # Find window of connection
                for p in self.parent.mdiArea.subWindowList():
                    self.parent.mdiArea.removeSubWindow(p)
                    break

                self.close()
            else:
                sys.exit(1)
            return

    def rtt(self):
        self.parent.newSubWindow(DataTableRuntimes(
            self.radioclient, self.G_stream), "Runtime Table")

    def bpt(self):
        self.parent.newSubWindow(DataTableBuffers(
            self.radioclient, self.G_stream), "Buffers Table")

    def resetPCs(self):
        knobs = []
        for b in self.blocks_list:
            knobs += [self.radioclient.Knob(b + "::reset_perf_counters"), ]
        k = self.radioclient.setKnobs(knobs)

    def toggleFlowgraph(self):
        if self.pauseFGAct.isChecked():
            self.pauseFlowgraph()
        else:
            self.unpauseFlowgraph()

    def pauseFlowgraph(self):
        knobs = [self.radioclient.Knob(self.top_block + "::lock"),
                 self.radioclient.Knob(self.top_block + "::stop")]
        k = self.radioclient.setKnobs(knobs)

    def unpauseFlowgraph(self):
        knobs = [self.radioclient.Knob(self.top_block + "::unlock")]
        k = self.radioclient.setKnobs(knobs)

    def stat_changed(self, index):
        self._statistic = str(self.stattype.currentText())

    def update_clock(self, clkidx):
        if(self.prevent_clock_change):
            return
        idx = self.clockSel.currentIndex()
        clk = list(self.clocks.values())[idx]
        # print("UPDATE CLOCK!!! %d -> %d"%(idx,clk);)
        k = self.radioclient.getKnobs([self.clockKey])
        k[self.clockKey].value = clk
        km = {}
        km[self.clockKey] = k[self.clockKey]
        self.radioclient.setKnobs(km)

    def __init__(self, radioclient=None, parent=None):

        super(MForm, self).__init__()

        if radioclient is None:
            askinfo = ConInfoDialog(self)
            self.radioclient = None
            if askinfo.exec_():
                host = str(askinfo.host.text())
                port = str(askinfo.port.text())
                try:
                    self.radioclient = GNURadioControlPortClient(
                        host, port, 'thrift').client
                    print("Connected to %s:%s" % (host, port))
                except:
                    print("Error connecting to %s:%s" % (host, port))
        else:
            self.radioclient = radioclient

        if self.radioclient is None:
            return

        self.parent = parent

        self.layoutTop = Qt.QVBoxLayout(self)
        self.ctlBox = Qt.QHBoxLayout()
        self.layout = Qt.QHBoxLayout()

        self.layoutTop.addLayout(self.ctlBox)
        self.layoutTop.addLayout(self.layout)

        self.rttAct = Qt.QAction("Runtime Table",
                                 self, statusTip="Runtime Table", triggered=self.rtt)
        self.rttBut = Qt.QToolButton()
        self.rttBut.setDefaultAction(self.rttAct)
        self.ctlBox.addWidget(self.rttBut)

        self.bptAct = Qt.QAction("Buffer Table",
                                 self, statusTip="Buffer Table", triggered=self.bpt)
        self.bptBut = Qt.QToolButton()
        self.bptBut.setDefaultAction(self.bptAct)
        self.ctlBox.addWidget(self.bptBut)

        self.resetPCsAct = Qt.QAction("Reset", self,
                                      statusTip="Reset all Performance Counters",
                                      triggered=self.resetPCs)
        self.resetPCsBut = Qt.QToolButton()
        self.resetPCsBut.setDefaultAction(self.resetPCsAct)
        self.ctlBox.addWidget(self.resetPCsBut)

        self.pauseFGAct = Qt.QAction("Pause", self,
                                     statusTip="Pause the Flowgraph",
                                     triggered=self.toggleFlowgraph)
        self.pauseFGAct.setCheckable(True)
        self.pauseFGBut = Qt.QToolButton()
        self.pauseFGBut.setDefaultAction(self.pauseFGAct)
        self.ctlBox.addWidget(self.pauseFGBut)

        self.prevent_clock_change = True
        self.clockKey = None
        self.clocks = {"MONOTONIC": 1, "THREAD": 3}
        self.clockSel = Qt.QComboBox(self)
        list(map(lambda x: self.clockSel.addItem(x), self.clocks.keys()))
        self.ctlBox.addWidget(self.clockSel)
        self.clockSel.currentIndexChanged.connect(self.update_clock)
        self.prevent_clock_change = False

        self._statistic = "Instantaneous"
        self._statistics_table = {"Instantaneous": "",
                                  "Average": "avg ",
                                  "Variance": "var "}
        self.stattype = Qt.QComboBox()
        self.stattype.addItem("Instantaneous")
        self.stattype.addItem("Average")
        self.stattype.addItem("Variance")
        self.stattype.setMaximumWidth(200)
        self.ctlBox.addWidget(self.stattype)
        self.stattype.currentIndexChanged.connect(self.stat_changed)

#        self.setLayout(self.layout)

        self.radio = self.radioclient
        self.knobprops = self.radio.properties([])
        self.parent.knobprops.append(self.knobprops)

        self.timer = QtCore.QTimer()
        self.constupdatediv = 0
        self.tableupdatediv = 0
        plotsize = 250

        # Set up the graph of blocks
        def input_name(x): return x + "::avg input % full"
        def output_name(x): return x + "::avg output % full"
        def wtime_name(x): return x + "::avg work time"
        def nout_name(x): return x + "::avg noutput_items"
        def nprod_name(x): return x + "::avg nproduced"

        tmplist = []
        knobs = self.radio.getKnobs([])
        edgelist = None
        msgedgelist = None
        for k in knobs:
            propname = k.split("::")
            blockname = propname[0]
            keyname = propname[1]
            if(keyname == "edge list"):
                edgelist = knobs[k].value
                self.top_block = blockname
            elif(keyname == "msg edges list"):
                msgedgelist = knobs[k].value
            elif(blockname not in tmplist):
                # only take gr_blocks (no hier_block2)
                if(input_name(blockname) in knobs):
                    tmplist.append(blockname)

        if not edgelist:
            sys.stderr.write("Could not find list of edges from flowgraph. " +
                             "Make sure the option 'edges_list' is enabled " +
                             "in the ControlPort configuration.\n\n")
            sys.exit(1)

        self.blocks_list = tmplist
        edges = edgelist.split("\n")[0:-1]
        msgedges = msgedgelist.split("\n")[0:-1]

        edgepairs_stream = []
        edgepairs_msg = []

        # add stream connections
        for e in edges:
            _e = e.split("->")
            edgepairs_stream.append((_e[0].split(":")[0], _e[1].split(
                ":")[0], {"type": "stream", "sourceport": int(_e[0].split(":")[1])}))

        # add msg connections
        for e in msgedges:
            _e = e.split("->")
            edgepairs_msg.append((_e[0].split(":")[0], _e[1].split(":")[0], {
                                 "type": "msg", "sourceport": _e[0].split(":")[1]}))

        self.G = nx.MultiDiGraph()
        self.G_stream = nx.MultiDiGraph()
        self.G_msg = nx.MultiDiGraph()

        self.G.add_edges_from(edgepairs_stream)
        self.G.add_edges_from(edgepairs_msg)

        self.G_stream.add_edges_from(edgepairs_stream)
        self.G_msg.add_edges_from(edgepairs_msg)

        n_edges = self.G.edges(data=True)
        for e in n_edges:
            e[2]["weight"] = 5 + random.random() * 10

        self.G = nx.MultiDiGraph()
        self.G.add_edges_from(n_edges)

        self.f = plt.figure(figsize=(10, 8), dpi=90)
        self.sp = self.f.add_subplot(111)
        self.sp.autoscale_view(True, True, True)
        self.sp.set_autoscale_on(True)

        self.layout.addWidget(self.f.canvas)

        self.pos = graphviz_layout(self.G)
        #self.pos = pygraphviz_layout(self.G)
        #self.pos = nx.spectral_layout(self.G)
        #self.pos = nx.circular_layout(self.G)
        #self.pos = nx.shell_layout(self.G)
        #self.pos = nx.spring_layout(self.G)

        # Indicate to self.update to initialize the graph
        self.do_update = False

        # generate weights and plot
        self.update()
        # set up timer
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.update)
        self.timer.start(1000)

        # Set up mouse callback functions to move blocks around.
        self._grabbed = False
        self._current_block = ''
        self.f.canvas.mpl_connect('button_press_event',
                                  self.button_press)
        self.f.canvas.mpl_connect('motion_notify_event',
                                  self.mouse_move)
        self.f.canvas.mpl_connect('button_release_event',
                                  self.button_release)

    def button_press(self, event):
        x, y = event.xdata, event.ydata
        thrsh = 100

        if(x is not None and y is not None):
            nearby = list(map(lambda z: math.sqrt(
                math.pow(x - z[0], 2) + math.pow(y - z[1], 2)), self.pos.values()))
            i = nearby.index(min(nearby))
            if(abs(list(self.pos.values())[i][0] - x) < thrsh and
               abs(list(self.pos.values())[i][1] - y) < thrsh):
                self._current_block = list(self.pos.keys())[i]
                # print "MOVING BLOCK: ", self._current_block
                # print "CUR POS: ", self.pos.values()[i]
                self._grabbed = True

    def mouse_move(self, event):
        if self._grabbed:
            x, y = event.xdata, event.ydata
            if(x is not None and y is not None):
                self.pos[self._current_block] = (x, y)
                self.updateGraph()

    def button_release(self, event):
        self._grabbed = False

    def openMenu(self, pos):
        index = self.table.treeWidget.selectedIndexes()
        item = self.table.treeWidget.itemFromIndex(index[0])
        itemname = str(item.text(0))
        self.parent.propertiesMenu(itemname, self.radioclient)

    def drawGraph(self):

        self.do_update = True
        self.f.canvas.updateGeometry()
        self.sp.clear()
        plt.figure(self.f.number)
        plt.subplot(111)
        nx.draw(self.G, self.pos,
                edge_color=self.edge_weights,
                node_color='#A0CBE2',
                width=list(
                    map(lambda x: 3 + math.log(x + 1e-20), self.edge_weights)),
                node_shape="s",
                node_size=self.node_weights,
                edge_cmap=plt.cm.Reds,
                ax=self.sp,
                arrows=False
                )
        nx.draw_networkx_labels(self.G, self.pos,
                                font_size=12)

        self.f.canvas.show()

    def updateGraph(self):

        self.sp.clear()
        nx.draw_networkx_nodes(self.G, self.pos,
                               node_color='#A0CBE2',
                               node_shape="s",
                               node_size=self.node_weights,
                               ax=self.sp)

        nx.draw_networkx_edges(self.G, self.pos,
                               edge_color=self.edge_weights,
                               width=list(
                                   map(lambda x: 3 + math.log(x + 1e-20), self.edge_weights)),
                               edge_cmap=plt.cm.Reds,
                               ax=self.sp,
                               arrows=False)

        nx.draw_networkx_labels(self.G, self.pos,
                                ax=self.sp, font_size=12)

        self.f.canvas.draw()


class MyApp(object):
    def __init__(self, args):

        parser = ArgumentParser(description="GNU Radio Performance Monitor")
        parser.add_argument("host", nargs="?",
                            default="localhost", help="host name or IP")
        parser.add_argument("port", help="port")
        args = parser.parse_args()

        signal.signal(signal.SIGINT, signal.SIG_DFL)
        signal.signal(signal.SIGTERM, signal.SIG_DFL)

        try:
            GNURadioControlPortClient(
                args.host, args.port, 'thrift', self.run, Qt.QApplication(sys.argv).exec_)
        except thrift.transport.TTransport.TTransportException:
            print("ControlPort failed to connect. Check the config of your endpoint.")
            print("\t[ControlPort] on = True")
            print("\t[ControlPort] edges_list = True")
            print("\t[PerfCounters] on = True")
            print("\t[PerfCounters] export = True")
            sys.exit(1)

    def run(self, client):
        MAINWindow(client).show()


MyApp(sys.argv)
