#!/usr/bin/python
# -*- coding: utf-8 -*-
# vim: ts=4 
###
#
# Copyright (c) 2010 J. Félix Ontañón
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation
#
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Authors : J. Félix Ontañón <fontanon@emergya.es>
# 
###

import sys
from gi.repository import Gtk, Gdk
from gi.repository import GObject
import logging

from udevdiscover import DeviceFinder, get_subsystems
from udevdiscover.utils import GConfStore, TextBufferHandler
import udevdiscover.device

# FIXME: This path needs to be assigned at installing time
UDEVDISCOVER_UI = '/usr/share/udev-discover/udev-discover.ui'
GCONF_KEY = '/apps/udevdiscover'

DEFAULT_SUBSYSTEMS = ['pci', 'usb', 'net', 'power_supply', 'block', 'sound', 
    'input', 'serio', 'platform', 'drm', 'video4linux', 'rfkill', 'bluetooth',
    'leds', 'dvb']

TEXTBUFFER_LOGGER = 'udevdiscover'
LOG_LEVEL = logging.DEBUG
LOG_FORMAT = '%(asctime)s %(levelname)s - %(message)s'
LOG_DATE_FORMAT = '%H:%M:%S'

PATH_COL, ICON_COL, NAME_COL, SUBSYSTEM_COL, VISIBLE_COL = range(5)
DEFAULT_SUBSYS_PRESET, ALL_SUBSYS_PRESET, CUSTOM_SUBSYS_PRESET = range(3)
ICON_SUBSYS_COL, NAME_SUBSYS_COL = range(2)

# Load icons
theme = Gtk.IconTheme.get_default()
device_icon = theme.load_icon('dialog-question', 24, 0)

class SubsystemChoserDialog(GConfStore):
    defaults = {
            'dialog_width': 600,
            'dialog_height': 400,
    }

    def __init__(self, preset=DEFAULT_SUBSYS_PRESET, chosen_subsystems=[]):
        GConfStore.__init__(self, GCONF_KEY)
        self.preset = preset
        self.chosen_subsystems = chosen_subsystems
        self.subsystems = get_subsystems()

        self.builder = Gtk.Builder()
        if not self.builder.add_objects_from_file(UDEVDISCOVER_UI,
                ['subsys_dialog', 'allsubsys_liststore', 'chosensubsys_liststore',
                'preset_radioaction', 'all_radioaction', 'custom_radioaction']):
            raise 'Cant load %s' % UDEVDISCOVER_UI
        self.builder.connect_signals(self)

        # Widgets
        self.subsys_dialog = self.builder.get_object('subsys_dialog')
        self.default_radiobutton = self.builder.get_object('default_radiobutton')
        self.all_radiobutton = self.builder.get_object('all_radiobutton')
        self.custom_radiobutton = self.builder.get_object('custom_radiobutton')
        self.addsubsys_button = self.builder.get_object('addsubsys_button')
        self.removesubsys_button = self.builder.get_object('removesubsys_button')
        self.allsubsys_liststore = self.builder.get_object('allsubsys_liststore')
        self.chosensubsys_liststore = \
            self.builder.get_object('chosensubsys_liststore')
        self.allsubsys_treeview = self.builder.get_object('allsubsys_treeview')
        self.chosensubsys_treeview = \
            self.builder.get_object('chosensubsys_treeview')

        self.allsubsys_treeview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
        self.chosensubsys_treeview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)

        # Signals
        self.subsys_dialog.connect('response', lambda d, r: d.hide())
        for widget, def_preset in (self.default_radiobutton, DEFAULT_SUBSYS_PRESET), \
                (self.all_radiobutton, ALL_SUBSYS_PRESET), \
                (self.custom_radiobutton, CUSTOM_SUBSYS_PRESET):
            widget.connect('toggled', self.preset_toggled_cb, def_preset)

        # Load gconf preferences
        self.loadconf()

        # Track main window width/height
        def catch_window_size(widget, allocate, options):
            window_alloc = widget.get_allocation()
            options['dialog_width'] = window_alloc.width
            options['dialog_height'] = window_alloc.height

        self.subsys_dialog.connect('size_allocate', catch_window_size, 
                self.options)

        # Restore main window width/heigh
        self.subsys_dialog.resize(self.options['dialog_width'], 
            self.options['dialog_height'])

        if self.preset == DEFAULT_SUBSYS_PRESET:
            self.default_radiobutton.set_active(True)
            self.populate_preset()
        elif self.preset == ALL_SUBSYS_PRESET:
            self.all_radiobutton.set_active(True)
        else:
            self.custom_radiobutton.set_active(True)

    def populate_preset(self, preset=DEFAULT_SUBSYS_PRESET):
        def populate(all_set, chosen_set):
            self.allsubsys_liststore.clear()
            self.chosensubsys_liststore.clear()

            for subsys in all_set:
                self.allsubsys_liststore.append([None, subsys])

            for subsys in chosen_set:
                self.chosensubsys_liststore.append([None, subsys])

        def subsystem_allow_edit(allow=True):
            for widget in self.addsubsys_button, self.removesubsys_button, \
                    self.allsubsys_treeview, self.chosensubsys_treeview:
                widget.set_sensitive(allow)

        if preset == DEFAULT_SUBSYS_PRESET:
            subsystem_allow_edit(False)
            default = set(DEFAULT_SUBSYSTEMS)
            populate(set(self.subsystems) - default, default)

        elif preset == ALL_SUBSYS_PRESET:
            subsystem_allow_edit(False)
            populate([], self.subsystems)

        elif preset == CUSTOM_SUBSYS_PRESET:
            subsystem_allow_edit()
            chosen = set(self.chosen_subsystems)
            populate(set(self.subsystems) - chosen, chosen)

        self.preset = preset

    def preset_toggled_cb(self, widget, preset):
        if not widget.get_active(): return
        self.populate_preset(preset)

    def subsyslist_item_activated(self, orig_treeview, dest_treeview):
        selection = orig_treeview.get_selection()
        model, selected_rows = selection.get_selected_rows()

        items = [(Gtk.TreeRowReference.new(model, selected), model[selected]) \
            for selected in selected_rows]

        if items: next = items[-1][1].get_next()

        for row_ref, (icon, name) in items:
            dest_treeview.get_model().append([icon, name])
            model.remove(model.get_iter(row_ref.get_path()))

        if next: orig_treeview.set_cursor(next.path, None, False)

    def allsubsys_treeview_row_activated_cb(self, widget, path, view_column):
        self.subsyslist_item_activated(widget, self.chosensubsys_treeview)

    def chosensubsys_treeview_key_release_event_cb(self, widget, event):
        if event.keyval == Gdk.keyval_from_name("Delete"):
            self.subsyslist_item_activated(widget, self.allsubsys_treeview)

    def addsubsys_button_clicked_cb(self, widget):
        self.subsyslist_item_activated(self.allsubsys_treeview,
            self.chosensubsys_treeview)

    def removesubsys_button_clicked_cb(self, widget):
        self.subsyslist_item_activated(self.chosensubsys_treeview, 
            self.allsubsys_treeview)

    def get_current_preset(self):
        return self.preset

    def get_chosen_subsystems(self):
        return [subsys[1] for subsys in self.chosensubsys_liststore]

    def run(self):
        return self.subsys_dialog.run()

    def destroy(self):
        self.saveconf()
        self.subsys_dialog.hide()


class UDevDiscoverGUI(GConfStore):
    defaults = {
            'window_width': 600,
            'window_height': 400,
            'devices_hpaned': 300,
            'parent_tree': True,
            'expanded': True,
            'eventslog_vpaned': 300,
            'eventslog_expanded': True,
            'follownew': True,
            'followchanged': True,
            'subsystem_preset': DEFAULT_SUBSYS_PRESET,
            'custom_subsystem_preset': ' '.join(DEFAULT_SUBSYSTEMS)
    }

    def __init__(self):
        GConfStore.__init__(self, GCONF_KEY)
        self.rows = {}
        self.is_filtered = False

        self.builder = Gtk.Builder()
        if not self.builder.add_objects_from_file(UDEVDISCOVER_UI,
                ['about_action', 'expand_toggleaction',
                'followchanged_toggleaction', 'follownew_toggleaction',
                'help_action', 'preferences_action', 'quit_action',
                'reload_action', 'showparents_toggleaction', 'about_dialog',
                'deviceprop_store', 'devices_treestore', 'eventslog_textbuffer',
                'main_window']):
            raise 'Cant load %s' % UDEVDISCOVER_UI
        self.builder.connect_signals(self)

        # Widgets
        self.main_window = self.builder.get_object('main_window')
        self.devices_hpaned = self.builder.get_object('devices_hpaned')
        self.eventslog_vpaned = self.builder.get_object('eventslog_vpaned')
        self.devices_tv = self.builder.get_object('devices_tv')
        self.devices_treestore = self.builder.get_object('devices_treestore')
        self.deviceprop_tv = self.builder.get_object('deviceprop_tv')
        self.deviceprop_store = self.builder.get_object('deviceprop_store')
        self.devicename_label = self.builder.get_object('devicename_label')
        self.devicedesc_label = self.builder.get_object('devicedesc_label')
        self.device_image = self.builder.get_object('device_image')
        self.parents_toolbtn = self.builder.get_object('parents_toolbtn')
        self.expand_toggleaction = self.builder.get_object('expand_toggleaction')
        self.showparents_toggleaction = self.builder.get_object('showparents_toggleaction')
        self.eventslog_expander = self.builder.get_object('eventslog_expander')
        self.eventslog_textbuffer = self.builder.get_object('eventslog_textbuffer')
        self.follownew_toggleaction = self.builder.get_object('follownew_toggleaction')
        self.followchanged_toggleaction = \
            self.builder.get_object('followchanged_toggleaction')
        self.summary_scrollwindow = self.builder.get_object('summary_scrollwindow')
        self.devicepropsum_label = self.builder.get_object('devicepropsum_label')
        self.search_entry = self.builder.get_object('search_entry')
        self.default_radiobutton = self.builder.get_object('default_radiobutton')

        # Load gconf preferences
        self.loadconf()

        # Track main window width/height
        def catch_window_size(widget, allocate, options):
            window_alloc = widget.get_allocation()
            options['window_width'] = window_alloc.width
            options['window_height'] = window_alloc.height

        self.main_window.connect('size_allocate', catch_window_size, 
                self.options)

        # Restore main window width/heigh
        self.main_window.resize(self.options['window_width'], 
            self.options['window_height'])

        # Subsystem chooser dialog
        self.subsys_dialog = SubsystemChoserDialog(
            self.options['subsystem_preset'],
            self.options['custom_subsystem_preset'].split(' '))

        # About dialog
        self.about_dialog = self.builder.get_object('about_dialog')
        self.about_dialog.connect('response', lambda d, r: d.hide())

        # Sets up the logger
        self.logger = logging.getLogger(TEXTBUFFER_LOGGER)
        self.logger.setLevel(LOG_LEVEL)
        handler = TextBufferHandler(self.eventslog_textbuffer)
        handler.setFormatter(logging.Formatter(LOG_FORMAT,
            datefmt=LOG_DATE_FORMAT))
        self.logger.addHandler(handler)

        # Populate treeview
        self.device_finder = DeviceFinder()
        self.device_finder.scan_subsystems(
            self.subsys_dialog.get_chosen_subsystems(),
            self.options['parent_tree'])
        self.device_finder.connect('added', self.new_device)
        self.device_finder.connect('removed', self.removed_device)
        self.device_finder.connect('changed', self.changed_device)
        self.populate(self.device_finder.get_devices())

        self.parents_toolbtn.set_active(self.options['parent_tree'])
        self.expand_toggleaction.set_active(self.options['expanded'])
        self.expand_toggleaction_toggled_cb(self.expand_toggleaction)
        self.follownew_toggleaction.set_active(self.options['follownew'])
        self.followchanged_toggleaction.set_active(self.options['followchanged'])

        self.search_entry.set_property('primary-icon-sensitive',False)
        self.search_entry.set_property('secondary-icon-sensitive',False)

        # Restore paned window positions
        self.devices_hpaned.set_position(self.options['devices_hpaned'])
        self.eventslog_vpaned.set_position(self.options['eventslog_vpaned'])
        self.eventslog_expander.set_expanded(self.options['eventslog_expanded'])

    def new_device(self, device_finder, device):
        row_ref = self.add_new_device(device)

        if self.options['expanded']:
            iter_path = row_ref.get_path()
            if self.is_filtered:
                iter_path = self.modelfilter.convert_child_path_to_path(row_ref.get_path())

            self.devices_tv.expand_to_path(iter_path)

        if self.options['follownew']:
            self.devices_tv.set_cursor(row_ref.get_path(), None, False)

        self.logger.info(_('Device added: %s') % device.nice_label)

    def removed_device(self, device_finder, device):
        if self.rows.has_key(device.path):
            ref_row = self.rows[device.path]
            treeiter = self.devices_treestore.get_iter(ref_row.get_path())
            self.devices_treestore.remove(treeiter)
            del(self.rows[device.path])

        self.logger.info(_('Device removed: %s') % device.nice_label)

    def changed_device(self, device_finder, device):
        old_device = self.device_finder.get_devices_tree()[device.path]

        if self.rows.has_key(device.path):
            # Remove from tree first
            ref_row = self.rows[device.path]
            treeiter = self.devices_treestore.get_iter(ref_row.get_path())
            self.devices_treestore.remove(treeiter)
            del(self.rows[device.path])

            # Add again
            row_ref = self.add_new_device(device)

            if self.options['expanded']:
                iter_path = row_ref.get_path()
                if self.is_filtered:
                    iter_path = self.modelfilter.convert_child_path_to_path(row_ref.get_path())

                self.devices_tv.expand_to_path(iter_path)

            if self.options['followchanged']:
                self.devices_tv.set_cursor(row_ref.get_path(), None, False)

        self.logger.info(_('Device changed: %s') % device.nice_label)

        # Log device updated info and propierties
        for i in udevdiscover.device.find_differences(old_device, device):
            if i[0] == 'changed': self.logger.debug('%s %s, %s: %s -> %s' % i)
            else: self.logger.debug('%s %s, %s: %s' % i)

    def populate(self, devices):
        self.devices_treestore.clear()
        self.rows = {}

        for device in devices:
            self.add_new_device(device)

    def add_new_device(self, device):
        device_icon = theme.load_icon(device.icon, 24, 0)
        if device.parent == None:
            treeiter = self.devices_treestore.append(None, [device.path, 
                device_icon, device.nice_label, device.subsystem, False, 
                device.name])
        else:
            if self.rows.has_key(device.parent.path):
                parent_treeiter = self.devices_treestore.get_iter(
                    self.rows[device.parent.path].get_path())
                treeiter = self.devices_treestore.append(parent_treeiter, 
                    [device.path, device_icon, device.nice_label, 
                    device.subsystem, False, device.name])
            else:
                treeiter = self.devices_treestore.append(None, [device.path, 
                    device_icon, device.nice_label, device.subsystem, False, 
                    device.name])

        self.rows[device.path] = Gtk.TreeRowReference.new(self.devices_treestore,
            self.devices_treestore.get_path(treeiter))

        search_text = self.search_entry.get_text().lower()
        if search_text:
            treerow = self.devices_treestore[treeiter]

            if udevdiscover.device.match_string(device, search_text):
                treerow[VISIBLE_COL] = True
                iter_parent = self.devices_treestore.iter_parent(treeiter)
                if iter_parent:
                    self.set_branch_visible(iter_parent)
            else:
                treerow[VISIBLE_COL] = False

        return self.rows[device.path]

    def preferences_action_activated_cb(self, widget):
        if self.subsys_dialog.run() == Gtk.ResponseType.ACCEPT:
            chosen_preset = self.subsys_dialog.get_current_preset()
            self.options['subsystem_preset'] = chosen_preset

            if chosen_preset == CUSTOM_SUBSYS_PRESET:
                self.options['custom_subsystem_preset'] = \
                    ' '.join(self.subsys_dialog.get_chosen_subsystems())

            self.reload_action_activate_cb()
        self.subsys_dialog.destroy()

    def showparents_toggleaction_toggled_cb(self, action):
        self.options['parent_tree'] = action.get_active()
        self.device_finder.scan_subsystems(
            self.subsys_dialog.get_chosen_subsystems(), 
            self.options['parent_tree'])

        self.populate(self.device_finder.get_devices())
        self.expand_toggleaction_toggled_cb(self.expand_toggleaction)

    def expand_toggleaction_toggled_cb(self, action):
        if action.get_active():
            self.devices_tv.expand_all()
        else:
            self.devices_tv.collapse_all()

        self.options['expanded'] = self.expand_toggleaction.get_active()

    def follownew_toggleaction_toggled_cb(self, action):
        self.options['follownew'] = self.follownew_toggleaction.get_active()

    def followchanged_toggleaction_toggled_cb(self, action):
        self.options['followchanged'] = self.followchanged_toggleaction.get_active()

    def reload_action_activate_cb(self, widget=None):
        self.device_finder.scan_subsystems(
            self.subsys_dialog.get_chosen_subsystems(),
            self.options['parent_tree'])

        self.populate(self.device_finder.get_devices())
        self.expand_toggleaction_toggled_cb(self.expand_toggleaction)

    def devices_tv_cursor_changed_cb(self, treeview):
        selection = self.devices_tv.get_selection()
        model, selected = selection.get_selected()

        if selected:
            row = model[selected]
            device = self.device_finder.get_devices_tree()[row[PATH_COL]]

            title = '<b>'+device.nice_label+'</b>'
            if hasattr(device, 'vendor_name') and device.vendor_name:
                title += '\n<i>%s</i>' % device.vendor_name.decode('UTF-8')
            if hasattr(device, 'model_name') and device.model_name:
                title += '\n<i>%s</i>' % device.model_name.decode('UTF-8')
            self.devicename_label.set_label(title)

            desc = '\n'.join([': '.join(('<b>'+key.capitalize()+'</b>', 
                str(val))) for key, val in device.get_info()])
            self.devicedesc_label.set_label(desc)

            self.device_image.set_from_icon_name(device.icon, Gtk.IconSize.DIALOG)

            if hasattr(device, 'get_summary'):
                self.summary_scrollwindow.set_visible(True)
                desc = '\n'.join([': '.join(('<b>'+key.capitalize()+'</b>', 
                    str(val))) for key, val in device.get_summary()])
                self.devicepropsum_label.set_label(desc)
            else:
                self.summary_scrollwindow.set_visible(False)

            self.deviceprop_store.clear()
            for key, val in device.get_props().items():
                self.deviceprop_store.append([key, 
                    val.decode("string-escape")])

    def search_entry_activate_cb(self, entry):
        search_text = entry.get_text().lower()
        if not search_text:
            return

        visible_iter = []
        for path in self.rows.keys():
            iter = self.devices_treestore.get_iter(self.rows[path].get_path())
            treerow = self.devices_treestore[iter]
            device = self.device_finder.get_devices_tree()[treerow[PATH_COL]]

            if udevdiscover.device.match_string(device, search_text):
                treerow[VISIBLE_COL] = True
                visible_iter.append(iter)
            else:
                treerow[VISIBLE_COL] = False

        if visible_iter:
            for iter in visible_iter:
                iter_parent = self.devices_treestore.iter_parent(iter)
                if iter_parent:
                    self.set_branch_visible(iter_parent)

        self.showparents_toggleaction.set_sensitive(False)
        self.modelfilter = self.devices_treestore.filter_new(None)
        self.modelfilter.set_visible_column(VISIBLE_COL)

        self.devices_tv.set_model(self.modelfilter)
        self.expand_toggleaction_toggled_cb(self.expand_toggleaction)
        self.is_filtered = True

    def set_branch_visible(self, itr):
        """ Sets the whole tree-branch upward as visible """

        tr = self.devices_treestore[itr]
        tr[VISIBLE_COL] = True
        itr_prnt = self.devices_treestore.iter_parent(itr)
        if itr_prnt:
            self.set_branch_visible(itr_prnt)

    def search_entry_changed_cb(self, entry):
        if entry.get_text():
            entry.set_property('primary-icon-sensitive',True)
            entry.set_property('secondary-icon-sensitive',True)
        else:
            self.devices_tv.set_model(self.devices_treestore)
            self.expand_toggleaction_toggled_cb(self.expand_toggleaction)
            self.showparents_toggleaction.set_sensitive(True)
            entry.set_property('primary-icon-sensitive',False)
            entry.set_property('secondary-icon-sensitive',False)
            self.is_filtered = False

    def search_entry_icon_release_cb(self, entry, icon_pos, event):
        if icon_pos == 0:
            entry.set_text('')
            self.devices_tv.set_model(self.devices_treestore)
            self.expand_toggleaction_toggled_cb(self.expand_toggleaction)
        else:
            self.search_entry_activate_cb(entry)

    def about_action_activate_cb(self, widget):
        self.about_dialog.run()

    def help_action_activate_cb(self, widget):
        pass

    def quit_action_activate_cb(self, data=None):
        self.options['devices_hpaned'] = self.devices_hpaned.get_position()
        self.options['eventslog_vpaned'] = self.eventslog_vpaned.get_position()
        self.options['eventslog_expanded'] = self.eventslog_expander.get_expanded()
        self.options['follownew'] = self.follownew_toggleaction.get_active()
        self.options['followchanged'] = self.followchanged_toggleaction.get_active()
        self.saveconf()
        sys.exit(0)

if __name__ == '__main__':
    UDevDiscoverGUI()
    GObject.MainLoop().run()
