#!/usr/bin/env python
#
# Copyright 2012-2013 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# 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.
#
# 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.
#
# 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.
#

from gnuradio import gr, ctrlport

from PyQt4 import QtCore,Qt,Qwt5
import PyQt4.QtGui as QtGui
import sys, time, re, pprint
import itertools
import scipy

import Ice
from gnuradio.ctrlport.IceRadioClient import *
from gnuradio.ctrlport.GrDataPlotter import *
from gnuradio.ctrlport import GNURadio

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

    def __init__(self, radio, port, interface):
        
        super(MAINWindow, self).__init__()
        self.conns = []
        self.plots = []
        self.knobprops = []
        self.interface = interface

        self.mdiArea = QtGui.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[QtGui.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(radio, port)
        icon = QtGui.QIcon(ctrlport.__path__[0] + "/icon.png" )
        self.setWindowIcon(icon)

    def newCon(self, radio=None, port=None):
        child = MForm(radio, port, len(self.conns), self)
        if(child.radio is not None):
            child.setWindowTitle(str(child.radio))
            horizbar = QtGui.QScrollArea()
            horizbar.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
            horizbar.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
            horizbar.setWidget(child)
            self.mdiArea.addSubWindow(horizbar)
            self.mdiArea.currentSubWindow().showMaximized()

        self.conns.append(child)
        self.plots.append([])

    def newUpdater(self, key, radio):
        updater = UpdaterWindow(key, radio, None)
        updater.setWindowTitle("Updater: " + key)
        updater.setModal(False)
        updater.exec_()

    def update(self, knobs, uid):
        #sys.stderr.write("KNOB KEYS: {0}\n".format(knobs.keys()))
        for plot in self.plots[uid]:
            data = knobs[plot.name()].value
            plot.update(data)
            plot.stop()
            plot.wait()
            plot.start()

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


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

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

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

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


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

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

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

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

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

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

        self.aboutQtAct = QtGui.QAction("About &Qt", self,
                statusTip="Show the Qt library's About box",
                triggered=QtGui.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.'''

        QtGui.QMessageBox.about(None, "gr-ctrlport-monitor", about_info)


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

        self.gridLayout = QtGui.QGridLayout(self)
        

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

        self.buttonBox = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok |
                                                QtGui.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 UpdaterWindow(QtGui.QDialog):
    def __init__(self, key, radio, parent):
        QtGui.QDialog.__init__(self, parent)

        self.key = key;
        self.radio = radio

        self.resize(300,200)
        self.layout = QtGui.QVBoxLayout()

        self.props = radio.properties([key])[key]
        info = str(self.props)

        self.infoLabel = QtGui.QLabel(info)
        self.layout.addWidget(self.infoLabel)

        self.cancelButton = QtGui.QPushButton("Ok")
        self.cancelButton.connect(self.cancelButton, QtCore.SIGNAL('clicked()'), self.reject)

        self.buttonlayout = QtGui.QHBoxLayout()
        self.buttonlayout.addWidget(self.cancelButton)
        self.layout.addLayout(self.buttonlayout)
 
        # set layout and go...
        self.setLayout(self.layout)
            
    def _set_slider_value(self, val):
        self.slider.setValue(self.steps*(val-self.props.min.value)/self.valspan)
             
    def _slide(self):
        val = (self.slider.value()*self.valspan + self.props.min.value)/float(self.steps)
        self.textInput.setText(str(val))

    def _apply(self):
        if(type(self.sv.value) == str):
            val = str(self.textInput.text())
        elif(type(self.sv.value) == int):
            val = int(round(float(self.textInput.text())))
        elif(type(self.sv.value) == float):
            val = float(self.textInput.text())
        else:
            sys.stderr.write("set type not supported! ({0})\n".format(type(self.sv.value)))
            sys.exit(-1)

        self.sv.value = val
        km = {}
        km[self.key] = self.sv
        self.radio.set(km)
        self._set_slider_value(self.sv.value)

    def _set(self):
        self._apply()
        self.done(0)


def build_edge_graph(sources, sinks, edges):
    '''
    Starting from the sources, walks through all of the edges to find
    the next connected block. The output is stored in 'allblocks'
    where each row starts with a source and follows one path down
    until it terminates in either a sink or as an input to a block
    that is part of another chain.
    '''
    def find_edge(src, sinks, edges, row, col):
        #print "\n\nAll blocks: "
        #printer.pprint(allblocks)
        #print "\nLooking for: ", src

        src0 = src.split(":")[0]
        if(src0 in sinks):
            if(len(allblocks) <= row):
                allblocks.append(col*[""])
            allblocks[row].append(src)
            return row+1

        for edge in edges:
            if(re.match(src0, edge)):
                s = edge.split("->")[0]
                b = edge.split("->")[1]
                if(len(allblocks) <= row):
                    allblocks.append(col*[""])
                allblocks[row].append(s)
                #print "Source: {0}     Sink: {1}".format(s, b)
                row = find_edge(b, sinks, edges, row, col+1)
        return row

    # Recursively get all edges as a matrix of source->sink
    n = 0
    allblocks = []
    for src in sources:
        n = find_edge(src, sinks, edges, n, 0)

    # Sort by longest list
    allblocks = sorted(allblocks, key=len)
    allblocks.reverse()

    # Make all rows same length by padding '' in front of sort rows
    maxrowlen = len(allblocks[0])
    for i,a in enumerate(allblocks):
        rowlen = len(a)
        allblocks[i] = (maxrowlen-rowlen)*[''] + a

    # Dedup rows
    allblocks = sorted(allblocks)
    allblocks = list(k for k,_ in itertools.groupby(allblocks))
    allblocks.reverse()

    return allblocks


class MForm(QtGui.QWidget):
    def update(self):
        try:
            st = time.time()
            knobs = self.radio.get([b[0] for b in self.block_dict])

            ft = time.time()
            latency = ft-st
            self.parent.statusBar().showMessage("Current GNU Radio Control Port Query Latency: %f ms"%\
                                                    (latency*1000))
            
        except Exception, e:
            sys.stderr.write("ctrlport-monitor: radio.get threw exception ({0}).\n".format(e))
            if(type(self.parent) is MAINWindow):
                # Find window of connection
                remove = []
                for p in self.parent.mdiArea.subWindowList():
                    if self.parent.conns[self.uid] == p.widget():
                        remove.append(p)

                # Find any subplot windows of connection
                for p in self.parent.mdiArea.subWindowList():
                    for plot in self.parent.plots[self.uid]:
                        if plot.qwidget() == p.widget():
                            remove.append(p)
                
                # Clean up local references to these
                self.parent.conns.remove(self.parent.conns[self.uid])
                self.parent.plots.remove(self.parent.plots[self.uid])

                # Remove subwindows for connection and plots
                for r in remove:
                    self.parent.mdiArea.removeSubWindow(r)

                # Clean up self
                self.close()
            else:
                sys.exit(1)
            return
            
        #UPDATE TABLE:
        self.updateItems(knobs)

        #UPDATE PLOTS
        self.parent.update(knobs, self.uid)
    
    def updateItems(self, knobs):
        for b in self.block_dict:
            if(knobs[b[0]].ice_id.im_class == GNURadio.KnobVecF):
                b[1].setText("{0:.4f}".format(knobs[b[0]].value[b[2]]))
            else:
                b[1].setText("{0:.4f}".format(knobs[b[0]].value))

    def __init__(self, radio=None, port=None, uid=0, parent=None):

        super(MForm, self).__init__()

        if(radio == None or port == None):
            askinfo = ConInfoDialog(self);
            if askinfo.exec_():
                host = str(askinfo.host.text());
                port = str(askinfo.port.text());
                radio = parent.interface.getRadio(host, port)
            else:
                self.radio = None
                return
        
        self.uid = uid
        self.parent = parent
        self.layout = QtGui.QGridLayout(self)
        self.layout.setSizeConstraint(QtGui.QLayout.SetFixedSize)

        self.radio = radio
        self.knobprops = self.radio.properties([])
        self.parent.knobprops.append(self.knobprops)
        self.resize(775,500)
        self.timer = QtCore.QTimer()
        self.constupdatediv = 0
        self.tableupdatediv = 0
        plotsize=250


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

        tmplist = []
        knobs = self.radio.get([])
        edgelist = None
        for k in knobs:
            propname = k.split("::")
            blockname = propname[0]
            keyname = propname[1]
            if(keyname == "edge list"):
                edgelist = knobs[k].value
            elif(blockname not in tmplist):
                # only take gr_blocks (no hier_block2)
                if(knobs.has_key(input_name(blockname))):
                    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)

        edges = edgelist.split("\n")[0:-1]
        producers = []
        consumers = []
        for e in edges:
            _e = e.split("->")
            producers.append(_e[0])
            consumers.append(_e[1])

        # Get producers and consumers as sets while ignoring the
        # ports.
        prods = set(map(lambda x: x.split(":")[0], producers))
        cons  = set(map(lambda x: x.split(":")[0], consumers))

        # Split out all blocks, sources, and sinks based on how they
        # appear as consumers and/or producers.
        blocks = prods.intersection(cons)
        sources = prods.difference(blocks)
        sinks = cons.difference(blocks)

        nblocks = len(prods) + len(cons)

        allblocks = build_edge_graph(sources, sinks, edges)
        nrows = len(allblocks)
        ncols = len(allblocks[0])

        col_width = 120

        self.block_dict = []

        for row, blockrow in enumerate(allblocks):
            for col, block in enumerate(blockrow):
                if(block == ''):
                    continue
                
                bgroup = QtGui.QGroupBox(block)
                playout = QtGui.QFormLayout()
                bgroup.setLayout(playout)
                self.layout.addWidget(bgroup, row, 2*col)

                blockname = block.split(":")[0]

                name = wtime_name(blockname)
                wtime = knobs[name].value
                newtime = QtGui.QLineEdit()
                newtime.setMinimumWidth(col_width)
                newtime.setText("{0:.4f}".format(wtime))
                self.block_dict.append((name, newtime))

                name = nout_name(blockname)
                nout = knobs[name].value
                newnout = QtGui.QLineEdit()
                newnout.setText("{0:.4f}".format(nout))
                newnout.setMinimumWidth(col_width)
                self.block_dict.append((name, newnout))

                name = nprod_name(blockname)
                nprod = knobs[name].value
                newnprod = QtGui.QLineEdit()
                newnprod.setMinimumWidth(col_width)
                newnprod.setText("{0:.4f}".format(nprod))
                self.block_dict.append((name, newnprod))

                playout.addRow("Work time", newtime)
                playout.addRow("noutput_items", newnout)
                playout.addRow("nproduced", newnprod)

                if blockname in blocks or blockname in sources:
                    # Add a buffer between blocks
                    buffgroup = QtGui.QGroupBox("Buffer")
                    bufflayout = QtGui.QFormLayout()
                    buffgroup.setLayout(bufflayout)
                    self.layout.addWidget(buffgroup, row, 2*col+1)
                
                    i = int(block.split(":")[1])
                    name = output_name(blockname)
                    obuff = knobs[name].value
                    for i,o in enumerate(obuff):
                        newobuff = QtGui.QLineEdit()
                        newobuff.setMinimumWidth(col_width)
                        newobuff.setText("{0:.4f}".format(o))
                        self.block_dict.append((name, newobuff, i))
                        bufflayout.addRow("Out Buffer {0}".format(i),
                                          newobuff)
                
                if blockname in blocks or blockname in sinks:
                    item = self.layout.itemAtPosition(row, 2*col-1)
                    if(item):
                        buffgroup = item.widget()
                        bufflayout = buffgroup.layout()
                    else:
                        buffgroup = QtGui.QGroupBox("Buffer")
                        bufflayout = QtGui.QFormLayout()
                        buffgroup.setLayout(bufflayout)
                        self.layout.addWidget(buffgroup, row, 2*col-1)
                
                    i = int(block.split(":")[1])
                    name = input_name(blockname)
                    ibuff = knobs[name].value[i]
                    newibuff = QtGui.QLineEdit()
                    newibuff.setMinimumWidth(col_width)
                    newibuff.setText("{0:.4f}".format(ibuff))
                    self.block_dict.append((name, newibuff, i))
                    bufflayout.addRow("In Buffer {0}".format(i),
                                      newibuff)

        # set up timer
        self.timer = QtCore.QTimer()
        self.connect(self.timer, QtCore.SIGNAL('timeout()'), self.update)
        self.timer.start(1000)

    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.radio, self.uid)
        

class MyClient(IceRadioClient):
    def __init__(self):
        IceRadioClient.__init__(self, MAINWindow)

sys.exit(MyClient().main(sys.argv))