# -*- coding: utf-8 -*-

import gtk
import gobject
import re
import os
import sys
import hashlib
from urlparse import urlparse
from io import BytesIO
import shutil

import logging
import nbxmpp
import gtkgui_helpers
from common import gajim
from common import ged
from common import helpers
from common import configpaths
import dialogs
from plugins import GajimPlugin
from plugins.helpers import log_calls, log
from plugins.gui import GajimPluginConfigDialog
from conversation_textview import TextViewImage
from .http_functions import get_http_head, get_http_file

from common import demandimport
demandimport.enable()
demandimport.ignore += ['_imp']

log = logging.getLogger('gajim.plugin_system.url_image_preview')

try:
    from PIL import Image
except:
    log.debug('Pillow not available')

try:
    if os.name == 'nt':
        from cryptography.hazmat.backends.openssl import backend
    else:
        from cryptography.hazmat.backends import default_backend
    from cryptography.hazmat.primitives.ciphers import Cipher
    from cryptography.hazmat.primitives.ciphers import algorithms
    from cryptography.hazmat.primitives.ciphers.modes import GCM
    decryption_available = True
except Exception as e:
    DEP_MSG = 'For preview of encrypted images, ' \
              'please install python-cryptography!'
    log.debug('Cryptography Import Error: ' + str(e))
    log.info('Decryption/Encryption disabled due to errors')
    decryption_available = False

ACCEPTED_MIME_TYPES = ('image/png', 'image/jpeg', 'image/gif', 'image/raw',
                       'image/svg+xml', 'image/x-ms-bmp')


class UrlImagePreviewPlugin(GajimPlugin):
    @log_calls('UrlImagePreviewPlugin')
    def init(self):
        if not decryption_available:
            self.available_text = DEP_MSG
        self.config_dialog = UrlImagePreviewPluginConfigDialog(self)
        self.events_handlers = {}
        self.events_handlers['message-received'] = (
            ged.PRECORE, self.handle_message_received)
        self.gui_extension_points = {
            'chat_control_base': (self.connect_with_chat_control,
                                  self.disconnect_from_chat_control),
            'print_special_text': (self.print_special_text, None), }
        self.config_default_values = {
            'PREVIEW_SIZE': (150, 'Preview size(10-512)'),
            'MAX_FILE_SIZE': (524288, 'Max file size for image preview')}
        self.controls = {}

    # remove oob tag if oob url == message text
    def handle_message_received(self, event):
        oob_node = event.stanza.getTag('x', namespace=nbxmpp.NS_X_OOB)
        oob_url = None
        oob_desc = None
        if oob_node:
            oob_url = oob_node.getTagData('url')
            oob_desc = oob_node.getTagData('desc')
            if oob_url and oob_url == event.msgtxt and \
                    (not oob_desc or oob_desc == ""):
                log.debug("Detected oob tag containing same"
                          "url as the message text, deleting oob tag...")
                event.stanza.delChild(oob_node)

    @log_calls('UrlImagePreviewPlugin')
    def connect_with_chat_control(self, chat_control):
        account = chat_control.contact.account.name
        jid = chat_control.contact.jid
        if account not in self.controls:
            self.controls[account] = {}
        self.controls[account][jid] = Base(self, chat_control)

    @log_calls('UrlImagePreviewPlugin')
    def disconnect_from_chat_control(self, chat_control):
        account = chat_control.contact.account.name
        jid = chat_control.contact.jid
        self.controls[account][jid].deinit()
        del self.controls[account][jid]

    def print_special_text(self, tv, special_text, other_tags, graphics=True,
                           iter_=None):
        account = tv.account
        for jid in self.controls[account]:
            if self.controls[account][jid].chat_control.conv_textview != tv:
                continue
            self.controls[account][jid].print_special_text(
                special_text, other_tags, graphics=True, iter_=iter_)
            return


class Base(object):
    def __init__(self, plugin, chat_control):
        self.plugin = plugin
        self.chat_control = chat_control
        self.textview = self.chat_control.conv_textview
        self.handlers = {}
        if os.name == 'nt':
            self.backend = backend
        else:
            self.backend = default_backend()

        self.directory = os.path.join(configpaths.gajimpaths['MY_DATA'],
                                      'downloads')
        self.thumbpath = os.path.join(configpaths.gajimpaths['MY_CACHE'],
                                      'downloads.thumb')

        try:
            self._create_path(self.directory)
            self._create_path(self.thumbpath)
        except Exception as e:
            log.error("Error creating download and/or thumbnail folder!")
            raise

    def deinit(self):
        # remove all register handlers on wigets, created by self.xml
        # to prevent circular references among objects
        for i in list(self.handlers.keys()):
            if self.handlers[i].handler_is_connected(i):
                self.handlers[i].disconnect(i)
            del self.handlers[i]

    def print_special_text(self, special_text, other_tags, graphics=True,
                           iter_=None):
        # remove qip bbcode
        special_text = special_text.rsplit('[/img]')[0]

        if special_text.startswith('www.'):
            special_text = 'http://' + special_text
        if special_text.startswith('ftp.'):
            special_text = 'ftp://' + special_text

        urlparts = urlparse(special_text)
        if urlparts.scheme not in ["https", "http", "ftp", "ftps"] or \
                not urlparts.netloc:
            log.info("Not accepting URL for image preview: %s" % special_text)
            return

        buffer_ = self.textview.tv.get_buffer()
        if not iter_:
            iter_ = buffer_.get_end_iter()

        # Show URL, until image is loaded (if ever)
        ttt = buffer_.get_tag_table()
        repl_start = buffer_.create_mark(None, iter_, True)
        buffer_.insert_with_tags(iter_, special_text,
            *[(ttt.lookup(t) if isinstance(t, str) else t) for t in ["url"]])
        repl_end = buffer_.create_mark(None, iter_, True)

        filename = os.path.basename(urlparts.path)
        ext = os.path.splitext(filename)[1]
        name = os.path.splitext(filename)[0]
        namehash = hashlib.sha1(special_text).hexdigest()
        newfilename = name + '_' + namehash + ext
        thumbfilename = name + '_' + namehash + '_thumb' + ext

        filepath = os.path.join(self.directory, newfilename)
        thumbpath = os.path.join(self.thumbpath, thumbfilename)
        filepaths = [filepath, thumbpath]

        key = ''
        iv = ''
        encrypted = False
        if len(urlparts.fragment):
            fragment = []
            for i in range(0, len(urlparts.fragment), 2):
                fragment.append(chr(int(urlparts.fragment[i:i + 2], 16)))
            fragment = ''.join(fragment)
            key = fragment[16:]
            iv = fragment[:16]
            if len(key) == 32 and len(iv) == 16:
                encrypted = True

        # file exists but thumbnail got deleted
        if os.path.exists(filepath) and not os.path.exists(thumbpath):
            with open(filepath, 'rb') as f:
                mem = f.read()
                f.closed
            gajim.thread_interface(
                self._save_thumbnail, [thumbpath, (mem, '')],
                self._update_img, [special_text, repl_start,
                                   repl_end, filepath, encrypted])

        # display thumbnail if already downloadeded
        # (but only if file also exists)
        elif os.path.exists(filepath) and os.path.exists(thumbpath):
            gajim.thread_interface(
                self._load_thumbnail, [thumbpath],
                self._update_img, [special_text, repl_start,
                                   repl_end, filepath, encrypted])

        # or download file, calculate thumbnail and finally display it
        else:
            if encrypted and not decryption_available:
                log.debug('Please install Crytography to decrypt pictures')
            else:
                # First get the http head request
                # which does not fetch data, just headers
                # then check the mime type and filesize
                gajim.thread_interface(
                    get_http_head, [self.textview.account, special_text],
                    self._check_mime_size, [special_text, repl_start, repl_end,
                                            filepaths, key, iv, encrypted])

        # Don't print the URL in the message window (in the calling function)
        self.textview.plugin_modified = True

    def _save_thumbnail(self, thumbpath, (mem, alt)):
        size = self.plugin.config['PREVIEW_SIZE']
        use_gtk = False
        output = None

        try:
            output = BytesIO()
            im = Image.open(BytesIO(mem))
            im.thumbnail((size, size), Image.ANTIALIAS)
            im.save(output, "jpeg", quality=100, optimize=True)
        except Exception as e:
            if output:
                output.close()
            log.info("Failed to load image using pillow, "
                     "falling back to gdk pixbuf.")
            log.debug(e)
            use_gtk = True

        if use_gtk:
            log.info("Pillow not available or file corrupt, "
                     "trying to load using gdk pixbuf.")
            try:
                output = BytesIO()
                loader = gtk.gdk.PixbufLoader()
                loader.write(mem)
                loader.close()
                pixbuf = loader.get_pixbuf()
                pixbuf, w, h = self._get_pixbuf_of_size(pixbuf, size)

                def cb(buf, data=None):
                    output.write(buf)
                    return True

                pixbuf.save_to_callback(cb, "jpeg", {"quality": "100"})
            except Exception as e:
                if output:
                    output.close()
                log.info("Failed to load image using gdk pixbuf, "
                         "ignoring image.")
                log.debug(e)
                return ('', '')

        mem = output.getvalue()
        output.close()
        try:
            self._create_path(os.path.dirname(thumbpath))
            self._write_file(thumbpath, mem)
        except Exception as e:
            dialogs.ErrorDialog(
                _('Could not save file'),
                _('Exception raised while saving thumbnail '
                  'for image file (see error log for more '
                  'information)'),
                transient_for=self.chat_control.parent_win.window)
            log.error(str(e))
        return (mem, alt)

    def _load_thumbnail(self, thumbpath):
        with open(thumbpath, 'rb') as f:
            mem = f.read()
            f.closed
        return (mem, '')

    def _write_file(self, path, data):
        log.info("Writing '%s' of size %d..." % (path, len(data)))
        try:
            with open(path, "wb") as output_file:
                output_file.write(data)
                output_file.closed
        except Exception as e:
            log.error("Failed to write file '%s'!" % path)
            raise

    def _update_img(self, (mem, alt), url, repl_start, repl_end,
                    filepath, encrypted):
        if mem:
            try:
                urlparts = urlparse(url)
                filename = os.path.basename(urlparts.path)
                eb = gtk.EventBox()
                eb.connect('button-press-event', self.on_button_press_event,
                           filepath, filename, url, encrypted)
                eb.connect('enter-notify-event', self.on_enter_event)
                eb.connect('leave-notify-event', self.on_leave_event)

                # this is threadsafe
                # (gtk textview is NOT threadsafe by itself!!)
                def add_to_textview():
                    try:        # textview closed in the meantime etc.
                        buffer_ = repl_start.get_buffer()
                        iter_ = buffer_.get_iter_at_mark(repl_start)
                        buffer_.insert(iter_, "\n")
                        anchor = buffer_.create_child_anchor(iter_)
                        # Use url as tooltip for image
                        img = TextViewImage(anchor, url)

                        loader = gtk.gdk.PixbufLoader()
                        loader.write(mem)
                        loader.close()
                        pixbuf = loader.get_pixbuf()
                        img.set_from_pixbuf(pixbuf)

                        eb.add(img)
                        eb.show_all()
                        self.textview.tv.add_child_at_anchor(eb, anchor)
                        buffer_.delete(iter_,
                                       buffer_.get_iter_at_mark(repl_end))
                    except:
                        pass
                    return False
                # add to mainloop --> make call threadsafe
                gobject.idle_add(add_to_textview)
            except Exception:
                # URL is already displayed
                log.error('Could not display image for URL: %s'
                          % url)
                raise
        else:
            # If image could not be downloaded, URL is already displayed
            log.error('Could not download image for URL: %s -- %s'
                      % (url, alt))

    def _check_mime_size(self, (file_mime, file_size),
                         url, repl_start, repl_end, filepaths,
                         key, iv, encrypted):
        # Check if mime type is acceptable
        if file_mime == '' and file_size == 0:
            log.info("Failed to load HEAD Request for URL: '%s'"
                     "(see debug log for more info)" % url)
            # URL is already displayed
            return
        if file_mime.lower() not in ACCEPTED_MIME_TYPES:
            log.info("Not accepted mime type '%s' for URL: '%s'"
                     % (file_mime.lower(), url))
            # URL is already displayed
            return
        # Check if file size is acceptable
        if file_size > self.plugin.config['MAX_FILE_SIZE'] or file_size == 0:
            log.info("File size (%s) too big or unknown (zero) for URL: '%s'"
                     % (str(file_size), url))
            # URL is already displayed
            return

        attributes = {'src': url,
                      'max_size': self.plugin.config['MAX_FILE_SIZE'],
                      'filepaths': filepaths,
                      'key': key,
                      'iv': iv}

        gajim.thread_interface(
            self._download_image, [self.textview.account,
                                   attributes, encrypted],
            self._update_img, [url, repl_start, repl_end,
                               filepaths[0], encrypted])

    def _download_image(self, account, attributes, encrypted):
        filepath = attributes['filepaths'][0]
        thumbpath = attributes['filepaths'][1]
        key = attributes['key']
        iv = attributes['iv']
        mem, alt = get_http_file(account, attributes)

        # Decrypt file if necessary
        if encrypted:
            mem = self._aes_decrypt_fast(key, iv, mem)

        try:
            # Write file to harddisk
            self._write_file(filepath, mem)
        except Exception as e:
            dialogs.ErrorDialog(
                _('Could not save file'),
                _('Exception raised while saving image file'
                  ' (see error log for more information)'),
                transient_for=self.chat_control.parent_win.window)
            log.error(str(e))

        # Create thumbnail, write it to harddisk and return it
        return self._save_thumbnail(thumbpath, (mem, alt))

    def _create_path(self, folder):
        if os.path.exists(folder):
            return
        log.debug("creating folder '%s'" % folder)
        os.mkdir(folder, 0700)

    def _aes_decrypt_fast(self, key, iv, payload):
        # Use AES128 GCM with the given key and iv to decrypt the payload.
        data = payload[:-16]
        tag = payload[-16:]
        decryptor = Cipher(
            algorithms.AES(key),
            GCM(iv, tag=tag),
            backend=self.backend).decryptor()
        return decryptor.update(data) + decryptor.finalize()

    def _get_pixbuf_of_size(self, pixbuf, size):
        # Creates a pixbuf that fits in the specified square of sizexsize
        # while preserving the aspect ratio
        # Returns tuple: (scaled_pixbuf, actual_width, actual_height)
        image_width = pixbuf.get_width()
        image_height = pixbuf.get_height()

        if image_width > image_height:
            if image_width > size:
                image_height = int(size / float(image_width) * image_height)
                image_width = int(size)
        else:
            if image_height > size:
                image_width = int(size / float(image_height) * image_width)
                image_height = int(size)

        crop_pixbuf = pixbuf.scale_simple(image_width, image_height,
                                          gtk.gdk.INTERP_BILINEAR)
        return (crop_pixbuf, image_width, image_height)

    def make_rightclick_menu(self, event, filepath, original_filename,
                             url, encrypted):
        xml = gtk.Builder()
        xml.set_translation_domain('gajim_plugins')
        xml.add_from_file(self.plugin.local_file_path('context_menu.ui'))
        menu = xml.get_object('context_menu')

        open_menuitem = xml.get_object('open_menuitem')
        save_as_menuitem = xml.get_object('save_as_menuitem')
        copy_link_location_menuitem = \
            xml.get_object('copy_link_location_menuitem')
        open_link_in_browser_menuitem = \
            xml.get_object('open_link_in_browser_menuitem')

        if encrypted:
            open_link_in_browser_menuitem.hide()

        id_ = open_menuitem.connect(
            'activate', self.on_open_menuitem_activate, filepath)
        self.handlers[id_] = open_menuitem
        id_ = save_as_menuitem.connect(
            'activate', self.on_save_as_menuitem_activate,
            filepath, original_filename)
        self.handlers[id_] = save_as_menuitem
        id_ = copy_link_location_menuitem.connect(
            'activate', self.on_copy_link_location_menuitem_activate, url)
        self.handlers[id_] = copy_link_location_menuitem
        id_ = open_link_in_browser_menuitem.connect(
            'activate', self.on_open_link_in_browser_menuitem_activate, url)
        self.handlers[id_] = open_link_in_browser_menuitem

        return menu

    def on_open_menuitem_activate(self, menu, filepath):
        helpers.launch_file_manager(filepath)

    def on_save_as_menuitem_activate(self, menu, filepath, original_filename):
        def on_continue(response, target_path):
            if response < 0:
                return
            shutil.copy(filepath, target_path)
            dialog.destroy()

        def on_ok(widget):
            target_path = dialog.get_filename()
            if os.path.exists(target_path):
                # check if we have write permissions
                if not os.access(target_path, os.W_OK):
                    file_name = os.path.basename(target_path)
                    dialogs.ErrorDialog(
                        _('Cannot overwrite existing file "%s"') % file_name,
                        _('A file with this name already exists and you do '
                          'not have permission to overwrite it.'))
                    return
                dialog2 = dialogs.FTOverwriteConfirmationDialog(
                    _('This file already exists'),
                    _('What do you want to do?'),
                    propose_resume=False,
                    on_response=(on_continue, target_path),
                    transient_for=dialog)
                dialog2.set_destroy_with_parent(True)
            else:
                dirname = os.path.dirname(target_path)
                if not os.access(dirname, os.W_OK):
                    dialogs.ErrorDialog(
                        _('Directory "%s" is not writable') % dirname,
                        _('You do not have permission to '
                          'create files in this directory.'))
                    return
                on_continue(0, target_path)

        def on_cancel(widget):
            dialog.destroy()

        dialog = dialogs.FileChooserDialog(
            title_text=_('Save Image as...'),
            action=gtk.FILE_CHOOSER_ACTION_SAVE,
            buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                     gtk.STOCK_SAVE, gtk.RESPONSE_OK),
            default_response=gtk.RESPONSE_OK,
            current_folder=gajim.config.get('last_save_dir'),
            on_response_ok=on_ok,
            on_response_cancel=on_cancel)

        dialog.set_current_name(original_filename)
        dialog.connect('delete-event', lambda widget, event:
                       on_cancel(widget))

    def on_copy_link_location_menuitem_activate(self, menu, url):
        clipboard = gtk.Clipboard()
        clipboard.set_text(url)

    def on_open_link_in_browser_menuitem_activate(self, menu, url):
        helpers.launch_browser_mailer('url', url)

    # Change mouse pointer to HAND2 when
    # mouse enter the eventbox with the image
    def on_enter_event(self, eb, event):
        self.textview.tv.get_window(
            gtk.TEXT_WINDOW_TEXT).set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))

    # Change mouse pointer to default when mouse leaves the eventbox
    def on_leave_event(self, eb, event):
        self.textview.tv.get_window(
            gtk.TEXT_WINDOW_TEXT).set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))

    def on_button_press_event(self, eb, event, filepath,
                              original_filename, url, encrypted):
        # left click
        if event.type == gtk.gdk.BUTTON_PRESS and event.button == 1:
            helpers.launch_file_manager(filepath)
        # right klick
        elif event.type == gtk.gdk.BUTTON_PRESS and event.button == 3:
            menu = self.make_rightclick_menu(event, filepath,
                                             original_filename, url, encrypted)
            # menu.attach_to_widget(self.tv, None)
            menu.popup(None, None, None, event.button, event.time)

    def disconnect_from_chat_control(self):
        pass


class UrlImagePreviewPluginConfigDialog(GajimPluginConfigDialog):
    max_file_size = [262144, 524288, 1048576, 5242880, 10485760]

    def init(self):
        self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path(
            'config_dialog.ui')
        self.xml = gtk.Builder()
        self.xml.set_translation_domain('gajim_plugins')
        self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, [
            'vbox1', 'liststore1'])
        self.preview_size_spinbutton = self.xml.get_object('preview_size')
        self.preview_size_spinbutton.get_adjustment().set_all(20, 10, 512, 1,
                                                              10, 0)
        self.max_size_combobox = self.xml.get_object('max_size_combobox')
        vbox = self.xml.get_object('vbox1')
        self.child.pack_start(vbox)

        self.xml.connect_signals(self)

    def on_run(self):
        self.preview_size_spinbutton.set_value(self.plugin.config[
            'PREVIEW_SIZE'])
        value = self.plugin.config['MAX_FILE_SIZE']
        if value:
            # this fails if we upgrade from an old version
            # which has other file size values than we have now
            try:
                self.max_size_combobox.set_active(
                    self.max_file_size.index(value))
            except:
                pass
        else:
            self.max_size_combobox.set_active(-1)

    def preview_size_value_changed(self, spinbutton):
        self.plugin.config['PREVIEW_SIZE'] = spinbutton.get_value()

    def max_size_value_changed(self, widget):
        self.plugin.config['MAX_FILE_SIZE'] = self.max_file_size[
            self.max_size_combobox.get_active()]