# -*- coding: UTF-8 -*-

__revision__ = '$Id$'

# Copyright (c) 2006-2011 Piotr Ożarowski
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published byp
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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 Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

# You may use and distribute this software under the terms of the
# GNU General Public License, version 2 or later

import gc
import glob
import importlib
import logging
import os
import os.path
import struct
import time
from gettext import gettext as _
from tempfile import mkstemp

import add

import db
import edit

from gi.repository import Gtk

log = logging.getLogger("Griffith")

# detect all plugins:
__all__ = [os.path.basename(x)[:-3]
           for x in glob.glob("%s/*.py" % os.path.dirname(__file__))]
__all__.remove('__init__')


class ImportPlugin(object):
    description = None
    author = None
    email = None
    version = None
    # file chooser:
    file_filters = None
    mime_types = None

    edit = False  # open add window for every movie?

    imported = 0
    data = None

    previous_dir = None

    # maps foreign key column names to mappings dicts name to id
    foreignkeymaps = None
    # mappings tag names to id
    tagmap = None

    def __init__(self, parent, fields_to_import):
        self.parent = parent
        self.db = parent.db
        self.locations = parent.locations
        self.fields = parent.field_names
        self.conditions = parent._conditions
        self.colors = parent._colors
        self.lang_types = parent._lang_types
        self.layers = parent._layers
        self.regions = parent._regions
        self.widgets = parent.widgets['import']
        self.fields_to_import = fields_to_import
        self._continue = True
        self.__source_name = None  # define outside of __init__

    def initialize(self):
        """Initializes plugin (get all parameters from user, etc.)"""
        self.imported = 0
        return True

    def abort(self, *_args):
        """called after abort button clicked"""
        self._continue = False

    def set_source(self, name):
        """Prepare source (open file, etc.)"""
        # change current dir because there are posters with relative paths
        # (perhaps)
        self.previous_dir = os.getcwd()
        os.chdir(os.path.dirname(name))

    def count_movies(self):
        """Returns number of movies in file which is about to be imported"""
        pass

    def get_movie_details(self):
        """Returns dictionary with movie details"""
        pass

    def run(self, name):
        """Import movies, function called in a loop over source files"""
        from add import validate_details, edit_movie
        from sqlalchemy import select
        from gi.repository import Gtk

        if not self.set_source(name):
            log.info("Can't read data from file %s", name)
            return False

        self.widgets['pwindow'].show()
        while Gtk.events_pending():  # give GTK some time for updates
            Gtk.main_iteration()

        # progressbar
        update_on = []
        count = self.count_movies()
        if count > 0:
            for i in range(0, 100):
                update_on.append(int(float(i) / 100 * count))

        session = self.db.Session()
        session.bind = self.db.session.bind

        # move some stuff outside the loop to speed it up
        set_fraction = self.widgets['progressbar'].set_fraction
        set_text = self.widgets['progressbar'].set_text
        main_iteration = Gtk.main_iteration

        # get some values from DB to avoid queries in the loop
        statement = select(db.Movie.number, db.Movie.title, db.Movie.o_title)
        data = session.execute(statement).fetchall()
        numbers = set(i[0] for i in data)
        titles = set(i[1].lower() for i in data if i[1])
        o_titles = set(i[2].lower() for i in data if i[2])

        gc_was_enabled = gc.isenabled()
        if gc_was_enabled:
            gc.collect()
            gc.disable()

        begin = time.time()
        processed = 0
        try:
            while self._continue:
                details = self.get_movie_details()
                if details is None:
                    break

                processed += 1
                if processed in update_on:
                    set_fraction(float(processed) / count)
                    main_iteration()
                    set_text("%s (%s/%s)" % (self.imported, processed, count))
                    main_iteration()
                    main_iteration()  # extra iteration for abort button

                o_title_lower = details.get('o_title', '').lower()
                title_lower = details.get('title', '').lower()

                if o_title_lower or title_lower:
                    if o_title_lower and o_title_lower in o_titles:
                        if title_lower and title_lower in titles:
                            log.info("movie already exists (o_title=%s, "
                                     "title=%s)", details['o_title'],
                                     details['title'])
                            continue
                    elif title_lower and title_lower in titles:
                        # o_title is not available so lets check title only
                        log.info("movie already exists (title=%s)", details[
                            'title'])
                        continue
                    if self.edit:  # XXX: not used for now
                        validate_details(details, self.fields_to_import)
                        response = edit_movie(self.parent, details)  # FIXME: wait until save or cancel button pressed
                        if response == 1:
                            self.imported += 1
                    else:
                        # get the number and make it unique. number is needed
                        # later
                        number = details.get('number', 1)
                        while number in numbers:
                            number += 1
                        if 'tags' in details:
                            tags = details.pop('tags')
                        else:
                            tags = None
                        poster = None
                        if 'poster' in details:
                            poster = details.pop('poster')
                        elif 'image' in details:
                            poster = details.pop('image')
                        try:
                            # optional: do mapping of lookup data
                            if not self.foreignkeymaps:
                                self._loadmappings()
                            for fkcolumnname in self.foreignkeymaps:
                                try:
                                    if fkcolumnname in details and details[
                                        fkcolumnname]:
                                        _fkcolumn_id = int(details[
                                                               fkcolumnname])
                                except Exception as ex:
                                    log.error(
                                        f"[E011]Caught too broad "
                                        f"try-exception {type(ex).__name__} "
                                        f"with arguments '{ex.args}'"
                                        )
                                    try:
                                        fkcolumn_id = self.normalizename(
                                            details[fkcolumnname])
                                        currentmap = self.foreignkeymaps[
                                            fkcolumnname]
                                        if fkcolumn_id in currentmap:
                                            details[fkcolumnname] = (
                                                currentmap)[fkcolumn_id]
                                        else:
                                            details[fkcolumnname] = (
                                                self._addmappingvalue(
                                                    fkcolumnname, details[
                                                        fkcolumnname]))
                                    except Exception as ex:
                                        log.error(f"[E012]Caught too broad "
                                                  f"try-exception "
                                                  f"{type(ex).__name__} with "
                                                  f"arguments '{ex.args}'")
                                        log.exception(fkcolumnname)
                                        details[fkcolumnname] = None
                            # validation before insertion
                            validate_details(details, self.fields_to_import)
                            # set number here again because if it is not
                            # selected validate_details will remove it
                            details['number'] = number
                            # insert the movie in the database
                            result = session.execute(db.tables.movies.insert(), details)
                            self.imported += 1
                            # optional: adding tags
                            if tags:
                                if self.tagmap is None:
                                    self.loadmappings()
                                for tag in tags:
                                    try:
                                        if isinstance(tag, str):
                                            # TODO: adding new tag names?
                                            tag_id = self.tagmap[tag.lower()]
                                        else:
                                            tag_id = int(tag)
                                        session.execute(
                                            db.tables.movie_tag.insert(),
                                            {
                                                'movie_id': result.lastrowid,
                                                'tag_id': tag_id,
                                            },
                                        )
                                    except Exception as ex:
                                        log.error(f"[E013]Caught too broad "
                                                  f"try-exception "
                                                  f"{type(ex).__name__} with "
                                                  f"arguments '{ex.args}'")
                                        pass
                            self.db.session.commit()
                            # adding poster
                            if poster:
                                posterfilename = ''
                                if len(poster) > 4:
                                    # check for JPEG/PNG header otherwise it
                                    # should be a filename
                                    header = struct.unpack_from(
                                        '4s', poster.encode('utf-8'))[0]
                                    if header == '\xff\xd8\xff\xe0' or \
                                            header == '\x89\x50\x4e\x47':
                                        # make a temporary file
                                        try:
                                            posterfilefd, posterfilename = (
                                                mkstemp('.img'))
                                            try:
                                                os.write(posterfilefd, poster)
                                            finally:
                                                os.close(posterfilefd)
                                            edit.update_image(
                                                self.parent, number,
                                                posterfilename)
                                        finally:
                                            if os.path.isfile(posterfilename):
                                                os.remove(posterfilename)
                                    else:
                                        edit.update_image(self.parent,
                                                          number, poster)
                        except Exception as ex:
                            log.error(f"[E014]Caught too broad try-exception "
                                      f"{type(ex).__name__} with arguments '"
                                      f"{ex.args}'")
                            log.exception("movie details are not unique, "
                                          "skipping")
                        numbers.add(number)
                else:
                    log.info('skipping movie without title and original title')
        finally:
            log.info("Import process took %s s; %s/%s movies imported",
                     (time.time() - begin), processed, count)
            if gc_was_enabled:
                gc.enable()
            self.widgets['pwindow'].hide()
        return True

    def clear(self):
        """clear plugin before next source file"""
        self.data = None
        self.imported = 0
        self.__source_name = None
        self._continue = True
        if self.previous_dir:
            os.chdir(self.previous_dir)

    def destroy(self):
        """close all resources"""
        pass

    def _loadmappings(self):
        # add all lookup mapping lists to a list which maps by foreign key
        # column name
        self.foreignkeymaps = {
            'medium_id': self._loadmappingmedium(),
            'vcodec_id': self._loadmappingvcodecs(),
            'collection_id': self._loadmappingcollections(),
            'volume_id': self._loadmappingvolumes()
            }
        # tags are no foreign key lookup data; it's an intersection
        self.tagmap = {}
        for tag in self.db.session.query(db.Tag.tag_id, db.Tag.name).all():
            tagname = self.normalizename(tag.name)
            if tagname not in self.tagmap:
                self.tagmap[tagname] = tag.tag_id

    def _loadmappingmedium(self):
        mediummap = {}
        for medium in self.db.session.query(db.Medium.medium_id,
                                            db.Medium.name).all():
            mediumname = self.normalizename(medium.name)
            if mediumname not in mediummap:
                mediummap[mediumname] = medium.medium_id
        return mediummap

    def _loadmappingvcodecs(self):
        vcodecsmap = {}
        for vcodec in self.db.session.query(db.VCodec.vcodec_id,
                                            db.VCodec.name).all():
            vcodecname = self.normalizename(vcodec.name)
            if vcodecname not in vcodecsmap:
                vcodecsmap[vcodecname] = vcodec.vcodec_id
        return vcodecsmap

    def _loadmappingcollections(self):
        collectionsmap = {}
        for collection in self.db.session.query(db.Collection.collection_id,
                                                db.Collection.name).all():
            collectionname = self.normalizename(collection.name)
            if collectionname not in collectionsmap:
                collectionsmap[collectionname] = collection.collection_id
        return collectionsmap

    def _loadmappingvolumes(self):
        volumesmap = {}
        for volume in self.db.session.query(db.Volume.volume_id,
                                            db.Volume.name).all():
            volumename = self.normalizename(volume.name)
            if volumename not in volumesmap:
                volumesmap[volumename] = volume.volume_id
        return volumesmap

    def _addmappingvalue(self, fkcolumnname, newname):
        mapping_id = None
        log.debug("Adding <%s> for <%s>", newname, fkcolumnname)
        if fkcolumnname == 'medium_id':
            mapping_id = add.add_medium(self.parent, newname)
            self.foreignkeymaps['medium_id'] = self._loadmappingmedium()
        elif fkcolumnname == 'vcodec_id':
            mapping_id = add.add_vcodec(self.parent, newname)
            self.foreignkeymaps['vcodec_id'] = self._loadmappingvcodecs()
        elif fkcolumnname == 'collection_id':
            mapping_id = add.add_collection(self.parent, newname)
            self.foreignkeymaps['collection_id'] = (
                self._loadmappingcollections())
        elif fkcolumnname == 'volume_id':
            mapping_id = add.add_volume(self.parent, newname)
            self.foreignkeymaps['volume_id'] = self._loadmappingvolumes()
        return mapping_id

    def normalizename(self, name):
        name = name.replace('-', '')
        name = name.replace('_', '')
        name = name.replace(' ', '')
        # unicode whitespace from XML data (&x160;)
        name = name.replace('\xa0', '')
        return name.lower().strip()


def on_import_plugin_changed(_combobox, widgets, *_args):
    import plugins.imp
    plugin_name = widgets['plugin'].get_active_text()
    __import__("plugins.imp.%s" % plugin_name)
    ip = eval("plugins.imp.%s.ImportPlugin" % plugin_name)
    widgets['author'].set_markup(f"<i>{ip.author}</i>")
    widgets['email'].set_markup(f"<i>{ip.email}</i>")
    widgets['version'].set_markup(f"<i>{ip.version}</i>")
    widgets['description'].set_markup(f"<i>{ip.description}</i>")
    # file filters
    for i in widgets['fcw'].list_filters():
        widgets['fcw'].remove_filter(i)
    f = Gtk.FileFilter()
    f.set_name(plugin_name)
    if ip.file_filters is not None:
        if (isinstance(ip.file_filters, tuple)
                or isinstance(ip.file_filters, list)):
            for i in ip.file_filters:
                f.add_pattern(i)
        else:
            f.add_pattern(ip.file_filters)
    if ip.mime_types is not None:
        if isinstance(ip.mime_types, tuple) or isinstance(ip.mime_types, list):
            for i in ip.mime_types:
                f.add_mime_type(i)
        else:
            f.add_mime_type(ip.mime_types)
    widgets['fcw'].add_filter(f)
    f = Gtk.FileFilter()
    f.set_name(_("All files"))
    f.add_pattern("*")
    widgets['fcw'].add_filter(f)


def on_import_button_clicked(_button, self, *_args):
    import plugins.imp
    import gutils
    plugin_name = self.widgets['import']['plugin'].get_active_text()
    filenames = self.widgets['import']['fcw'].get_filenames()

    fields = []
    w = self.widgets['import']['fields']
    for i in w:
        if w[i].get_active():
            fields.append(i)

    __import__("plugins.imp.%s" % plugin_name)
    if self.debug_mode:
        log.debug('reloading %s', plugin_name)
        import sys
        importlib.reload(sys.modules["plugins.imp.%s" % plugin_name])
    ip = eval("plugins.imp.%s.ImportPlugin(self, fields)" % plugin_name)
    if ip.initialize():
        self.widgets['window'].set_sensitive(False)
        try:
            self.widgets['import']['window'].hide()
            self.widgets['import']['pabort'].connect('clicked', ip.abort, ip)
            for filename in filenames:
                self.widgets['import']['progressbar'].set_fraction(0)
                self.widgets['import']['progressbar'].set_text('')
                if ip.run(filename):
                    gutils.info(_("%s file has been imported. %s movies added.")
                                % (plugin_name, ip.imported),
                                self.widgets['window']
                                )
                    self.populate_treeview()
                ip.clear()
        except Exception as e:
            log.exception('')
            gutils.error(str(e), self.widgets['window'])
        finally:
            ip.destroy()
            self.widgets['import']['pwindow'].hide()
            self.widgets['window'].set_sensitive(True)


def on_abort_button_clicked(_button, self, *_args):
    close_dialog(self)


def on_delete_event(_widget, _event, self):
    close_dialog(self)
    return True


def close_dialog(self):
    self.widgets['import']['window'].hide()
    self.widgets['import']['pwindow'].hide()
    self.widgets['window'].set_sensitive(True)
