#!/usr/bin/env python
# -*- coding: utf-8 -*

'''
    Metadata anonymisation toolkit - GUI edition
'''

import gtk
import gobject

import gettext
import locale
import logging
import os
import sys
import mimetypes
import xml.sax
import urllib2

from lib import mat
from lib import strippers


logging.basicConfig(level=mat.LOGGING_LEVEL)


class CFile(object):
    '''
        Contain the "parser" class of the file "filename"
        This class exist just to be "around" my parser.Generic_parser class,
        since the gtk.ListStore does not accept it.
    '''
    def __init__(self, filename, backup, add2archive):
        try:
            self.file = mat.create_class_file(filename, backup, add2archive)
        except:
            self.file = None


class GUI:
    '''
        Main GUI class
    '''
    def __init__(self):
        # Preferences
        self.force = False
        self.backup = True
        self.add2archive = True

        # Main window
        self.window = gtk.Window()
        self.window.set_title('Metadata Anonymisation Toolkit')
        self.window.connect('destroy', gtk.main_quit)
        self.window.set_default_size(800, 600)
        self.logo = mat.get_sharedir('logo.png')
        icon = gtk.gdk.pixbuf_new_from_file_at_size(self.logo, 50, 50)
        self.window.set_icon(icon)

        self.accelerator = gtk.AccelGroup()
        self.window.add_accel_group(self.accelerator)

        vbox = gtk.VBox()
        self.window.add(vbox)

        menubar = self.__create_menu()
        toolbar = self.__create_toolbar()
        content = gtk.ScrolledWindow()
        content.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        vbox.pack_start(menubar, False, True, 0)
        vbox.pack_start(toolbar, False, True, 0)
        vbox.pack_start(content, True, True, 0)

        # parser.class - name - path - type - cleaned
        self.liststore = gtk.ListStore(object, str, str, str, str, str)

        self.treeview = gtk.TreeView(model=self.liststore)
        self.treeview.set_search_column(1)  # filename column is searchable
        self.treeview.set_rules_hint(True)  # alternate colors for rows
        self.treeview.set_rubber_banding(True)  # mouse selection
        self.treeview.connect("key_press_event", self.treeview_keyboard_event)
        self.treeview.connect('row-activated', self.__popup_metadata)
        self.treeview.connect('drag_data_received',
                self.__on_drag_data_received)
        self.treeview.drag_dest_set(gtk.DEST_DEFAULT_MOTION |
                gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_DROP,
                [('text/uri-list', 0, 80), ], gtk.gdk.ACTION_COPY)
        self.__add_columns()
        self.selection = self.treeview.get_selection()
        self.selection.set_mode(gtk.SELECTION_MULTIPLE)

        content.add(self.treeview)

        self.statusbar = gtk.Statusbar()
        self.statusbar.push(1, _('Ready'))
        vbox.pack_start(self.statusbar, False, False, 0)

        self.window.show_all()

    def __create_toolbar(self):
        '''
            Returns a vbox object, which contains a toolbar with buttons
        '''
        toolbar = gtk.Toolbar()

        toolbutton = gtk.ToolButton(gtk.STOCK_ADD)
        toolbutton.set_label(_('Add'))
        toolbutton.connect('clicked', self.__add_files)
        toolbutton.set_tooltip_text(_('Add files'))
        toolbar.add(toolbutton)

        toolbutton = gtk.ToolButton(gtk.STOCK_CLEAR)
        toolbutton.set_label(_('Clean'))
        toolbutton.connect('clicked', self.__process_files, self.__mat_clean)
        toolbutton.set_tooltip_text(_('Clean selected files'))
        toolbar.add(toolbutton)

        toolbutton = gtk.ToolButton(gtk.STOCK_FIND)
        toolbutton.set_label(_('Check'))
        toolbutton.connect('clicked', self.__process_files, self.__mat_check)
        toolbutton.set_tooltip_text(_('Check selected files for harmful meta'))
        toolbar.add(toolbutton)

        toolbutton = gtk.ToolButton(stock_id=gtk.STOCK_QUIT)
        toolbutton.set_label(_('Quit'))
        toolbutton.connect('clicked', gtk.main_quit)
        toolbar.add(toolbutton)

        vbox = gtk.VBox(spacing=3)
        vbox.pack_start(toolbar, False, False, 0)
        return vbox

    def __add_columns(self):
        '''
            Create the columns, and add them to the treeview
        '''
        colname = [_('Path'), _('Filename'), _('Mimetype'), _('State'),
            _('Cleaned file')]

        for i, j in enumerate(colname, 1):
            filename_column = gtk.CellRendererText()
            column = gtk.TreeViewColumn(j, filename_column, text=i)
            column.set_sort_column_id(i)
            column.set_resizable(True)  # column is resizeable
            self.treeview.append_column(column)

    def __create_menu_item(self, name, func, menu, pix, shortcut):
        '''
            Create a MenuItem() like Preferences, Quit, Add, Clean, ...
        '''
        item = gtk.ImageMenuItem()
        if shortcut:
            key, mod = gtk.accelerator_parse(shortcut)
            item.add_accelerator('activate', self.accelerator,
                key, mod, gtk.ACCEL_VISIBLE)
        picture = gtk.Image()
        picture.set_from_stock(pix, gtk.ICON_SIZE_MENU)
        item.set_image(picture)
        item.set_label('_' + name)
        item.set_use_underline(True)
        item.connect('activate', func)
        menu.append(item)

    def __create_sub_menu(self, name, menubar):
        '''
            Create a submenu like File, Edit, Clean, ...
        '''
        submenu = gtk.Menu()
        menuitem = gtk.MenuItem()
        menuitem.set_submenu(submenu)
        menuitem.set_label('_' + name)
        menuitem.set_use_underline(True)
        menubar.append(menuitem)
        return submenu

    def __create_menu(self):
        '''
            Return a MenuBar
        '''
        menubar = gtk.MenuBar()

        file_menu = self.__create_sub_menu(_('Files'), menubar)
        self.__create_menu_item(_('Add files'), self.__add_files, file_menu,
            gtk.STOCK_ADD, '<Control>O')
        self.__create_menu_item(_('Quit'), gtk.main_quit, file_menu,
            gtk.STOCK_QUIT, '<Control>Q')

        edit_menu = self.__create_sub_menu(_('Edit'), menubar)
        self.__create_menu_item(_('Clear the filelist'),
            lambda x: self.liststore.clear(), edit_menu, gtk.STOCK_REMOVE,
            None)
        self.__create_menu_item(_('Preferences'), self.__preferences,
                edit_menu, gtk.STOCK_PREFERENCES, '<Control>P')

        process_menu = self.__create_sub_menu(_('Process'), menubar)
        item = gtk.ImageMenuItem()
        key, mod = gtk.accelerator_parse('<Control>L')
        item.add_accelerator('activate', self.accelerator,
            key, mod, gtk.ACCEL_VISIBLE)
        picture = gtk.Image()
        picture.set_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU)
        item.set_image(picture)
        item.set_label(_('Clean'))
        item.connect('activate', self.__process_files, self.__mat_clean)
        process_menu.append(item)

        item = gtk.ImageMenuItem()
        key, mod = gtk.accelerator_parse('<Control>h')
        item.add_accelerator('activate', self.accelerator,
            key, mod, gtk.ACCEL_VISIBLE)
        picture = gtk.Image()
        picture.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
        item.set_image(picture)
        item.set_label(_('Check'))
        item.connect('activate', self.__process_files, self.__mat_check)
        process_menu.append(item)

        help_menu = self.__create_sub_menu(_('Help'), menubar)
        self.__create_menu_item(_('Supported formats'), self.__supported,
            help_menu, gtk.STOCK_INFO, False)
        self.__create_menu_item(_('About'), self.__about, help_menu,
                gtk.STOCK_ABOUT, False)

        return menubar

    def treeview_keyboard_event(self, widget, event):
        '''
            Remove selected files from the treeview
            when the use hit the 'suppr' key
        '''
        if gtk.gdk.keyval_name(event.keyval) == "Delete":
            rows = []
            self.selection.selected_foreach(
                    lambda model, path, iter: rows.append(iter))
            [self.liststore.remove(i) for i in rows]

    def __add_files(self, button):
        '''
            Add the files chosed by the filechoser ("Add" button)
        '''
        chooser = gtk.FileChooserDialog(title=_('Choose files'),
            parent=self.window, action=gtk.FILE_CHOOSER_ACTION_OPEN,
            buttons=(gtk.STOCK_OK, 0, gtk.STOCK_CANCEL, 1))
        chooser.set_default_response(0)
        chooser.set_select_multiple(True)

        all_filter = gtk.FileFilter()  # filter that shows all files
        all_filter.set_name(_('All files'))
        all_filter.add_pattern('*')
        chooser.add_filter(all_filter)

        supported_filter = gtk.FileFilter()
        # filter that shows only supported formats
        [supported_filter.add_mime_type(i) for i in strippers.STRIPPERS.keys()]
        supported_filter.set_name(_('Supported files'))
        chooser.add_filter(supported_filter)

        response = chooser.run()

        if response is 0:  # gtk.STOCK_OK
            filenames = chooser.get_filenames()
            task = self.populate(filenames)
            gobject.idle_add(task.next)  # asynchrone processing
        chooser.destroy()

    def populate(self, filenames):
        '''
            Append selected files by add_file to the self.liststore
        '''
        not_supported = []
        for filename in filenames:  # filenames : all selected files/folders
            if os.path.isdir(filename):  # if "filename" is a directory
                for root, dirs, files in os.walk(filename):
                    for item in files:
                        path_to_file = os.path.join(root, item)
                        if self.__add_file_to_treeview(path_to_file):
                            not_supported.append(item)
            else:  # filename is a regular file
                if self.__add_file_to_treeview(filename):
                    not_supported.append(filename)
            yield True
        if not_supported:
            self.__popup_non_supported(not_supported)
        yield False

    def __add_file_to_treeview(self, filename):
        '''
            Add a file to the list if it's format is supported
        '''
        if not os.path.isfile(filename):
            # if filename does not exist
            return False

        cf = CFile(filename, self.backup, self.add2archive)
        if cf.file is not None:  # if the file is supported by the mat
            self.liststore.append([cf, os.path.dirname(cf.file.filename) + os.path.sep,
                cf.file.basename, cf.file.mime, _('unknow'), 'None'])
            return False
        else:
            return True

    def __popup_metadata(self, widget, row, col):
        '''
            Popup that display on double-clic
            metadata from a file
        '''
        label = '<b>%s</b>\'s metadatas:\n' % self.liststore[row][1]
        meta = ''
        if self.liststore[row][4] == _('Clean') or\
                self.liststore[row][0].file.is_clean():
            meta = 'No metadata found'
            self.liststore[row][4] = _('Clean')
        else:
            self.liststore[row][4] = _('Dirty')
            iterator = self.liststore[row][0].file.get_meta().iteritems()
            for i, j in iterator:
                name = '-<b>' + str(i) + '</b> : '
                meta += (name + str(j) + '\n')

        w = gtk.MessageDialog(self.window,
                gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
                gtk.MESSAGE_INFO, gtk.BUTTONS_CLOSE, label)
        w.set_resizable(True)
        w.set_size_request(400, 300)
        scrolled_window = gtk.ScrolledWindow()
        scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        w.vbox.pack_start(scrolled_window, True, True, 0)
        content = gtk.Label(meta)
        content.set_selectable(True)
        content.set_alignment(0, 0)
        content.set_use_markup(True)
        scrolled_window.add_with_viewport(content)
        w.set_markup(label)
        w.show_all()
        click = w.run()
        if click:
            w.destroy()

    def __popup_non_supported(self, filelist):
        '''
            Popup that warn the user about the unsupported files
            that he want to process
        '''
        dialog = gtk.Dialog(title=_('Not-supported'), parent=self.window,
                flags=0, buttons=(gtk.STOCK_OK, 0))
        vbox = gtk.VBox(spacing=5)
        dialog.get_content_area().pack_start(vbox, True, True, 0)
        store = gtk.ListStore(str, str)

        # append filename - mimetype to the store
        #FIXME : I'm ugly
        for item in filelist:
            mime = mimetypes.guess_type(item)[0]
            if mime:
                store.append([item, mime])
            else:
                store.append([item, 'unknown'])

        treeview = gtk.TreeView(store)
        vbox.pack_start(treeview, True, True, 0)

        #create column
        rendererText = gtk.CellRendererText()
        column = gtk.TreeViewColumn(_('Filename'), rendererText, text=0)
        treeview.append_column(column)
        column = gtk.TreeViewColumn(_('Mimetype'), rendererText, text=1)
        treeview.append_column(column)

        dialog.show_all()
        click = dialog.run()
        if click is 0:  # Ok button
            dialog.destroy()

    def __about(self, button):
        '''
            About popup
        '''
        w = gtk.AboutDialog()
        w.set_authors(['Julien (jvoisin) Voisin', ])
        w.set_artists(['Marine Benoît', ])
        w.set_copyright('GNU Public License v2')
        w.set_comments(_('This software was coded during the GSoC 2011'))
        w.set_logo(gtk.gdk.pixbuf_new_from_file_at_size(self.logo, 400, 200))
        w.set_program_name('Metadata Anonymisation Toolkit')
        w.set_version(mat.__version__)
        w.set_website('https://mat.boum.org')
        w.set_website_label(_('Website'))
        w.set_position(gtk.WIN_POS_CENTER)
        w.run()
        w.destroy()

    def __supported(self, button):
        '''
            List the supported formats
        '''
        dialog = gtk.Dialog(_('Supported formats'), self.window, 0,
            (gtk.STOCK_CLOSE, 0))
        vbox = gtk.VBox(spacing=5)
        dialog.get_content_area().pack_start(vbox, True, True, 0)

        label = gtk.Label()
        label.set_markup('<big><u>Supported fileformats</u></big>')
        vbox.pack_start(label, True, True, 0)

        #parsing xml
        handler = mat.XMLParser()
        parser = xml.sax.make_parser()
        parser.setContentHandler(handler)
        path = mat.get_sharedir('FORMATS')
        with open(path, 'r') as xmlfile:
            parser.parse(xmlfile)

        def expander_callback(current):
            ''' Close every expander except the current one '''
            for i in vbox.get_children()[1:]:  # first child is a gtk.Label
                if i != current:
                    i.set_expanded(False)

        for item in handler.list:  # list of dict : one dict per format
            # create one expander per format
            # only if the format is supported
            if item['mimetype'].split(',')[0] in strippers.STRIPPERS:
                # some format have more than one mimetype
                title = '%s (%s)' % (item['name'], item['extension'])
                support = ('\t<b>%s</b> : %s' % ('support', item['support']))
                metadata = '\n\t<b>metadata</b> : ' + item['metadata']
                method = '\n\t<b>method</b> : ' + item['method']
                content = support + metadata + method
                if item['support'] == 'partial':
                    content += '\n\t<b>remaining</b> : ' + item['remaining']

                expander = gtk.Expander(title)
                vbox.pack_start(expander, False, False, 0)
                label = gtk.Label()
                label.set_markup(content)
                expander.add(label)
                expander.connect('activate', expander_callback)

        dialog.show_all()
        click = dialog.run()
        if click is 0:  # Close
            dialog.destroy()

    def __preferences(self, button):
        '''
            Preferences popup
        '''
        dialog = gtk.Dialog(_('Preferences'), self.window, 0,
                (gtk.STOCK_OK, 0))
        hbox = gtk.HBox()
        dialog.get_content_area().pack_start(hbox, False, False, 0)

        icon = gtk.Image()
        icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
        hbox.pack_start(icon, False, False, 20)

        table = gtk.Table(3, 2, False)  # nb rows, nb lines
        hbox.pack_start(table, True, True, 0)

        force = gtk.CheckButton(_('Force Clean'), False)
        force.set_active(self.force)
        force.connect('toggled', self.__invert, 'force')
        force.set_tooltip_text(_('Do not check if already clean before \
cleaning'))
        table.attach(force, 0, 1, 0, 1)

        backup = gtk.CheckButton(_('Backup'), False)
        backup.set_active(self.backup)
        backup.connect('toggled', self.__invert, 'backup')
        backup.set_tooltip_text(_('Keep a backup copy'))
        table.attach(backup, 0, 1, 1, 2)

        add2archive = gtk.CheckButton(_('Add unsupported file to archives'),
            False)
        add2archive.set_active(self.add2archive)
        add2archive.connect('toggled', self.__invert, 'add2archive')
        add2archive.set_tooltip_text(_('Add non-supported (and so \
non-anonymised) file to output archive'))
        table.attach(add2archive, 0, 1, 2, 3)

        hbox.show_all()
        response = dialog.run()
        if response is 0:  # gtk.STOCK_OK
            dialog.destroy()

    def __invert(self, button, name):
        '''
            Invert a preference state
        '''
        if name == 'force':
            self.force = not self.force
        elif name == 'backup':
            self.backup = not self.backup
            for line in xrange(len(self.liststore)):
                # change the "backup" property of all files
                self.liststore[line][0].file.backup = self.backup
            self.treeview.get_column(4).set_visible(self.backup)
        elif name == 'add2archive':
            self.add2archive = not self.add2archive

    def __on_drag_data_received(self, widget, context,
            x, y, selection, target_type, timestamp):
        '''
            This function is called when something is
            drag'n'droped into mat.
            It basically add files.
        '''
        urls = selection.data.strip('\r\n\x00')  # strip stupid characters
        cleaned_urls = map(self.__clean_draged_file_path, urls.split('\n'))
        task = self.populate(cleaned_urls)
        gobject.idle_add(task.next)  # asynchrone processing

    def __clean_draged_file_path(self, url):
        '''
            Since the dragged urls are ugly,
            we need to process them
        '''
        url = urllib2.unquote(url)  # unescape stupid chars
        if url.startswith('file:\\\\\\'):  # windows
            return url[8:]  # 8 is len('file:///')
        elif url.startswith('file://'):  # nautilus, rox
            return url[7:]  # 7 is len('file://')
        elif url.startswith('file:'):  # xffm
            return url[5:]  # 5 is len('file:')

    def __process_files(self, button, func):
        '''
            Launch the function "func" in a asynchrone way
        '''
        iterator = self.selection.get_selected_rows()[1]
        if not iterator:  # if nothing is selected : select everything
            iterator = xrange(len(self.liststore))
        task = func(iterator)  # launch func() in an asynchrone way
        gobject.idle_add(task.next)

    def __mat_check(self, iterator):
        '''
            Check if selected elements are clean
        '''
        for line in iterator:  # for each file in selection
            self.statusbar.push(0, _('Checking %s...') % self.liststore[line][1])
            if self.force is True or self.liststore[line][4] != _('Clean'):
                if self.liststore[line][0].file.is_clean():
                    string = _('Clean')
                else:
                    string = _('Dirty')
                logging.info('%s is %s' % (self.liststore[line][1], string))
                self.liststore[line][4] = string
                yield True
        self.statusbar.push(0, _('Ready'))
        yield False

    def __mat_clean(self, iterator):
        '''
            Clean selected elements
        '''
        for line in iterator:  # for each file in selection
            logging.info('Cleaning %s' % self.liststore[line][1])
            self.statusbar.push(0, _('Cleaning %s...') % self.liststore[line][1])
            if self.force is True or self.liststore[line][4] != _('Clean'):
                if self.liststore[line][0].file.remove_all():
                    self.liststore[line][4] = _('Clean')
                    if self.backup:  # the backup copy state
                        self.liststore[line][5] = os.path.basename(self.liststore[line][0].file.output)
            yield True
        self.statusbar.push(0, _('Ready'))
        yield False

if __name__ == '__main__':
    gettext.install('MAT', unicode=True)

    #Main
    gui = GUI()

    #Add files from command line
    infiles = [arg for arg in sys.argv[1:] if os.path.exists(arg)]
    if infiles:
        task = gui.populate(infiles)
        gobject.idle_add(task.next)

    gtk.main()
