#!/usr/bin/python
#
# An applet to monitor a Test-AutoBuild instance via
# its published RSS feed

import threading
import sys
import os
import signal
from Queue import *

# Hack for local eggtrayicon module
eggtraydir = "/usr/lib/autobuild-applet"
if os.path.exists ("./.libs"):
    eggtraydir = "./.libs"

gladedir = "/usr/share/autobuild-applet"
if os.path.exists("./autobuild-applet.glade"):
    gladedir = "."
elif os.path.exists("../autobuild-applet.glade"):
    gladedir = ".."

icondir = "/usr/share/autobuild-applet/images"
if os.path.exists("./images/applet-error.svg"):
    icondir = "./images"
elif os.path.exists("../images/applet-error.svg"):
    icondir = "../images"

sys.path.insert(0,eggtraydir)


from time import *
import urllib2
import libxml2
import libeggtrayicon
import gobject
import gtk
import gtk.gdk
import gtk.glade
import gnome
import gnome.ui
import gconf

gtk.threads_init()

gtk.window_set_default_icon_from_file(icondir + "/applet-success.svg")

# Applets shouldn't impact real work getting done
try:
    os.nice(10)
except:
    pass;

colors = {}
colors['pending'] = "#00ffff"
colors['success'] = "#00ff00"
colors['cache'] = "#00ff00"
colors['skipped'] = "#ff00ff"
colors['failed'] = "#ff0000"
colors['error'] = "#ffff00"


class autobuildApplet:
    def __init__(self):
        gnome.program_init("autobuild-applet", "0.0.1")

        self.applet_size = 22
        self.jobs = jobPool()
        self.fetcher = feedFetcher(self.jobs)

        self.tooltip = gtk.Tooltips()
        self.applet_window = libeggtrayicon.create_window("autobuild-applet")
        self.applet_window.connect("destroy", self.exit_app)

        self.builders = [{'url': "http://demo.autobuild.org/index.rss"}]
        self.conf = gconf.client_get_default()
        self.conf.add_dir ("/apps/autobuild-applet",
                           gconf.CLIENT_PRELOAD_NONE)

        self.conf.notify_add ("/apps/autobuild-applet/builders",
                              self.change_builders)
        self.update_builders(self.conf.get_list("/apps/autobuild-applet/builders", gconf.VALUE_STRING))

        self.conf.notify_add ("/apps/autobuild-applet/refresh",
                              self.change_refresh)
        self.update_refresh(self.conf.get_int("/apps/autobuild-applet/refresh"))


        self.conf.notify_add ("/apps/autobuild-applet/proxyUser",
                              self.change_proxy)
        self.conf.notify_add ("/apps/autobuild-applet/proxyPass",
                              self.change_proxy)
        self.conf.notify_add ("/apps/autobuild-applet/proxyHost",
                              self.change_proxy)
        self.conf.notify_add ("/apps/autobuild-applet/proxyPort",
                              self.change_proxy)
        self.update_proxy(self.conf.get_string("/apps/autobuild-applet/proxyHost"),
                          self.conf.get_int("/apps/autobuild-applet/proxyPort"),
                          self.conf.get_string("/apps/autobuild-applet/proxyUser"),
                          self.conf.get_string("/apps/autobuild-applet/proxyPass"))

        try:
            self.session = gnome.ui.gnome_master_client()
        except:
            self.session = gnome.ui.master_client()
        if self.session:
            gtk.Object.connect(self.session, "save-yourself",
                               self.save_yourself)
            gtk.Object.connect(self.session, "die", self.exit_app)

        self.menu = gtk.Menu()
        self.menu_items = {}

        self.menu_items["about"] = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
        self.menu_items["about"].connect("activate", self.show_about)
        self.menu_items["about"].show()
        self.menu.add(self.menu_items["about"])

        self.menu.add(gtk.SeparatorMenuItem())

        self.menu_items["refresh"] = gtk.ImageMenuItem(gtk.STOCK_REFRESH)
        self.menu_items["refresh"].connect("activate", self.refresh_all)
        self.menu_items["refresh"].show()
        self.menu.add(self.menu_items["refresh"])

        self.menu_items["configure"] = gtk.ImageMenuItem(gtk.STOCK_PREFERENCES)
        self.menu_items["configure"].connect("activate", self.configure)
        self.menu_items["configure"].show()
        self.menu.add(self.menu_items["configure"])

        self.menu.add(gtk.SeparatorMenuItem())

        self.menu_items["exit"] = gtk.ImageMenuItem(gtk.STOCK_QUIT)
        self.menu_items["exit"].connect("activate", self.exit_app)
        self.menu_items["exit"].show()
        self.menu.add(self.menu_items["exit"])

        self.menu.show_all()

        self.event_box = gtk.EventBox()
        self.image_widget = gtk.Image()
        self.image_widget.set_from_pixbuf(gtk.gdk.pixbuf_new_from_file_at_size(icondir + "/applet-error.svg", self.applet_size, self.applet_size))

        self.event_box.add(self.image_widget)
        self.event_box.set_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.CONFIGURE)

#        self.applet_window.set_size_request(self.applet_size, self.applet_size)

        self.image_widget.show()
        self.event_box.connect("button_press_event", self.applet_face_click)
        self.image_widget.connect('destroy', self.on_destroy)

        self.module_data = gtk.TreeStore(str, str, str, str)

        self.applet_window.add(self.event_box)
        self.applet_window.show_all()

        self.window = autobuildStatus(self.module_data)
        self.prefs_dialog = None
        self.about_dialog = None

        self.change_refresh()
        self.timer = gobject.timeout_add(1000, self.refresh_handler)
        self.refresh()


    def change_builders(self, client, cnxn_id, entry, data):
        self.update_builders(self.conf.get_list("/apps/autobuild-applet/builders", gconf.VALUE_STRING))

    def update_builders(self, builderUrls):
        self.builders = {}
        if builderUrls:
            for builderUrl in builderUrls:
                self.builders[builderUrl] = { 'lastRefresh': None, 'xml': None, 'updating': 0 }

    def change_refresh(self, client=None, cnxn_id=None, entry=None, data=None):
        rate = self.conf.get_int("/apps/autobuild-applet/refresh")
        self.update_refresh(rate)

    def update_refresh(self, rate):
        if rate == None or rate < 1:
            rate = 5

        self.refresh_rate = rate * 60

    def change_proxy(self, client, cnxn_id, entry, data):
        self.update_proxy(self.conf.get_string("/apps/autobuild-applet/proxyHost"),
                          self.conf.get_int("/apps/autobuild-applet/proxyPort"),
                          self.conf.get_string("/apps/autobuild-applet/proxyUser"),
                          self.conf.get_string("/apps/autobuild-applet/proxyPass"))
        self.refresh_all()

    def update_proxy(self, host, port, user, password):
        proxy_info = {
            'user' : user,
            'pass' : password,
            'host' : host,
            'port' : port
            }

        if host == None or host == "":
            #print "No host, disabling proxy"
            opener = urllib2.build_opener(urllib2.HTTPHandler)
            urllib2.install_opener(opener)
        else:
            if not port:
                #print "Setting default port 3128"
                port = 3128

            if user == None or password == None or user == "" or password == "":
                #print "No proxy auth"
                proxy_support = urllib2.ProxyHandler({
                    "http" :
                    "http://%(host)s:%(port)d" % proxy_info})
            else:
                #print "Using proxy auth"
                proxy_support = urllib2.ProxyHandler({
                    "http" :
                    "http://%(user)s:%(pass)s@%(host)s:%(port)d" % proxy_info})

            opener = urllib2.build_opener(proxy_support, urllib2.HTTPHandler)
            urllib2.install_opener(opener)

    def save_yourself(self, *args):
        if self.session:
            self.session.set_clone_command(1, ["/usr/bin/autobuild-applet"])
            self.session.set_restart_command(1, ["/usr/bin/autobuild-applet"])
        return True

    def exit_app(self, widget=None):
        gtk.main_quit()

    def load_link(self, path, col, param):
        gnome.url_show(self.module_data[col[0]][2])

    #def select_link(self):
        #gnome.url_show(self.module_data[col[0]][2])

    def configure(self, widget=None):
        if self.prefs_dialog == None:
            self.prefs_dialog = autobuildPrefs(self.conf);
        self.prefs_dialog.display()

    def refresh_handler(self):
        #print "Doing refresh"
        try:
            if not self.fetcher.isAlive():
                #print "Starting url fetcher"
                self.fetcher.start()
            self.refresh()
        except:
            #print "Ignoring failure during refresh process"
            pass
        #print "Scheduling refresh timer in 5 seconds time"
        self.timer = gobject.timeout_add(1000*5, self.refresh_handler)
        return

    def refresh_all(self,widget=None):
        #print "Forcing immediate refresh of all"
        for builderUrl in self.builders.keys():
            builder = self.builders[builderUrl]
            builder['lastRefresh'] = None
        self.refresh()

    def refresh(self, widget=None):
        now = time()
        updated = 0
        #print "Refresh at " + str(now) + " rate " + str(self.refresh_rate)
        # Decide if any builder URLs need re-fetching, and if
        # so queue them up
        for builderUrl in self.builders.keys():
            builder = self.builders[builderUrl]
            #print "Checking state of " + builderUrl + " " + str(builder['lastRefresh']) + " " + str(builder['updating'])
            if ((builder['lastRefresh'] == None) or ((now - builder['lastRefresh']) > self.refresh_rate)) and (not builder['updating']):
                #print "Schedule update " + builderUrl
                self.jobs.add_job(builderUrl)
                builder['updating'] = 1
                updated = 1


        # See if any updates are now ready
        while self.jobs.has_results():
            result = self.jobs.job_result()
            #print "Got update of " + result['url']
            # Might have been removed since the update was scheduled
            if not self.builders.has_key(result['url']):
                continue

            builder = self.builders[result['url']]
            builder['errors'] = []
            builder['updating'] = 0
            builder['lastRefresh'] = now
            builder['xml'] = result['xml']
            if result['error'] != None:
                print "Got error " + str(result["error"])
                builder['errors'].append(result['error'])
            builder['modules'] = []
            if len(builder['errors']) == 0:
                self.parse_xml(result['url'])

            updated = 1

        if updated:
            self.recalculate()

    def parse_xml(self, builderUrl):
        builder = self.builders[builderUrl]
        doc = None
        try:
            doc = libxml2.parseDoc(builder['xml'])
        except:
            builder['errors'].append(sys.exc_info()[1])

        if len(builder['errors']) == 0:
            ctx = doc.xpathNewContext()
            ctx.xpathRegisterNs("ab", "http://xmlns.autobuild.org/2004/05/17-rss-rdf")
            ctx.xpathRegisterNs("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
            ctx.xpathRegisterNs("rss", "http://purl.org/rss/1.0/")

            builder['title'] = ctx.xpathEval("/rdf:RDF/rss:channel/rss:title")[0].get_content().strip()
            builder['link'] = ctx.xpathEval("/rdf:RDF/rss:channel/rss:link")[0].get_content().strip()
            builder['description'] = ctx.xpathEval("/rdf:RDF/rss:channel/rss:description")[0].get_content().strip()
            builder['fail_count'] = ctx.xpathEval("count(//ab:status[text()!='success' and text()!='cache'])")

            for module in ctx.xpathEval("/rdf:RDF/rss:item"):
                modEntry = {}
                ctx.setContextNode(module)
                modEntry['title'] = ctx.xpathEval("rss:title")[0].get_content().strip()
                modEntry['link'] = ctx.xpathEval("rss:link")[0].get_content().strip()
                modEntry['status'] = ctx.xpathEval("ab:status")[0].get_content().strip()
                builder['modules'].append(modEntry)

    def recalculate(self):
        #print "Recalculating state"
        self.module_data.clear()
        pending = []
        aborted = []
        failed = []
        for url in self.builders.keys():
            builder = self.builders[url]
            if builder['lastRefresh'] == None:
                pending.append(url)
                self.module_data.append(None, [url, "pending", url, colors["pending"]])
                continue

            if len(builder['errors']) > 0:
                aborted.append(url)
                self.module_data.append(None, [url, "error", url, colors["error"]])
                continue

            if builder['fail_count'] > 0:
                failed.append(url)
                status = "failed"
            else:
                status = "success"

            site = self.module_data.append(None, [builder['title'], status, builder['link'], colors[status]])

            for mod in builder['modules']:
                self.module_data.append(site, [mod['title'], mod['status'], mod['link'], colors[mod['status']]])

        icon = "applet-success.svg"
        tip = "All builders passed"
        if len(pending) > 0:
            tip = "Still pending status for"
            for url in pending:
                tip = tip + " " + url
            icon = "applet-update.svg"
        elif len(aborted) > 0:
            tip = "Errors while fetching status for"
            for url in aborted:
                tip = tip + " " + url
            icon = "applet-error.svg"
        elif len(failed) > 0:
            tip = "Failed module builds for"
            for url in failed:
                tip = tip + " '" + self.builders[url]['title'] + "' (" + url + ")"
            icon = "applet-failed.svg"

        self.tooltip.set_tip(self.applet_window, tip)
        self.image_widget.set_from_pixbuf(gtk.gdk.pixbuf_new_from_file_at_size(icondir + "/" + icon, self.applet_size, self.applet_size))
        self.window.set_icon(icondir + "/" + icon)
        self.window.repack()

    def applet_face_click(self, widget, event):
        if event.button == 3:
            self.menu.popup(None, None, None, 0, event.time)
        else:
            self.window.toggle_display()

    def show_about(self,ignore=None):
        if self.about_dialog == None:
            self.about_dialog = gtk.glade.XML(gladedir + "/autobuild-applet.glade", "about-dialog")
        self.about_dialog.get_widget("about-dialog").set_version("1.0.1")
        self.about_dialog.get_widget("about-dialog").show_all()

    def on_destroy(self):
        gtk.main_quit()

class autobuildPrefs:
    def __init__(self, conf):
        self.conf = conf
        self.dialog = gtk.glade.XML(gladedir + "/autobuild-applet.glade", "applet-settings")

        self.dialog.get_widget("applet-settings").hide()

        builderList = self.dialog.get_widget("builder-list")
        builderList.set_model(gtk.TreeStore(str))

        url_text = gtk.CellRendererText()
        url = gtk.TreeViewColumn("RSS Feed URL")
        url.pack_start(url_text, True)
        url.add_attribute(url_text, 'text', 0)

        builderList.append_column(url)

        self.dialog.signal_autoconnect({
            "on_applet_settings_close" : self.finished,
            "on_applet_settings_delete_event" : self.finished,
            "on_close_clicked" : self.finished,
            "on_url_add_clicked": self.add_url,
            "on_url_delete_clicked": self.delete_url,
            "on_proxy_host_changed": self.commit_proxy_host,
            "on_proxy_port_changed": self.commit_proxy_port,
            "on_proxy_user_changed": self.commit_proxy_user,
            "on_proxy_pass_changed": self.commit_proxy_pass,
            "on_refresh_rate_changed": self.commit_refresh_rate})

    def finished(self, widget, *args):
        self.dialog.get_widget("applet-settings").hide()
        return True

    def display(self):
        self.populate_widgets()
        self.dialog.get_widget("applet-settings").show_all()

    def populate_widgets(self):
        builders = self.conf.get_list("/apps/autobuild-applet/builders", gconf.VALUE_STRING)
        self.populate_builder_list(builders)

        refresh = self.conf.get_int("/apps/autobuild-applet/refresh")
        refreshBox = self.dialog.get_widget("refresh-rate")
        if refresh:
            refreshBox.set_value(int(refresh))
        else:
            refreshBox.set_value(5)

        proxyHost = self.conf.get_string("/apps/autobuild-applet/proxyHost")
        proxyHostBox = self.dialog.get_widget("proxy-host")
        if proxyHost:
            proxyHostBox.set_text(proxyHost)
        else:
            proxyHostBox.set_text("")

        proxyPort = self.conf.get_int("/apps/autobuild-applet/proxyPort")
        proxyPortBox = self.dialog.get_widget("proxy-port")
        if proxyPort:
            proxyPortBox.set_text(str(proxyPort))
        else:
            proxyPortBox.set_text("")

        proxyUser = self.conf.get_string("/apps/autobuild-applet/proxyUser")
        proxyUserBox = self.dialog.get_widget("proxy-user")
        if proxyUser:
            proxyUserBox.set_text(proxyUser)
        else:
            proxyUserBox.set_text("")

        proxyPass = self.conf.get_string("/apps/autobuild-applet/proxyPass")
        proxyPassBox = self.dialog.get_widget("proxy-pass")
        if proxyPass:
            proxyPassBox.set_text(proxyPass)
        else:
            proxyPassBox.set_text("")

    def populate_builder_list(self, builders):
        builderList = self.dialog.get_widget("builder-list")
        builderModel = builderList.get_model()
        builderModel.clear()
        if builders and len(builders) > 0:
            for url in builders:
                builderModel.append(None, [url])

    def add_url(self, entry, *args):
        text = self.dialog.get_widget("builder-url").get_chars (0, -1)
        builders = self.conf.get_list("/apps/autobuild-applet/builders", gconf.VALUE_STRING)
        if builders == None:
            builders = []
        builders.append(text)
        self.populate_builder_list(builders)
        self.commit_builders(builders)


    def delete_url(self, entry, *args):
        builderList = self.dialog.get_widget("builder-list")
        sel = builderList.get_selection().get_selected()
        if sel[1] != None:
            oldUrl = builderList.get_model().get_value(sel[1], 0)
            newBuilders = []
            for url in self.conf.get_list("/apps/autobuild-applet/builders", gconf.VALUE_STRING):
                if oldUrl != url:
                    newBuilders.append(url)

            self.populate_builder_list(newBuilders)
            self.commit_builders(newBuilders)


    def commit_builders(self, builders):
        self.conf.set_list("/apps/autobuild-applet/builders",
                           gconf.VALUE_STRING,
                           builders)

    def commit_refresh_rate(self, entry, *args):
        self.commit_int("refresh", entry.get_chars (0, -1))

    def commit_proxy_host(self, entry, *args):
        self.commit_string("proxyHost", entry.get_chars(0, -1))

    def commit_proxy_port(self, entry, *args):
        self.commit_int("proxyPort", entry.get_chars(0, -1))

    def commit_proxy_user(self, entry, *args):
        self.commit_string("proxyUser", entry.get_chars(0, -1))

    def commit_proxy_pass(self, entry, *args):
        self.commit_string("proxyPass", entry.get_chars(0, -1))

    def commit_string(self, key, text):
        if text:
            self.conf.set_string("/apps/autobuild-applet/" + key,
                                 text)
        else:
            self.conf.unset("/apps/autobuild-applet/" + key)

    def commit_int(self, key, text):
        if text:
            self.conf.set_int("/apps/autobuild-applet/" + key,
                              int(text))
        else:
            self.conf.unset("/apps/autobuild-applet/" + key)


class autobuildStatus:
    def __init__(self, module_data):
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.window.set_title("Builder Status")
        self.window.connect("delete_event", self.hide_window)

        self.window.set_frame_dimensions(3,3,3,3)
        self.window.set_default_size(450,500)

        self.module_data = module_data
        self.modules = gtk.TreeView(module_data)
        self.modules.set_headers_visible(False)
        self.modules.set_rules_hint(True)
        self.modules.get_selection().set_mode(gtk.SELECTION_NONE)
        #self.modules.get_selection().set_mode(gtk.SELECTION_SINGLE)
        #self.modules.get_selection().connect('changed', self.select_link)
        self.modules.connect('row-activated', self.load_link)

        self.col_module_text = gtk.CellRendererText()
        self.col_module = gtk.TreeViewColumn("Module")
        self.col_module.pack_start(self.col_module_text, True)
        #self.col_module_text.set_property("underline", 1)
        self.col_module.add_attribute(self.col_module_text, 'text', 0)

        self.col_status_text = gtk.CellRendererText()
        self.col_status = gtk.TreeViewColumn("Status")
        self.col_status.pack_start(self.col_status_text, False)
        self.col_status.add_attribute(self.col_status_text, 'text', 1)
        self.col_status.add_attribute(self.col_status_text, 'background', 3)

        self.modules.append_column(self.col_module)
        self.modules.append_column(self.col_status)

        self.link_box = gtk.ScrolledWindow()
        self.link_box.add_with_viewport(self.modules)
        self.modules.show()
        self.window.add(self.link_box)
        self.link_box.show()
        self.is_visible = 0

    def hide_window(self, widget, event):
        self.is_visible = 0
        self.window.hide()
        return True

    def load_link(self, path, col, param):
        site = self.module_data[col]
        gnome.url_show(site[2])

    def repack(self):
        self.window.resize_children()


    def set_icon(self, file):
        self.window.set_icon_from_file(file)

    def toggle_display(self):
        if self.is_visible:
            self.window.hide()
            self.is_visible = 0
        else:
            self.window.show_all()
            self.is_visible = 1
            self.link_box.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))

class jobPool:
    def __init__(self):
        self.urls = Queue()
        self.results = Queue()

    def add_job(self, url):
        self.urls.put(url)

    def next_job(self):
        return self.urls.get(True)

    def has_jobs(self):
        return not self.urls.empty()

    def add_result(self, url, xml, error):
        self.results.put({ 'url': url, 'xml': xml, 'error': error})

    def job_result(self):
        return self.results.get(True)

    def has_results(self):
        return not self.results.empty()


class feedFetcher(threading.Thread):
    def __init__(self, jobPool):
        threading.Thread.__init__(self)
        self.setDaemon(True)
        self.setName("feedFetcher")
        self.jobPool = jobPool

    def run(self):
        while True:
            try:
                #print "About to get job"
                url = self.jobPool.next_job()
                #print "Got url to fetch " + url
                data = None
                error = None
                try:
                    #signal.alarm(10)
                    req = urllib2.Request(url)
                    req.add_header("Pragma", "no-cache")
                    req.add_header("Cache-control", "no-cache")
                    data = urllib2.urlopen(req).read()
                    #signal.alarm(0)
                except:
                    error = sys.exc_info()[1]
                #if data != None:
                #    print "Got data " + str(len(data))
                #print "Got error " + str(error)
                self.jobPool.add_result(url, data, error)
            except:
                print "Unexpected error while processing urls " + str(sys.exc_info()[0]) + " " + str(sys.exc_info()[1])


def main():
    signal.signal(signal.SIGINT, signal.SIG_DFL)
    #signal.signal(signal.SIGALRM, signal.SIG_IGN)
    applet = autobuildApplet()
    gtk.main()

if __name__ == "__main__":
    main()
