#!/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. Try using an impulse rather than line plot - this is bursty noise, not
#    really a contour.
#

import gtk, stripchart, sys
# need numpy for float128, normal python floats are too small to hold a timespec
import numpy

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

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

        # get the data
        self.read_data(stream)

        self.create_StripTableau()
        self.display_StripTableau()

        # 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 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, self.lines, 1, 1, self.lines * 0.5)
        sel  = gtk.Adjustment(0)
        self.striptableau = stripchart.StripTableau(hadj, sel)
        self.striptableau.metawidth  = 80
        self.striptableau.gradewidth = 80
        self.vbox.pack_end(self.striptableau.widget, gtk.TRUE, gtk.TRUE)

    def create_toolbar(self):
        # Create toolbar for zoom and quit buttons

        self.toolbar = gtk.Toolbar()
        self.toolbar.show()

        # Zoom buttons
        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)

        # Quit button
        self.toolbar.insert_stock(gtk.STOCK_QUIT, "Quit", None,
                lambda b, w: gtk.mainquit(), self.window, -1)

        # Put the toolbar into a HandleBox
        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 display_StripTableau(self):
        # display the graph of each ntp_unit

        for ntp_unit in self.ntp_data:
            # gtk.Adjustment(value, lower, upper, step_incr, page_incr,
            #                page_size)
            spread = self.ntp_upper[ntp_unit] - self.ntp_lower[ntp_unit]
            # prevent divide by zero
            if spread == 0:
                spread = 1
            vadj_ntp = gtk.Adjustment(
                    self.ntp_lower[ntp_unit],         # initial  value
                    self.ntp_lower[ntp_unit] - 0.001, # lower extreme
                    self.ntp_upper[ntp_unit] + 0.001, # upper extreme
                    1 / spread,                       # step_incr
                    spread,                           # page_incr
                    spread)                           # page size
            ntp_item = self.striptableau.addChannel(self.ntp_data[ntp_unit], 
                    vadj_ntp)
            ntp_item.name = ntp_unit

    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.ntp_data  = {} # data sets for each ntp unit
        record         = [] # A single record in the file or data stream
        line_counts    = {} # Count of total lines for each ntp unit
        self.lines     = 0  # width of graph
        self.ntp_upper = {} # Upper limit of the data set
        self.ntp_lower = {} # Lower limit of the data set
        offset         = 0  # offset value to add to the array 
        self.ntp_vadj  = {} # vertical adjustment for each graph

        for line in stream:
            if len(line.split(' ')) > 6:
                if line[:1] != '#':
                    line = line.lstrip()
                    record = line.split(' ')
                    offset = numpy.float128(record[3]) - numpy.float128(record[4])

                    # If the NTP unit is in the dictionary
                    # append the offset to the list
                    # Otherwise, create a new list in the dictionary
                    # and add the offset.
                    if record[1] not in self.ntp_data:
                        self.ntp_data[record[1]]  = []
                        line_counts[record[1]]    = 0
                        self.ntp_upper[record[1]] = round(offset, 9)
                        self.ntp_lower[record[1]] = round(offset, 9)

                    self.ntp_data[record[1]].append(offset)
                    line_counts[record[1]] += 1

                    # Update the bounds of the NTP unit if needed
                    if offset > self.ntp_upper[record[1]]:
                        self.ntp_upper[record[1]] = round(offset, 9)
                    if offset < self.ntp_lower[record[1]]:
                        self.ntp_lower[record[1]] = round(offset, 9)

                    # Update the max record count if needed
                    if line_counts[record[1]] > self.lines:
                        self.lines = line_counts[record[1]]
        stream.close()

if __name__ == "__main__":
    ntpOffset(sys.stdin)
    sys.exit()
