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

""" Metadata anonymisation toolkit - GUI edition """

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GObject, Gtk, GLib
from gi.repository import Gdk, GdkPixbuf

import gettext
import logging
import os
import sys
import xml.sax

try:
    from  urllib2 import unquote
except ImportError:  # python3
    from urllib.parse import unquote

from libmat import mat
from libmat import strippers
from libmat import parser
from libmat import archive

logging.basicConfig(level=mat.LOGGING_LEVEL)


class CFile(GObject.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 because it does not
        extends Gobject.Object
    """

    def __init__(self, filename, **kwargs):
        self.file = mat.create_class_file(filename, False, **kwargs)


class GUI(object):
    """ Main GUI class """

    def __init__(self):
        # Preferences
        self.add2archive = False
        self.pdf_quality = False

        # Main window
        self.builder = Gtk.Builder()
        self.builder.set_translation_domain('MAT')
        self.builder.add_from_file(mat.get_datafile_path('mat.glade'))
        self.builder.connect_signals(self)

        self.logo = mat.get_logo()
        icon = GdkPixbuf.Pixbuf.new_from_file_at_size(self.logo, 50, 50)

        self.window = self.builder.get_object('MainWindow')
        self.window.set_icon(icon)
        self.liststore = self.builder.get_object('MainWindowListstore')
        ''' The Liststore contains:
                0: The CFile instance that represents the file
                1: The file's name
                2: The file's state (Localised string)
        '''

        self.treeview = self.builder.get_object('MainWindowTreeview')

        self.statusbar = self.builder.get_object('Statusbar')
        self.statusbar.push(1, _('Ready'))

        self.__init_supported_popup()
        self.__set_drag_treeview()

        self.window.show_all()

    def __init_supported_popup(self):
        """ Initialise the "supported formats" popup """
        self.supported_dict = mat.XMLParser()
        xml_parser = xml.sax.make_parser()
        xml_parser.setContentHandler(self.supported_dict)
        path = mat.get_datafile_path('FORMATS')
        with open(path, 'r') as xmlfile:
            xml_parser.parse(xmlfile)

        supported_cbox = self.builder.get_object('supported_cbox')
        store = Gtk.ListStore(GObject.TYPE_INT, GObject.TYPE_STRING)
        for i, j in enumerate(self.supported_dict.list):
            store.append([i, j['name']])
        supported_cbox.set_model(store)
        supported_cbox.set_entry_text_column(1)
        supported_cbox.set_active(0)

        self.builder.get_object('supported_metadata').set_buffer(Gtk.TextBuffer())
        self.builder.get_object('supported_remaining').set_buffer(Gtk.TextBuffer())
        self.builder.get_object('supported_method').set_buffer(Gtk.TextBuffer())

        self.cb_update_supported_popup(supported_cbox)  # to initially fill the dialog

    def __set_drag_treeview(self):
        """ Setup the drag'n'drop handling by the treeview """
        self.treeview.drag_dest_set(
            Gtk.DestDefaults.MOTION |
            Gtk.DestDefaults.HIGHLIGHT |
            Gtk.DestDefaults.DROP,
            [], Gdk.DragAction.COPY)
        targets = Gtk.TargetList.new([])
        targets.add_uri_targets(80)
        self.treeview.drag_dest_set_target_list(targets)

    @staticmethod
    def cb_hide_widget(widget, _):
        """ This function is a little hack to hide instead
        of close re-usable popups, like supported-fileformats,
        popup-metadata, ..."""
        widget.hide()
        return False

    def cb_update_supported_popup(self, window):
        """ Fill GtkEntries of the supported_format_popups
            with corresponding data.
        """
        index = window.get_model()[window.get_active_iter()][0]
        support = self.builder.get_object('supported_support')
        support.set_text(self.supported_dict.list[index]['support'])
        metadata = self.builder.get_object('supported_metadata').get_buffer()
        metadata.set_text(self.supported_dict.list[index]['metadata'])
        method = self.builder.get_object('supported_method').get_buffer()
        method.set_text(self.supported_dict.list[index]['method'])
        remaining = self.builder.get_object('supported_remaining').get_buffer()
        remaining.set_text(self.supported_dict.list[index]['remaining'])

    @staticmethod
    def cb_close_application(_):
        """ Close the application """
        Gtk.main_quit()

    def cb_add_files(self, button):
        """ Add the files chosen by the filechooser ("Add" button) """
        chooser = Gtk.FileChooserDialog(title=_('Choose files'),
                                        parent=self.window, action=Gtk.FileChooserAction.OPEN,
                                        buttons=(Gtk.STOCK_OK, 0, Gtk.STOCK_CANCEL, 1))
        chooser.set_default_response(0)
        chooser.set_select_multiple(True)

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

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

        if not chooser.run():  # Gtk.STOCK_OK
            filenames = chooser.get_filenames()
            GLib.idle_add(self.populate(filenames).next)  # asynchronous processing
        chooser.destroy()

    def cb_popup_metadata(self, widget, row, col):
        """ Popup that display on double-click
            metadata from a file
        """
        metadataPopupListStore = self.builder.get_object('MetadataPopupListStore')
        metadataPopupListStore.clear()
        if self.liststore[row][0].file.is_clean():
            self.liststore[row][2] = _('Clean')
            metadataPopupListStore.append([_('No metadata found'), ''])
        else:
            self.liststore[row][2] = _('Dirty')
            for i, j in self.liststore[row][0].file.get_meta().items():
                metadataPopupListStore.append([i, j])

        popup_metadata = self.builder.get_object('MetadataPopup')
        title = self.liststore[row][0].file.basename
        popup_metadata.set_title(_("%s's metadata") % title.decode('utf8'))
        popup_metadata.show_all()
        popup_metadata.run()
        popup_metadata.hide()

    def cb_about_popup(self, button):
        """ About popup """
        w = Gtk.AboutDialog()
        w.set_authors(['Julien (jvoisin) Voisin', ])
        w.set_artists(['Marine BenoƮt', ])
        w.set_copyright('GNU General Public License v2')
        w.set_comments(_('Trash your meta, keep your data'))
        w.set_logo(GdkPixbuf.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.WindowPosition.CENTER)
        w.set_transient_for(self.window)
        w.run()
        w.destroy()

    def cb_supported_popup(self, w):
        """ Show the "supported formats" popup"""
        dialog = self.builder.get_object('SupportedWindow')
        dialog.show_all()
        dialog.run()
        dialog.hide()

    def cb_clear_list(self, _):
        """ Clear the file list """
        self.liststore.clear()

    def cb_mat_check(self, button):
        """ Callback for checking files """
        self.__process_files(self.__mat_check)

    def cb_mat_clean(self, button):
        """ Callback for cleaning files """
        self.__process_files(self.__mat_clean)

    def cb_preferences_popup(self, button):
        """ Preferences popup """
        dialog = Gtk.Dialog(_('Preferences'), self.window, 0, (Gtk.STOCK_OK, 0))
        dialog.connect('delete-event', self.cb_hide_widget)
        dialog.set_resizable(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.IconSize.DIALOG)
        hbox.pack_start(icon, False, False, 20)

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

        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, 0, 1)

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

        hbox.show_all()
        if not dialog.run():  # Gtk.STOCK_OK
            for f in self.liststore:  # update preferences
                f[0].file.add2archive = self.add2archive
                if f[0].file.mime.startswith('pdf'):
                    f[0].file.pdf_quality = self.pdf_quality
            dialog.hide()

    def cb_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.
        """

        def clean_path(url):
            """ Since the dragged urls are ugly,
                we need to process them
            """
            url = unquote(url)  # unquote url
            url = url.decode('utf-8')  # decode in utf-8
            if url.startswith('file:\\\\\\'):  # windows
                return url[8:]  # 8 is len('file:///')
            elif url.startswith('file://'):  # nautilus, rox, thunar
                return url[7:]  # 7 is len('file://')
            elif url.startswith('file:'):  # xffm
                return url[5:]  # 5 is len('file:')

        dirty_urls = selection.get_uris()
        cleaned_urls = map(clean_path, dirty_urls)
        GLib.idle_add(self.populate(cleaned_urls).next)  # asynchronous processing

    def __add_file_to_treeview(self, filename):
        """ Add a file to the list if its format is supported """
        cf = CFile(filename, add2archive=self.add2archive, low_pdf_quality=self.pdf_quality)
        if cf.file and cf.file.is_writable:
            self.liststore.append([cf, cf.file.basename, _('Unknown')])
            return False
        return True

    def __process_files(self, func):
        """ Launch the function "func" in a asynchronous way """
        iterator = self.treeview.get_selection().get_selected_rows()[1]
        if not iterator:  # if nothing is selected : select everything
            iterator = range(len(self.liststore))
        sync_task = func(iterator)  # launch func() in an asynchronous way
        GLib.idle_add(sync_task.next)

    def __invert(self, button, name):
        """ Invert a preference state """
        if name == 'pdf_quality':
            self.pdf_quality = not self.pdf_quality
        elif name == 'add2archive':
            self.add2archive = not self.add2archive

    def populate(self, filenames):
        """ Append selected files by add_file to the self.liststore
        :param filenames: selected files
        """
        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)
                        yield True
            else:  # filename is a regular file
                if self.__add_file_to_treeview(filename):
                    not_supported.append(filename)
                yield True
        self.cb_mat_check(None)
        if not_supported:
            self.__popup_non_supported(not_supported)
        yield False

    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=Gtk.DialogFlags.MODAL, buttons=(Gtk.STOCK_OK, 0))
        dialog.set_size_request(220, 180)
        vbox = Gtk.VBox(spacing=5)
        sc = Gtk.ScrolledWindow()
        sc.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        sc.add_with_viewport(vbox)

        dialog.get_content_area().pack_start(sc, True, True, 0)
        store = Gtk.ListStore(str, str)

        # appends "filename - reason" to the ListStore
        for item in filelist:
            if os.path.splitext(item)[1] in parser.NOMETA:
                store.append([os.path.basename(item), _('Harmless fileformat')])
            elif not os.access(item, os.R_OK):
                store.append([os.path.basename(item), _('Cant read file')])
            else:
                store.append([os.path.basename(item), _('Fileformat not supported')])

        treeview = Gtk.TreeView(store)
        vbox.pack_start(Gtk.Label(_('These files can not be processed:')), False, False, 0)
        vbox.pack_start(treeview, True, True, 0)

        # Create columns
        rendererText = Gtk.CellRendererText()
        column = Gtk.TreeViewColumn(_('Filename'), rendererText, text=0)
        treeview.append_column(column)
        column = Gtk.TreeViewColumn(_('Reason'), rendererText, text=1)
        treeview.append_column(column)

        dialog.show_all()
        dialog.run()
        dialog.destroy()

    def __popup_archive(self, file_name, files_list):
        """ Popup that shows the user what files
            are not going to be include into to outputed
            archive
        """
        dialog = Gtk.Dialog(title=_('Non-supported files in archive'), parent=self.window,
                            flags=Gtk.DialogFlags.MODAL, buttons=(_('Clean'), 0))
        dialog.set_size_request(220, 180)
        vbox = Gtk.VBox(spacing=5)
        sc = Gtk.ScrolledWindow()
        sc.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        sc.add_with_viewport(vbox)

        dialog.get_content_area().pack_start(sc, True, True, 0)
        store = Gtk.ListStore(GObject.TYPE_BOOLEAN, GObject.TYPE_STRING)
        for i in files_list:  # store.extend is not supported, wtf?!
            store.append([0, os.path.basename(i)])

        treeview = Gtk.TreeView(store)
        column_toggle = Gtk.TreeViewColumn(_('Include'))
        column_text = Gtk.TreeViewColumn(_('Filename'))
        treeview.append_column(column_toggle)
        treeview.append_column(column_text)

        cellrenderer_text = Gtk.CellRendererText()
        column_text.pack_start(cellrenderer_text, False)
        column_text.add_attribute(cellrenderer_text, 'text', 1)

        cellrenderer_toggle = Gtk.CellRendererToggle()
        column_toggle.pack_start(cellrenderer_toggle, True)
        column_toggle.add_attribute(cellrenderer_toggle, 'active', 0)

        def cell_toggled(widget, path, model):
            model[path][0] = not model[path][0]

        cellrenderer_toggle.connect('toggled', cell_toggled, store)

        vbox.pack_start(Gtk.Label(_('MAT is not able to clean the'
                                    ' following files, found in the %s archive') % file_name), False, False, 0)
        label = Gtk.Label()
        label.set_markup('Select the files you want to <b>include</b>'
                         ' in the cleaned up archive anyway.')
        vbox.pack_start(label, False, False, 0)
        vbox.pack_start(treeview, True, True, 0)

        dialog.show_all()
        dialog.run()
        dialog.destroy()
        return [i[1] for i in store if i[0]]

    def __mat_check(self, iterator):
        """ Check elements in iterator are clean """
        for line in iterator:  # for each file in selection
            msg = _('Checking %s') % self.liststore[line][1].decode('utf-8', 'replace')
            logging.info(msg)
            self.statusbar.push(0, msg)
            if self.liststore[line][0].file.is_clean():
                self.liststore[line][2] = _('Clean')
            else:
                self.liststore[line][2] = _('Dirty')
            logging.info('%s is %s' % (self.liststore[line][1], self.liststore[line][2]))
            yield True
        self.statusbar.push(0, _('Ready'))
        yield False

    def __mat_clean(self, iterator):
        """ Clean elements in iterator """
        for line in iterator:  # for each file in selection
            msg = _('Cleaning %s') % self.liststore[line][1].decode('utf-8', 'replace')
            logging.info(msg)
            self.statusbar.push(0, msg)
            is_archive = isinstance(self.liststore[line][0].file, archive.GenericArchiveStripper)
            is_terminal = isinstance(self.liststore[line][0].file, archive.TerminalZipStripper)
            list_to_add = []
            if is_archive and not is_terminal:
                unsupported_list = self.liststore[line][0].file.list_unsupported()
                if type(unsupported_list) == list and unsupported_list:
                    logging.debug("Unsupported list: %s" % unsupported_list)
                    filename = os.path.basename(self.liststore[line][0].file.filename)
                    list_to_add = self.__popup_archive(filename, unsupported_list)
                if self.liststore[line][0].file.remove_all(whitelist=list_to_add):
                    self.liststore[line][2] = _('Clean')
            elif self.liststore[line][0].file.remove_all():
                self.liststore[line][2] = _('Clean')
            yield True
        self.statusbar.push(0, _('Ready'))
        yield False


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

    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)
        GLib.idle_add(task.next)

    Gtk.main()