#!/usr/bin/python -tt
#
# Copyright 2004-2006 Red Hat, Inc.
#
# Jeremy Katz <katzj@redhat.com>
# Paul Nasrat <pnasrat@redhat.com>
# Luke Macken <lmacken@redhat.com>
# 
# This program 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; version 2 only
#
# This program 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 Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.


import os,sys,fcntl
import string
import signal
import subprocess
import time
import rpm
import webbrowser

import gtk
import gtk.glade
import gtk.gdk as gdk
import gobject
import pango

import yum
import yum.plugins
import yum.Errors
from yum.constants import *
import urlgrabber, urlgrabber.progress
import rpmUtils.miscutils

from yum.update_md import UpdateMetadata

from rhpl.exception import installExceptionHandler
from rhpl.translate import _, N_, textdomain

from pirut import *
from pirut.Errors import *

rebootpkgs = ("kernel", "kernel-smp", "kernel-xen-hypervisor", "kernel-PAE",
              "kernel-xen0", "kernel-xenU", "kernel-xen", "kernel-xen-guest",
              "glibc", "hal", "dbus")

class PackageUpdater(GraphicalYumBase):
    def __init__(self):
        if os.path.exists("data/pup.glade"):
            fn = "data/pup.glade"
        else:
            fn = "/usr/share/pirut/ui/pup.glade"
        self.pupxml = gtk.glade.XML(fn, domain="pirut")

        if os.path.exists("data/pup.png"):
            imgfn = "data/pup.png"
        else:
            imgfn = "/usr/share/pirut/pixmaps/pup.png"
        i = self.pupxml.get_widget("pupImage")
        i.set_from_pixbuf(gtk.gdk.pixbuf_new_from_file(imgfn))

        self.mainwin = self.pupxml.get_widget("pupWindow")
        self.mainwin.set_icon_from_file(imgfn)

        self.vpaned = self.pupxml.get_widget("vpaned1")
        self.details = self.pupxml.get_widget("updateDetails")
        self.expander = self.pupxml.get_widget("detailsExpander")

        self.scratchBuffer = gtk.TextBuffer()
        self.updateMetadata = UpdateMetadata()

        self._connectSignals()
        self._createUpdateStore()
        self.mainwin.connect("delete_event", self._quit)

        self.pupMenu = self.pupxml.get_widget("pupMenu")

        self.registered = False

        # note that nothing which takes "time" should be called here!
        GraphicalYumBase.__init__(self, False)

    def _connectSignals(self):
        sigs = {"on_quitButton_clicked": self._quit,
                "on_pupWindow_delete": self._quit,
                "on_applyButton_clicked": self._apply,
                "on_updateList_button_press": self._updateButtonPress,
                "on_updateList_popup_menu": self._updatePopup,
                "on_updateNotebook_scroll_event": self._notebookScroll,
                "on_refreshButton_clicked": self.doRefresh,
                "on_pupMenu_select": self._selectPackages,
                "on_pupMenu_unselect": self._unselectPackages }
        self.pupxml.signal_autoconnect(sigs)

        self.details.set_buffer(gtk.TextBuffer())
        self.details.connect("event-after", UpdateDetails.event_after)
        self.expander.connect("activate", lambda x: self.vpaned.set_position(-1))

        # FIXME: figure out why this event only gets called when your cursor
        # enters and leaves the TextView (making it impossible to change the
        # cursor when hovering over a link)
        #self.details.connect("motion-notify-event",
        #                     UpdateDetails.motion_notify_event)

    def _createUpdateStore(self):
        # checkbox, display string, list of
        # (updateFunc, printFunc, new, old, notice) tuples
        self.store = gtk.TreeStore(gobject.TYPE_BOOLEAN,
                                   gobject.TYPE_STRING,
                                   gobject.TYPE_PYOBJECT,
                                   gobject.TYPE_STRING,
                                   gobject.TYPE_PYOBJECT)
        tree = self.pupxml.get_widget("updateList")
        tree.set_model(self.store)

        column = gtk.TreeViewColumn(None, None)
        column.set_clickable(True)
        column.set_spacing(6)
        pixr = gtk.CellRendererPixbuf()
        pixr.set_property('stock-size', 1)
        column.pack_start(pixr, False)
        column.add_attribute(pixr, 'stock-id', 3)
        cbr = gtk.CellRendererToggle()
        cbr.connect ("toggled", self._toggledUpdate)
        column.pack_start(cbr, False)
        column.add_attribute(cbr, 'active', 0)
        tree.append_column(column)

        renderer = gtk.CellRendererText()
        column = gtk.TreeViewColumn('Text', renderer, text=1)
        column.set_clickable(False)
        tree.append_column(column)
        tree.columns_autosize()

        self.store.set_sort_column_id(1, gtk.SORT_ASCENDING)

        self.details.set_buffer(self.scratchBuffer)
        selection = tree.get_selection()
        selection.connect("changed", self._updateSelected)
        selection.set_mode(gtk.SELECTION_MULTIPLE)

        tree.set_search_equal_func(self.__search_pkgs)

    def __search_pkgs(self, model, col, key, i):
        lst = model.get_value(i, 2)
        if len(lst) < 1:
            return True
        (updf, strf, new, old) = lst[0]
        val = new.returnSimple('sourcerpm')
        if val.lower().startswith(key.lower()):
            return False
        return True

    def _toggledUpdate(self, data, row):
        i = self.store.get_iter((int(row),))
        val = self.store.get_value(i, 0)
        self.store.set_value(i, 0, not val)

    def _updateSelected(self, selection):
        if selection.count_selected_rows() != 1:
            self.details.get_buffer().set_text("")
            return
        
        (model, paths) = selection.get_selected_rows()
        i = model.get_iter(paths[0])
        lst = model.get_value(i, 2)
        notice = model.get_value(i, 4)
        if notice:
            self.details.set_buffer(notice)
            return

        # FIXME: if we have other indicators, this won't work anymore :-P
        needsReboot = False
        if model.get_value(i, 3) is not None:
            needsReboot = True 

        strs = []
        for (updfunc, strfunc, new, old) in lst:
            md = self.updateMetadata.get_notice((new.name,new.ver,new.rel))
            if md:  # use the update metadata
                details = UpdateDetails(md.get_metadata(), needsReboot)
                self.details.set_buffer(details)
                model.set_value(i, 4, details)
                return
            else:   # use the predefined strfunc
                strs.append(strfunc(new, old))


            self.scratchBuffer.set_text(string.join(strs, '\n'))
            if needsReboot:
                tag = self.scratchBuffer.create_tag(weight=pango.WEIGHT_BOLD)
                theiter = self.scratchBuffer.get_end_iter()
                self.scratchBuffer.insert(theiter, "\n\n")
                self.scratchBuffer.insert_with_tags(theiter,
                                                   _("This update will require a reboot."),
                                                   tag)

            self.details.set_buffer(self.scratchBuffer)

    def _changeSelected(self, state):
        tree = self.pupxml.get_widget("updateList")
        sel = tree.get_selection()
        if sel.count_selected_rows() < 0:
            return
        (model, paths) = sel.get_selected_rows()
        for p in paths:
            i = model.get_iter(p)
            model.set_value(i, 0, state)

    def _selectPackages(self, *args):
        self._changeSelected(True)
    def _unselectPackages(self, *args):
        self._changeSelected(False)

    def __doUpdatePopup(self, button, time):
        menu = self.pupMenu
        menu.popup(None, None, None, button, time)
        menu.show_all()

    def _updateButtonPress(self, widget, event):
        if event.button == 3:
            x = int(event.x)
            y = int(event.y)
            pthinfo = widget.get_path_at_pos(x, y)
            if pthinfo is not None:
                sel = widget.get_selection()
                if sel.count_selected_rows() == 1:
                    path, col, cellx, celly = pthinfo                    
                    widget.grab_focus()
                    widget.set_cursor(path, col, 0)
                self.__doUpdatePopup(event.button, event.time)
            return 1

    def _updatePopup(self, widget):
        sel = widget.get_selection()
        if sel.count_selected_rows() > 0:
            self.__doUpdatePopup(0, 0)

    # block mouse scroll scrolling tabs
    def _notebookScroll(self, *args): 
        pass

    def _runGtkmain(self, *args):
        while gtk.events_pending():
            gtk.main_iteration()

    def _busyCursor(self):
        self.mainwin.window.set_cursor(gdk.Cursor(gdk.WATCH))
        self.mainwin.set_sensitive(False)
        self._runGtkmain()

    def _normalCursor(self):
        self.mainwin.window.set_cursor(None)
        self.mainwin.set_sensitive(True)
        self._runGtkmain()

    def doRefresh(self, *args):
        self.mainwin.show()
        
        pbar = PirutProgressCallback(_("Retrieving update information"),
                                     self.mainwin, num_tasks = 2)
        pbar.show()
        self._busyCursor()
        try:
            self.reposSetup(pbar)
        except yum.Errors.LockError:
            pbar.destroy()
            self._normalCursor()
            d = gtk.MessageDialog(self.mainwin, gtk.DIALOG_MODAL,
                                  gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
                                  _("Another application is running which "
                                    "is accessing software information."))
            d.show_all()
            d.run()
            d.destroy()
            sys.exit(1)
        except PirutDownloadError, e:
            pbar.destroy()
            self._normalCursor()
            d = PirutDetailsDialog(self.mainwin, gtk.MESSAGE_ERROR,
                                   [('gtk-ok', gtk.BUTTONS_OK,)],
                                  _("Unable to retrieve update information"))
            d.format_secondary_text(_("Unable to retrieve software "
                                      "information.  This could be caused by "
                                      "not having a network connection "
                                      "available."))
            d.set_details("%s" %(e,))
            d.run()
            d.destroy()
            sys.exit(1)

        # FIXME: this should call real repo config code that let's you
        # generically add repos and would pluggably call rhn_register.
        # but, for now, we just need something that works
        if not self.registered and len(self.repos.listEnabled()) == 0:
            self.registered = True
            if os.path.exists("/etc/sysconfig/rhn") and os.path.exists("/usr/sbin/rhn_register"):
                def checkRegister(p): # when rhn_register exits, refresh us
                    if p.poll() is not None:
                        gobject.idle_add(self.doRefresh)
                        return False
                    return True

                pbar.destroy()
                self._normalCursor()
                self.mainwin.hide()
                self._runGtkmain()
                p = subprocess.Popen(["/usr/sbin/rhn_register"],
                                     close_fds = True)
                gobject.timeout_add(2 * 1000, checkRegister, p)
                

        self.doUpdateSetup()
        pbar.next_task()
        self.populateUpdates()
        pbar.next_task()        
        self._normalCursor()
        pbar.destroy()

    def _doUpdate(self, new, old):
        self.tsInfo.addUpdate(new, old)

    def _doObsolete(self, new, old):
        self.tsInfo.addObsoleting(new, old)
        self.tsInfo.addObsoleted(old, new)

    def _printUpdate(self, new, old):
        return _("%s updates %s") %(new, old)

    def _printObsolete(self, new, old):
        return _("%s obsoletes %s") %(new, old)

    def populateUpdates(self):
        self.store.clear()

        upds = {}
        reboots = {}
        repos = []

        # handle obsoletes
        opt = self.conf.obsoletes
        if opt:
            obsoletes = self.up.getObsoletesTuples(newest=1)
        else:
            obsoletes = []
        for (obs, inst) in obsoletes:
            obsoleting = self.getPackageObject(obs)
            installed = self.rpmdb.searchPkgTuple(inst)[0]
            srpm = obsoleting.returnSimple("sourcerpm")
            if upds.has_key(srpm):
                upds[srpm].append( (self._doObsolete, self._printObsolete,
                                    obsoleting, installed) )
            else:
                upds[srpm] = [ (self._doObsolete, self._printObsolete,
                                obsoleting, installed) ]
                reboots[srpm] = False

            if obsoleting.returnSimple("name") in rebootpkgs:
                reboots[srpm] = True

        # and updates
        updates = self.up.getUpdatesTuples()
        for (new, old) in updates:
            updating = self.getPackageObject(new)
            updated = self.rpmdb.searchPkgTuple(old)[0]

            # populate update metadata
            if not updating.repoid in repos:
                repo = self.repos.getRepo(updating.repoid)
                try: # attempt to grab the updateinfo.xml.gz from the repodata
                    self.updateMetadata.add(repo)
                except yum.Errors.RepoMDError:
                    pass # No metadata found for this repo
                repos.append(updating.repoid)

            srpm = updating.returnSimple("sourcerpm")
            if upds.has_key(srpm):
                upds[srpm].append( (self._doUpdate, self._printUpdate,
                                    updating, updated) )
            else:
                upds[srpm] = [ (self._doUpdate, self._printUpdate,
                                updating, updated) ]
                reboots[srpm] = False

            if updating.returnSimple("name") in rebootpkgs:
                reboots[srpm] = True

        for (srpm, lst) in upds.items():
            if reboots[srpm]:
                pix = 'gtk-refresh'
            else:
                pix = None
            self.store.append(None, [True,
                                     _("Updated %s packages available")
                                     % (rpmUtils.miscutils.splitFilename(srpm)[0],),
                                     lst, pix, None])

        if len(upds) == 0:
            self.pupxml.get_widget("updateNotebook").set_current_page(1)
            self.pupxml.get_widget("applyButton").set_sensitive(False)

    def _apply(self, *args):
        needReboot = False

        map(lambda x: self.tsInfo.remove(x.pkgtup), self.tsInfo)
        self.tsInfo.makelists()
        del self.ts
        self.initActionTs()

        # select packages that are chosen
        for row in self.store:
            (on, pkgstr, lst, pix, bar) = row
            if on:
                for (updfunc, strfunc, new, old) in lst:
                    if new.name in rebootpkgs:
                        needReboot = True
                    updfunc(new, old)

        if len(self.tsInfo) <= 0:
            d = gtk.MessageDialog(self.mainwin, gtk.DIALOG_MODAL,
                                  gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
                                  _("No packages were selected for upgrade."))
            d.show_all()
            d.run()
            d.destroy()
            return

        try:
            self.applyChanges(self.mainwin)
        except PirutError:
            return 

        if needReboot:
            self._rebootWindow()
        else:
            d = gtk.MessageDialog(self.mainwin, gtk.DIALOG_MODAL,
                                  gtk.MESSAGE_INFO, gtk.BUTTONS_OK,
                                  _("Software update successfully completed."))
            d.show_all()
            d.run()
            d.destroy()

        # and we're done
        self._quit()

    def _rebootWindow(self):
        def _setTimeout():
            l = self.pupxml.get_widget("rebootLabel")
            if self.timeleft >= 0:
                l.set_text(_("Will reboot in %d seconds...") % self.timeleft)
            else:
                l.hide()

        def _countdown():
            self.timeleft -= 1
            _setTimeout()
            if self.timeleft == 0:
                return False
            return True

        def _timeUp():
            _reboot()
            return False

        def _reboot():
            d.hide()
            self._cleanup()
            subprocess.call(["/sbin/shutdown", "-r", "now"])
            sys.exit(0)

        d = self.pupxml.get_widget("pupRebootDialog")
        primary = self.pupxml.get_widget ("rebootPrimaryLabel")
        primary.set_markup ("<span size=\"larger\" weight=\"bold\">%s</span>" % _("Reboot recommended"))
        d.set_transient_for(self.mainwin)
        self.timeleft = -1
        _setTimeout()
        d.show()
        if self.timeleft >= 0:
            gobject.timeout_add(1 * 1000, _countdown)
            gobject.timeout_add(self.timeleft * 1000, _timeUp)        
        rc = d.run()
        d.hide()
        if rc == gtk.RESPONSE_OK:
            return _reboot()
        return

    def run(self):
        self.mainwin.show()
        self._runGtkmain()
        self.doRefresh()
        gtk.main()

    def _cleanup(self, *args):
        # FIXME: should make sure we close down access to lock files and dbs
        # cleanly here
        try:
            self.closeRpmDB()
        except Exception, e:
            print >> sys.stderr, "Error closing rpmdb: ", e

        self.doUnlock(YUM_PID_FILE)            
        try:
            gtk.main_quit()
        except:
            pass

    def _quit(self, *args):
        self._cleanup()
        sys.exit(0)

hovering_over_link = False
hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)

class UpdateDetails(gtk.TextBuffer):

    def __init__(self, metadata, needsReboot = False):
        gtk.TextBuffer.__init__(self)
        self.md = metadata
        self.needsReboot = needsReboot
        self.iter = self.get_start_iter()
        self._build_tags()
        self._parse_references()
        self._populate_details()

    def _build_tags(self):
        self.bold_tag = self.create_tag(weight=pango.WEIGHT_BOLD)
        self.title_tag = self.create_tag(font='DejaVu LGC Sans Mono Bold')
        self.title_tag.set_property('foreground', 'white')
        self.title_tag.set_property('background-gdk',
                                    gtk.gdk.color_parse('#CCCCCC'))

    def _parse_references(self):
        self.cves = []
        self.bzs = []
        for ref in self.md['references']:
            type = ref['type']
            if type == 'cve':
                self.cves.append((ref['id'], ref['href']))
            elif type == 'bugzilla':
                self.bzs.append((ref['id'], ref['href']))

    def _populate_details(self):
        titlecol_width = 12
        margin = ''.zfill(titlecol_width + 1).replace('0', ' ')

        def _add_item(title, field=None):
            title = title.zfill(titlecol_width).replace('0', ' ')
            self.insert_with_tags(self.iter, '%s ' % title, self.title_tag)
            if field:
                self.insert_with_tags(self.iter, ' %s\n' % self.md[field])

        _add_item('ID', 'update_id')
        _add_item('Type', 'type')
        _add_item('Status', 'status')
        _add_item('Issued', 'issued')
        _add_item('Updated', 'updated')

        # Append the references
        for title, list, lmt in (('Bugs', self.bzs, 6), ('CVEs', self.cves, 4)):
            if len(list) == 0:
                continue
            title = title.zfill(titlecol_width).replace('0', ' ')
            self.insert_with_tags(self.iter, '%s ' % title, self.title_tag)
            i = 0
            for id, url in list:
                self.insert(self.iter, ' %s' % id)
                # Disable linking bugs and CVEs until we figure out a way to
                # NOT launch firefox as root (Bug #216552)
                #self._insert_link(id, url)
                #self.insert(self.iter, ' ')
                i += 1
                if i % lmt == 0:  # allow lmt references per line
                    self.insert(self.iter, '\n')
                    self.insert_with_tags(self.iter, margin, self.title_tag)
            self.insert(self.iter, '\n')

        if self.md['description']:
            desc = 'Description'.zfill(titlecol_width).replace('0', ' ')
            self.insert_with_tags(self.iter, desc + ' ', self.title_tag)
            lines = self.md['description'].split('\n')
            self.insert(self.iter, ' %s' % lines[0])
            for line in lines[1:]:
                self.insert(self.iter, '\n')
                self.insert_with_tags(self.iter, margin, self.title_tag)
                self.insert(self.iter, ' ' + line)

        if self.needsReboot:
            self.insert(self.iter, "\n")
            self.insert_with_tags(self.iter,
                                  _("This update will require a reboot."),
                                  self.bold_tag)

    def _insert_link(self, text, url):
        tag = self.create_tag(underline=pango.UNDERLINE_SINGLE,
                              foreground='blue')
        tag.set_data('page', url)
        self.insert(self.iter, ' ')
        self.insert_with_tags(self.iter, text, tag)

    @staticmethod
    def event_after(view, event):
        """ Callback to monitor mouse clicks and handle links. """
        if event.type != gtk.gdk.BUTTON_RELEASE or event.button != 1:
            return False

        # don't follow a link if the user has selected something
        bounds = view.get_buffer().get_selection_bounds()
        if len(bounds) != 0:
            return False

        x, y = view.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET,
                                            int(event.x), int(event.y))
        iter = view.get_iter_at_location(x, y)

        for tag in iter.get_tags():
            page = tag.get_data('page')
            if page:
                webbrowser.open(page, new=True)

    ## FIXME: figure out why this method is only getting called when the
    ## cursor enters or leaves the TextView.
    @staticmethod
    def motion_notify_event(view, event):
        """ Callback to monitor mouse motion and change the cursor if it is
            hovering over a link.
        """
        print "motion_notify_event"
        global hovering_over_link
        hovering = False

        x, y = view.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET,
                                            int(event.x), int(event.y))
        iter = view.get_iter_at_location(x, y)
        for tag in iter.get_tags():
            page = tag.get_data('page')
            if page:
                print "Hovering == True"
                hovering = True
                break
        if hovering != hovering_over_link:
            print "Changing cursor"
            hovering_over_link = hovering
            win = view.get_window(gtk.TEXT_WINDOW_WIDGET)
            win.set_cursor(hovering_over_link and hand_cursor or regular_cursor)


def main():
    textdomain("pirut")
    gtk.glade.bindtextdomain("pirut", "/usr/share/locale")
    try:
        pup = PackageUpdater()
    except PirutError, e:
        startupError(e)
    pup.run()

if __name__ == "__main__":
    installExceptionHandler("Software Updater", "")    
    main()
