#!/usr/bin/env python
# 
# ntpshmviz - graph the drift of NTP servers
# Written by Keane Wolter <daemoneye2@gmail.com>
# 
# pystripchart can be found at
# https://sourceforge.net/projects/jstripchart
#
# To do:
#
# 1. Exaggerate vertical scale so data spans about 3/4ths of graph height
#    and we can actually see features.
# 2. Try using an impulse rather than line plot - this is bursty noise, not
#    really a contour.
# 3. Exit button - WM might be a tiler like i3.
# 4. Program should dynamically handle any number of NTP units.
#

import array, gtk, stripchart, sys

class ntpOffset:
    def __init__(self, stream):
        # Initialize the class

        # get the data
        self.read_data(stream)

        # create the GUI for the application
        self.create_GUI()

        # enter the GTK main loop
        gtk.main()

    def create_GUI(self):
        # Creates the gui for the class

        # create a standard top-level GTK window
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.window.set_title("NTP Offset")
        self.window.connect("destroy", gtk.mainquit)
        self.window.set_default_size(700, 400)
        self.window.show()

        # create a VBox to hold all the top-level GUI items
        self.vbox = gtk.VBox()
        self.vbox.show()
        self.window.add(self.vbox)

        # create the StripTableau and add the data to it
        self.create_StripTableau()

        # create the toolbar
        self.create_toolbar()

    def create_StripTableau(self):
        # create the striptable widget

        # gtk adjustment (value, lower, upper, step_incr, page_incr, page_size)
        hadj = gtk.Adjustment(0, 0, 1, 1, 1, self.lines)
        sel = gtk.Adjustment(-1)
        self.striptableau = stripchart.StripTableau(hadj, sel)
        self.striptableau.metawidth  = 120
        self.striptableau.gradewidth = 100
        self.vbox.pack_end(self.striptableau.widget, gtk.TRUE, gtk.TRUE)

        # Add the channel for NTP2
        # adjust the size of the graph for NTP2 to allow all the data
        # to fit within the graph
        vadj_ntp2 = gtk.Adjustment(self.ntp2_lower, self.ntp2_lower-0.1, self.ntp2_upper+0.1, 0.1, 0, self.ntp2_upper+0.1)
        ntp2_item = self.striptableau.addChannel(self.ntp2, vadj_ntp2)
        ntp2_item.name = "NTP2"
        ntp2_item.meta = self.create_text("NTP2 Offset Values")

        # add the channel for NTP3
        # adjust the size of the graph for NTP2 to allow all the data
        # to fit within the graph
        vadj_ntp3 = gtk.Adjustment(self.ntp3_lower-0.1, self.ntp3_lower-0.1, self.ntp3_upper+0.1, 0.1, 0, self.ntp3_upper+0.1)
        ntp3_item = self.striptableau.addChannel(self.ntp3, vadj_ntp3)
        ntp3_item.name = "NTP3"
        ntp3_item.meta = self.create_text("NTP3 Offset Values")

    def create_text(self, text):
        # Creates a text widget to contain a description of a channel.
        scrolled_window = gtk.ScrolledWindow()
        scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        scrolled_window.set_shadow_type(gtk.SHADOW_IN)

        textview = gtk.TextView()
        buffer   = gtk.TextBuffer()
        iter     = buffer.get_iter_at_offset(0)
        buffer.insert(iter, text)
        textview.set_buffer(buffer)
        textview.set_editable(gtk.TRUE)
        textview.set_cursor_visible(gtk.TRUE)
        textview.set_wrap_mode(gtk.WRAP_WORD)
        textview.set_left_margin(5)
        textview.set_right_margin(5)
        textview.set_pixels_above_lines(5)
        textview.set_pixels_below_lines(5)

        scrolled_window.add(textview)
        scrolled_window.show()

        return scrolled_window

    def create_toolbar(self):
        # Create the toolbar
        self.toolbar = gtk.Toolbar()
        self.toolbar.show()

        # add buttons for zoom and wire them to the StripTableau widget
        self.toolbar.insert_stock(gtk.STOCK_ZOOM_IN, "Zoom in", None,
                lambda b, w: self.striptableau.zoomIn(), self.window, -1)
        self.toolbar.insert_stock(gtk.STOCK_ZOOM_OUT, "Zoom out", None,
                lambda b, w: self.striptableau.zoomOut(), self.window, -1)
        self.toolbar.insert_stock(gtk.STOCK_ZOOM_FIT, "Zoom fit", None,
                lambda b, w: self.striptableau.zoomSel(), self.window, -1)

        # sit the toolbar inside a HandleBox so that it can be detacjed
        self.handlebox = gtk.HandleBox()
        self.handlebox.show()
        self.handlebox.add(self.toolbar)

        # pack the toolbar into the main window
        self.vbox.pack_start(self.handlebox, gtk.FALSE)

    def get_offset(self, data):
        # get the difference between the clock time and receiver time of day
        return (float(data.split(' ')[3]) - float(data.split(' ')[4]))

    def read_data(self, stream):
        # Reads data from a ntp log file.  Layout is:
        #
        #   - The keyword "sample"
        #   - The NTP unit from which it was collected.
        #   - Collection time of day, expressed in seconds
        #   - Receiver time of day, expressed in seconds
        #   - Clock time of day, expressed in seconds
        #   - Leep-second notification status
        #   - Source precision (log(2) of source jitter)

        self.ntp2       = array.array("d") # ntp2 array
        self.ntp3       = array.array("d") # ntp3 array
        self.lines      = 0                # width of graph - set to the size of the largest array
        self.ntp2_upper = 0                # highest value in ntp2 array
        self.ntp2_lower = 0                # lowest value in ntp2 array
        self.ntp3_upper = 0                # highest value in ntp3 array
        self.ntp3_lower = 0                # lowest value in ntp3 array
        offset          = 0                # offset value to add to the array and to check the upper and lower bounds of the graph

        for line in stream:
            if len(line.split(' ')) > 6:
                if 'NTP2' in line:
                    offset = self.get_offset(line)
                    self.ntp2.append(offset)
                    if offset > self.ntp2_upper:
                        self.ntp2_upper = round(offset, 5)
                    if offset < self.ntp2_lower:
                        self.ntp2_lower = round(offset, 5)
                if 'NTP3' in line:
                    offset = self.get_offset(line)
                    self.ntp3.append(offset)
                    if offset > self.ntp3_upper:
                        self.ntp3_upper = round(offset, 5)
                    if offset < self.ntp3_lower:
                        self.ntp3_lower = round(offset, 5)
        stream.close()

        # Get the line count for the larger of the two arrays.
        # This will set the width of the graph when it is displayed.
        if len(self.ntp2) > len(self.ntp3):
            self.lines = len(self.ntp2)
        else:
            self.lines = len(self.ntp3)

# Run the class
if __name__ == "__main__":
    # instantiate the application
    ntpOffset(sys.stdin)
