#!/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 MAT import mat
from MAT 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, **kwargs):
        try:
            self.file = mat.create_class_file(filename, backup, **kwargs)
        except:
            self.file = None


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

        # 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_logo()
        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 not response:  # 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, add2archive=self.add2archive, low_pdf_quality=self.pdf_quality)
        if cf.file:  # 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
        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 not click:  # 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_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 not click:  # Close
            dialog.destroy()

    def __preferences(self, button):
        '''
            Preferences popup
        '''
        dialog = gtk.Dialog(_('Preferences'), self.window, 0,
                (gtk.STOCK_OK, 0))
        dialog.set_resizable(False)
        dialog.set_deletable(False)
        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)

        pdf_quality = gtk.CheckButton(_('Reduce PDF quality'), False)
        pdf_quality.set_active(self.pdf_quality)
        pdf_quality.connect('toggled', self.__invert, 'pdf_quality')
        pdf_quality.set_tooltip_text(_('Reduce the produced PDF size and quality'))
        table.attach(pdf_quality, 0, 1, 2, 3)

        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, 3, 4)

        hbox.show_all()
        if not dialog.run():  # gtk.STOCK_OK
            for i in self.liststore:  # update preferences
                i[0].backup = self.backup
                i[0].add2archive = self.add2archive
                if i[2].startswith('pdf'):
                    i[0].pdf_quality = self.pdf_quality
            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 == 'pdf_quality':
            self.pdf_quality = not self.pdf_quality
        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 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 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)
    _ = gettext.gettext

    #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()
