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

from __future__ import (unicode_literals, division, absolute_import,
                        print_function)

__license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'

try:
    from PyQt5.Qt import QAction, Qt, QApplication, QCursor, QMenu, QUrl, QMessageBox
except ImportError:
    from PyQt4.Qt import QAction, Qt, QApplication, QCursor, QMenu, QUrl, QMessageBox

import os, regex, zipfile, time, sys, json
from Queue import Queue
from collections import OrderedDict
from functools import partial
from lxml import etree
from calibre.gui2.tweak_book.plugin import Tool
from calibre.gui2.tweak_book import editor_name, set_current_container, dictionaries, editors
from calibre.gui2.tweak_book.function_replace import builtin_functions
from calibre.spell.dictionary import best_locale_for_language
from calibre.gui2 import error_dialog, info_dialog, question_dialog, gprefs, open_url
import calibre.gui2.tweak_book.function_replace as fr
from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES
from calibre.ebooks.oeb.base import NCX_MIME
from calibre.ebooks.oeb.polish.pretty import guess_type, pretty_html, pretty_opf, pretty_xml_tree
from calibre.utils.config import JSONConfig, config_dir
from calibre.utils.localization import canonicalize_lang
from calibre_plugins.typex import PLUGIN_NAME, PLUGIN_VERSION
from calibre_plugins.typex.dialogs import RemoteCtrl, RegexStrainer, ReplaceCount
from calibre_plugins.typex.utils import OrderedSet, log, myexcepthook, LOGPATH, anon, load_resources
from calibre_plugins.typex.jobutils import SearchFunctions, CountWorker
from calibre_plugins.typex.oeb import ThisBook, InfoDialog, CALIBRE_LOCALE

try:
    load_translations()
except NameError:
    pass
sys.excepthook = myexcepthook

open(LOGPATH, 'w').close()

PLUGIN_PATH = os.path.join(config_dir, 'plugins', 'Typex.zip')
COMMENTS = 'typex_comments.txt'
COMMENTS_PATH = os.path.join(config_dir, 'plugins', 'typex_comments.txt')

prefs = JSONConfig('plugins/Typex.json')


class Typex(Tool):
    name = 'typex'
    # : If True the user can choose to place this tool in the plugins toolbar
    allowed_in_toolbar = True
    # : If True the user can choose to place this tool in the plugins menu
    allowed_in_menu = True

    flags = regex.VERSION1 | regex.WORD | regex.FULLCASE | regex.MULTILINE | regex.UNICODE
    cache = {}

    def create_action(self, for_toolbar=True):
        # Create an action, this will be added to the plugins toolbar and
        # the plugins menu
        ac = QAction(get_icons('resources/typex_icon.png'), 'Typex', self.gui)
        menu = QMenu()
        menu.setToolTipsVisible(True)
        menu_item_help = menu.addAction(_('Visualiser le quick start'), partial(self.show_file, 'quickstart.pdf'))
        menu_item_help.setToolTip(_('Aide pour l\'utilisation de Typex'))
        menu_item_view_func = menu.addAction(_('Voir les fonctions regex'), self.show_func)
        menu_item_view_func.setToolTip(_('Visualiser le code des fonctions-regex'))
        menu_item_import_func = menu.addAction(_('Importer les fonctions regex'), self.import_func)
        menu_item_import_func.setToolTip(_('Importe les fonctions regex de Typex dans les fonctions sauvegardées de calibre'))
        menu_item_dict = menu.addAction(_('Aide sur les dictionnaires'), partial(self.show_file, 'dicthelp.pdf'))
        menu_item_dict.setToolTip(_('Comment installer un dictionnaire français'))
        menu_item_about = menu.addAction(_('À propos de Typex'), self.about)
        ac.setMenu(menu)
        ac.triggered.connect(self.main)
        return ac

    def get_json_file(self):
        text_files = {'Verbes_Sans_Non-verbes.txt', 'Verbes_y_c_Non-verbes.txt', 'NomsPropresCommuns.txt', \
                       'ParticipesPasses_Avoir.txt', '1erMot.txt', 'ParticipesPasses_Etre.txt'}
        with zipfile.ZipFile(PLUGIN_PATH) as zf:
            json_func = ''.join([name for name in zf.namelist() if name == 'typex_user_functions.json'])
            json_zip = ''.join([name for name in zf.namelist() if name.endswith('.json') and '/' not in name and name.startswith('Regex')])
            if json_zip == '' :
                error_dialog(self.gui, _('Pas de fichier de regex !'), \
                           _('Veuillez inclure dans l\'archive un fichier json contenant les regex'), show=True)
                return False
            racine = json_zip.split('V')[0]
            json_userlist = sorted([file for file in os.listdir(os.path.join(config_dir, 'plugins'))\
                               if file.startswith(racine) and file.endswith('.json')])
            if len(json_userlist) != 0:
                json_user = json_userlist[-1]
                for i in range(0, len(json_userlist)):
                    os.remove(os.path.join(config_dir, 'plugins', json_userlist[i]))
                if json_zip >= json_user:
                    json_name = json_zip
                    zf.extract(json_name, os.path.join(config_dir, 'plugins'))
            else :
                json_name = json_zip
                zf.extract(json_name, os.path.join(config_dir, 'plugins'))
            zf.extract(json_func, os.path.join(config_dir, 'plugins'))
            zf.extract(COMMENTS, os.path.join(config_dir, 'plugins'))
            if not os.path.exists(os.path.join(config_dir, 'dictionaries')):
                os.mkdir(os.path.join(config_dir, 'dictionaries'))
            for fn in text_files:
                with open(os.path.join(config_dir, 'dictionaries', fn), 'w') as f:
                    f.write(zf.read('resources/{}'.format(fn)))

            self.obj = JSONConfig('plugins/{}'.format(json_name))
        return True

    def main(self):
        self.tb = ThisBook(self)
        self.tc = []
        self.isactive = [w for w in self.gui.children() if w.objectName() in ('typex-view', 'typex-remote')]
        if len(self.isactive):
            return
        locale = best_locale_for_language(canonicalize_lang(self.tb.opf_lang))  # 'fra'
        if locale and not dictionaries.dictionary_for_locale(locale):
            info_dialog(self.gui, _('dictionnaires'), _('Vous n\'avez pas de dictionnaire français installé.\n\
            les regex-fonctions donneront un résultat erroné'), show=True)
        if not self.current_container:
            return error_dialog(self.gui, _('Aucun livre ouvert'), _('Ouvrez d\'abord un livre pour édition'), show=True)
        if not self.get_json_file():
            return error_dialog(self.gui, _('Pas de fichier de regex'), _('Ajoutez un fichier de regex dans l\'archive'), show=True)
        self.initial_container = self.boss.global_undo.current_container
        self.boss.add_savepoint(_('Avant {}').format(PLUGIN_NAME))

        try:
            start = time.time()
            pp = self.prime_polish()
            end = time.time()
            log('1er traitement :', end - start)
            if not pp:
                QApplication.restoreOverrideCursor()
                return self._quit(diag=False)
            self.boss.add_savepoint(_('Après embellissement'))
        except Exception:
            import traceback
            error_dialog(self.gui, _('Erreur'),
                _('Erreur dans l\'exécution des regex préliminaires. Cliquez sur "Afficher les détails" pour plus d\'info'),
                det_msg=traceback.format_exc(), show=True)
            # Revert to the saved restore point
            self.boss.revert_requested(self.boss.global_undo.previous_container)
        else:
            self.boss.update_editors_from_container()

        start = time.time()
        infd = InfoDialog(self.tb, self.tc).exec_()
        end = time.time()
        log('infodialogue :', end - start)
        if not infd:
            self.boss.revert_requested(self.initial_container)
            return self._quit(diag=False)

        self.json_func = JSONConfig('plugins/typex_user_functions.json')
        self.sf = SearchFunctions(self)
        self.searches, self.key_search_map = self.sf.searches_splitter()

        fr.user_functions.update(self.json_func)
        self.fdict = fr.functions(refresh=True)

        builtin = {'Capitalize text (ignore tags)', 'Title-case text (ignore tags)', 'Capitalize text', \
                    'Upper-case text (ignore tags)', 'Upper-case text'}
        for funcname in [d['replace'] for d in self.obj['searches'] if d['mode'] == "function"]:
            if  funcname not in self.json_func and funcname not in builtin :
                error_dialog(None, _('Erreur'), \
                              _("Le nom de la fonction-regex '{}' ne correspond à rien dans le fichier 'typex_user_functions.json'.\
                              Veuillez faire un rapport de bug.").format(funcname), \
                               show=True)

        self.somethingchanged = False
        self.i = 0
        self.global_count = OrderedDict()
        self.reg_count_map = OrderedDict()
        self.count_map = OrderedDict()
        self.key_file_map = {}

        self.dlg = RegexStrainer(self, self.searches, self.count_map)
        self.dlg.widg.applysignal.connect(self.apply)
        self.dlg.typexclosed.connect(self._quit)
        self.dlg.show()
        self.dlg.setFocus()

    def apply(self):
        self.steps = []
        selected_keys, title, self.steps = self.dlg.get_keys()
        # log(u'regex sélectionnées : en auto {}\n\t\t\t en PàP {}'.format(selected_keys, self.steps))
        try:
            changedfiles = self.regexterminator(selected_keys, title=title)
            self.boss.update_editors_from_container()
        except Exception:
            import traceback
            error_dialog(self.gui, _('Erreur'),
                _('Erreur dans l\'exécution des regex. Cliquez sur "Afficher les détails" pour plus d\'info'),
                det_msg=traceback.format_exc(), show=True)
            # Revert to the saved restore point
            self.boss.revert_requested(self.boss.global_undo.previous_container)
        else:
            self.steps.sort()
            stepsearches = []
            stepcounts = []
            for step in self.steps :
                stepname = self.key_search_map[step]
                stepsearches.append(stepname)
                stepcount = self.count_map[stepname['name']]
                stepcounts.append(stepcount)
            if len(self.reg_count_map) :
                refresh = True
                if self.somethingchanged:
                    c = ReplaceCount(self.reg_count_map, self.gui)

                    if c.exec_():
                        pass
                    else:
                        self.boss.show_current_diff()
                        refresh = False
                        self.dlg.widg.refreshbutton.setStyleSheet("color:red;")
                        for f in changedfiles:
                            del self.cache[f]
                        self.boss.update_editors_from_container()

                else:
                    info_dialog(self.gui, _('Aucun remplacement'),
                    '<p>{0}</p>'.format(_('Les regex sélectionnées n\'ont pas modifié les fichiers.')), show=True)
                if refresh:
                    self.dlg.widg.refresh()
            if len(self.steps):
                zap = RemoteCtrl(self, stepsearches, stepcounts)
                zap.show()
            self.boss.commit_all_editors_to_container()

    def _quit(self, diag=True):
        for w in self.gui.children() :
            if w.objectName() == 'typex-remote' :
                w.close()
        com = regex.compile(r'<\!\s*--(?:(?!--\s*>).)*--\s*>', regex.DOTALL | regex.MULTILINE)
        off = regex.compile(r'\[\[([^\]]*)\]\](?=(?:(?!(?:<!|--\s*>)).)*?--\s*>)', regex.DOTALL | regex.MULTILINE)
        for file in self.tc:
            data = self.cache[file] if self.cache.get(file, False) else self.current_container.raw_data(file, decode=True, normalize_to_nfc=False)
            for m in com.finditer(data):
                newm = None
                if off.search(m.group()):
                    newm = regex.sub(r'\[\[([^\]]*)\]\](?=(?:(?!(?:<!|--\s*>)).)*?--\s*>)', r'<\1>', m.group(), flags=regex.DOTALL | regex.MULTILINE)
                    newm = regex.sub(r'<!-- Commentaire modifié par Typex :', r'<!--', newm)
                if newm:
                    data = data.replace(m.group(), newm)
                    self.current_container.open(file, 'wb').write(data)
                    self.boss.update_editors_from_container()

        if diag :
            q = question_dialog(self.gui, _('Fermeture de {}').format(PLUGIN_NAME), _('Voulez-vous valider les modifications ?'),
                        skip_dialog_name='typex_quit')
            if not q :
                self.boss.revert_requested(self.initial_container)

    def do_count(self, j):
        self.count_map = OrderedDict()
        self.key_file_map = {}
        regdict = self.searches[j]

        def f(x):return regex.search(r'(\[#?\w*?\])', x).group(0)

        keys = [f(reg['name']) for reg in regdict[1:]]
        # Threading #

        keys_queue = Queue()
        result_queue = Queue()
        workers = []
        for i in range(len(keys)):
            worker = CountWorker(self.sf, keys_queue, result_queue)
            worker.daemon = True
            worker.start()
            workers.append(worker)
            keys_queue.put(keys[i])
        keys_queue.join()
        for i in range(len(keys)):
            name, total, count_time, key, filelist = result_queue.get()
            self.count_map[name] = (total, count_time)
            self.key_file_map[key] = filelist
            result_queue.task_done()
        for i in range(len(keys)):
            keys_queue.put(None)
        for w in workers:
            w.join()

        # end threading#

        # no threading

        #=======================================================================
        # from threading import RLock
        # lock = RLock()
        # for key in keys:
        #     name, total, count_time, __, filelist = self.sf.regexcounter(key, lock)
        #     self.count_map[name] = (total, count_time)
        #     self.key_file_map[key] = filelist
        #=======================================================================

        #
        self.global_count.update(self.count_map)

    def regexterminator(self, keys, title=None):
        self.reg_count_map.clear()
        changedfiles = set()
        self.somethingchanged = False
        if title:
            self.boss.add_savepoint(_('Avant {}').format(title))

        for key in keys :
            kmode, kflags, kfind, file_list, repl, kname = self.sf.qualify_key(key)

            def function_replace(match):
                if kmode == 'function':
                    f = self.fdict[repl]
                    f.init_env()
                    if f(match) != match.group() :
                        self.key_count += 1
                    return f(match)
                else :
                    if match.expand(repl) != match.group() :
                        self.key_count += 1
                    return match.expand(repl)

            total = 0
            reg = ur'{}'.format(kfind)
            pat = regex.compile(reg, flags=kflags)
            for file in file_list :
                self.key_count = 0
                data = self.cache[file] if self.cache.get(file, False) else self.current_container.raw_data(file, decode=True, normalize_to_nfc=True)
                if file in self.tb.aliens :
                    # log(file)
                    olcache = list()
                    #===========================================================
                    # for i, (pstart, pend) in enumerate(self.tb.aliens[file]) :
                    #     #log('pstart, pend', pstart, pend)
                    #     ppat = regex.compile(r'.*', flags=self.flags | regex.DOTALL)
                    #     p = ppat.search(data, pos=pstart, endpos=pend)
                    #     olcache.append(p.group())
                    #     #log('p.group()', p.group())
                    #     olrep = '¥' * (len(p.group()) - 1) + str(i)
                    #     data = data.replace(p.group(), olrep)
                    #===========================================================

                    #####################################

                    ol_reg = regex.compile(r'<([a-z]+)[^<]*lang="([a-zA-Z-]{2,5})"[^>]*>', self.flags)
                    i = 0

                    for ol in ol_reg.finditer(data):
                        if ol.group(2)[0:2].lower() != CALIBRE_LOCALE :
                            closing = regex.search(r'<\/{}'.format(ol.group(1)), data, self.flags, pos=ol.end())
                            p = regex.search(r'.*', data, self.flags, pos=ol.end(), endpos=closing.start())
                            if p :
                                log(p.group().decode('latin-1'))
                                orig = ol.group() + p.group() + closing.group()
                                olcache.append(orig)
                                olrep = '¥' * (len(orig) - 1) + str(i)
                                data = data.replace(orig, olrep)
                                log(olrep)
                                i += 1
                if isinstance(repl, fr.Function):
                    repl.context_name = file
                data = pat.sub(function_replace, data)

                if self.key_count > 0:
                    changedfiles.add(file)
                    self.somethingchanged = True
                total += self.key_count
                if file in self.tb.aliens :
                    data = regex.sub(r'¥+(\d)?', lambda m: olcache[int(m.group(1))], data)
                    olcache = []
                self.cache[file] = data
            for f in changedfiles:
                data = self.cache[f]
                self.current_container.open(f, 'wb').write(data)
            self.reg_count_map[kname] = total

        return changedfiles

    def prime_polish(self):

        from calibre.ebooks.oeb.polish.css import remove_unused_css
        c = self.current_container
        prime_flags = self.flags | regex.IGNORECASE
        xml_types = {guess_type('a.ncx'), guess_type('a.xml'), guess_type('a.svg')}
        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        for name, mt in c.mime_map.iteritems():
            if mt in OEB_DOCS:
                data = c.raw_data(name, decode=True, normalize_to_nfc=True)
                # replace line feeds by spaces
                linefeed_flag = prime_flags | regex.DOTALL
                data = regex.sub(r'((?:</(?:h\d|div)>\s*)?(?:<(?:div|section)[^>]*>\s*)+|</(?:p|div|h\d|blockquote)>\s*)(<(?:a|span|br)[^/>]*/>|<(a\b|span)[^>]*></\3>)',
                    r'\2\1', data, flags=prime_flags)
                __, n = regex.subn(r'''(<(p\b|div|h\d|blockquote)[^>]*>(?:[^\n](?!<(?:p\b|div|h\d|blockquote)|/\2))*\n(?!<(?:p\b|div|h\d|blockquote)|/\2)(?:.(?!<(?:p\b|div|h\d|blockquote)|/\2))*[^\x20]\x20*</\2)''',
                                      "", data, flags=linefeed_flag)
                if n > 0 :
                    data = regex.sub(r"\n\x20*", '\x20', data, flags=prime_flags)
                pdata = pretty_html(c, c.parsed(name), data.decode('utf-8'))
                # remove dir="ltr"if no dir="rtl"
                if 'dir="rtl"' in name:
                    pdata = regex.sub(r'\x20dir="rtl"', "", pdata, flags=prime_flags)
                pdata = regex.sub(r'<span\s+class="[\w-]+">\xa0</span>', '\xa0', pdata, flags=prime_flags)
                # pdata = regex.sub(ur'\u00ad|\u200b', '', pdata, flags=prime_flags)  # \u00ad \xc2\xad|\u200b
                pdata = pdata.replace('\u00ad', '').replace('\u200b', '')
                # pdata = regex.sub(ur'\u202F', '\xa0', pdata, flags=prime_flags)  # \u202F \xE2\x80\xAF
                pdata = pdata.replace('\u202F', '\xa0')
                for sp in ('\u2002', '\u2003', '\u2005', '\u2009'):
                    pdata = pdata.replace(sp, '\x20')
                # replace tags in comments
                com = regex.compile(r'<\!\s*--(?:(?!--\s*>).)*--\s*>', regex.DOTALL | regex.MULTILINE)
                on = regex.compile(r'<([^!][^>]*)>(?=(?:(?!(?:<!|--\s*>)).)*?--\s*>)', regex.DOTALL | regex.MULTILINE)
                for m in com.finditer(pdata):
                    newm = None
                    if on.search(m.group()):
                        newm = regex.sub(r'<([^!][^>]*)>(?=(?:(?!(?:<!|--\s*>)).)*?--\s*>)', r'[[\1]]', m.group(), flags=regex.DOTALL | regex.MULTILINE)
                        newm = regex.sub(r'(<!--)', _(r'\1 Commentaire modifié par Typex :'), newm)
                    if newm :
                        pdata = pdata.replace(m.group(), newm)
                        self.tc.append(name)
                c.open(name, 'wb').write(pdata)
                # self.cache[name] = pdata
            elif mt in OEB_STYLES:
                data = c.raw_data(name, decode=True, normalize_to_nfc=True)
                data = regex.sub(r'\n\s*line-height\s*:\s*normal\s?;?[^\n]*', '', data)
                c.open(name, 'wb').write(data)
                c.parsed(name)
            elif name == c.opf_name:
                l = [elem for elem in c.opf_xpath('//opf:metadata//dc:contributor') if PLUGIN_NAME in elem.text]
                # no dc:contributor with Typex
                if not len(l):
                    meta = c.opf_get_or_create('metadata')
                    contrib = etree.Element('{http://purl.org/dc/elements/1.1/}contributor', nsmap={'dc':'http://purl.org/dc/elements/1.1/'})
                    contrib.text = PLUGIN_NAME + ' V ' + PLUGIN_VERSION
                    meta.append(contrib)
                # Typex was used, test the version
                else :
                    typex_ver = l[0].text.split()[-1]
                    if typex_ver != PLUGIN_VERSION :
                        l[0].text = PLUGIN_NAME + ' V ' + PLUGIN_VERSION
                        msg1 = _("Typex a déjà été utilisé sur ce livre avec la version {0}.\nVotre version actuelle est {1}.")\
                                    .format(typex_ver, PLUGIN_VERSION)
                        msg2 = _('\n\nVoulez-vous continuer ?')
                        QApplication.setOverrideCursor(QCursor(Qt.ArrowCursor))
                        qd = question_dialog(self.gui, _('Content de vous revoir !'), msg1 + msg2)
                        if not qd:
                            QApplication.restoreOverrideCursor()
                            self.boss.revert_requested(self.initial_container)
                            return False
                        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
                root = c.parsed(name)
                pretty_opf(root)
                pretty_xml_tree(root)
            elif mt in xml_types:
                pretty_xml_tree(c.parsed(name))
            c.dirty(name)
        remove_unused_css(c)
        self.boss.apply_container_update_to_gui()
        self.boss.commit_all_editors_to_container()
        self.boss.set_modified()
        QApplication.restoreOverrideCursor()
        self.cache = {}
        return True

    def show_file(self, name):
        with zipfile.ZipFile(PLUGIN_PATH) as zf:
            path = zf.extract('resources/{}'.format(name), self.boss.tdir)
        # path = os.path.join(config_dir, 'plugins', name)
            # path = zf.open('resources/{}'.format(name))
            url = 'file:///' + path
            open_url(QUrl(url))

    def import_func(self):
        try :
            from calibre.gui2.tweak_book import tprefs
            if not len([w for w in self.gui.children() if w.objectName() in ('typex-view', 'typex-remote')]):
                self.get_json_file()
            esrf = JSONConfig('editor-search-replace-functions.json')
            json_func = JSONConfig('plugins/typex_user_functions.json')
            esrf.update(json_func)
            esrf.commit()
            searches = [d for d in self.obj['searches'] if d['mode'] == 'function']
            tprefs['saved_searches'] += searches
            info_dialog(self.gui, _('Typex'), _('L\'importation a réussi.'), show=True)
        except :
            raise

    def show_func(self):
        from calibre_plugins.typex.dialogs import DisplayFunction
        func_map = {}
        if not len([w for w in self.gui.children() if w.objectName() in ('typex-view', 'typex-remote')]):
            self.get_json_file()
        json_func = JSONConfig('plugins/typex_user_functions.json')
        for k, v in json_func.items() :
            try:
                if k == "utils":
                    func_map[k] = (v, '')
                    continue
                func_map[k] = (v, [d['find'] for d in self.obj['searches'] if d['replace'] == k][0])

            # function unused in regex json file
            except IndexError:
                continue
        df = DisplayFunction()
        df.init_display(func_map)
        df.exec_()

    def about(self):
        from calibre_plugins.typex import PLUGIN_VERSION
        msg = _(''' Typex est un correcteur typographique d'ebooks 
        basé sur des regex et fonctions-regex.
        Il a été developpé à 4 mains par l'équipe EbookMakers.
        Enjoy !''')
        QMessageBox.about(None, _("A propos de Typex version {}").format(PLUGIN_VERSION), msg)

