#!/usr/bin/python -tt
#
# Copyright 2004-2007 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
import gettext
import string
import subprocess
import webbrowser

from optparse import OptionParser

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

import yum.Errors
from yum.constants import *
import rpmUtils.miscutils

from yum.update_md import UpdateMetadata, UpdateNoticeException

from rhpl.exception import installExceptionHandler

from pirut import *
from pirut.constants import *
from pirut.Errors import *

t = gettext.translation(I18N_DOMAIN, "/usr/share/locale", fallback = True)
_ = t.lgettext

class PackageUpdater(GraphicalYumBase):
    def __init__(self, interactive = True, config = None, pkgs = []):
        self.interactive = interactive
        self.toupdate = pkgs
        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")

        self.mainwin = self.pupxml.get_widget("pupWindow")
        self.mainwin.set_icon_name("system-software-update")

        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, config)

    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)

        # 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)
        tree.append_column(column)
        tree.columns_autosize()

        tree.connect ("row-activated", self._rowToggled)        

        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 _rowToggled(self, tree, path, col):
        self._toggledUpdate(None, path[0])

    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 = self.doRefreshRepos(destroy=False)

        # 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]

            if self.toupdate and obsoleting.returnSimple("name") not in self.toupdate:
                print >> sys.stderr, "skipping", obsoleting
                continue

            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]

            if self.toupdate and updating.returnSimple("name") not in self.toupdate:
                print >> sys.stderr, "skipping", updating
                continue

            # 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
                except UpdateNoticeException:
                    pass # Metadata parsing error
                repos.append(updating.repoid)

            srpm = updating.returnSimple("sourcerpm")
            if srpm is None:
                continue
            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:
            output = self.applyChanges(self.mainwin)
        except PirutError:
            return 

        # this is a little tricky and bears some explanation
        # 1) if there's no warning output and we don't need to reboot, show
        # a simple "success!" dialog.
        # 2) if there's no warning output and we need to reboot, only show
        # the "reboot recommended" dialog
        # 3) if there's warning output _and_ we need to reboot, we need
        # to show as two dialogs to avoid confusion
        d = PirutDetailsDialog(self.mainwin, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE,
                              _("Software update successfully completed."))
        if output:
            d.format_secondary_text("Some warnings were seen during update.")
            d.set_details(buffer = outputDictAsTextBuffer(output))

        if not needReboot or output:
            d.set_buttons(gtk.BUTTONS_OK)
            rc = d.run()
            d.destroy()
        
        if needReboot:
            d = PirutDetailsDialog(self.mainwin, gtk.MESSAGE_INFO,
                                   gtk.BUTTONS_NONE,
                                   _("Reboot recommended"))
            d.format_secondary_text(_("Due to the updates installed, it is recommended that you reboot your system.  You can either reboot now or choose to do so at a later time."))
            d.set_buttons([(_("Reboot _later"), gtk.RESPONSE_CANCEL),
                           (_("_Reboot now"), gtk.RESPONSE_DELETE_EVENT)])
            d.set_default_response(gtk.RESPONSE_CANCEL)
            rc = d.run()

        if rc == gtk.RESPONSE_DELETE_EVENT:
            d.hide()
            subprocess.call(["/sbin/shutdown", "-r", "now"])
            self.quit()

        d.destroy()
        # and we're done
        self.quit()

    def run(self):
        self.mainwin.show()
        self._runGtkmain()
        self.doRefresh()
        if not self.interactive:
            gobject.idle_add(self._apply)
        gtk.main()

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('Title', 'title')
        _add_item('ID', 'update_id')
        _add_item('Type', 'type')
        _add_item('Status', 'status')
        _add_item('Issued', 'issued')

        # 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():
    parser = OptionParser()
    parser.add_option("-a", "--apply", action="store_true",
                      dest="autoapply", 
                      help="Automatically apply updates")
    parser.add_option("-c", "--config", type="string",
                      dest="config", default="/etc/yum.conf",
                      help="Config file to use (default: /etc/yum.conf)")
    (options,args) = parser.parse_args()

    if len(args) > 0:
        options.autoapply = True
    
    gtk.glade.bindtextdomain("pirut", "/usr/share/locale")
    try:
        # right now, we have to run privileged...
        if os.getuid() != 0:
            raise PirutError(_("Must be run as root."))
        pup = PackageUpdater(not options.autoapply, options.config, args)
    except PirutError, e:
        print e
        startupError(e)
    pup.run()

if __name__ == "__main__":
    installExceptionHandler("pirut", "")    
    main()
