#!/usr/bin/env python

__license__   = 'GPL v3'
__copyright__ = '2022, Thiago Oliveira <thiago.eec@gmail.com>'
__docformat__ = 'restructuredtext en'

# Load translation files (.mo) on the folder 'translations'
load_translations()

# Standard libraries
import os
import re
import time
import threading
import json
import socket
import zipfile
from contextlib import contextmanager
from urllib.request import urlopen
from datetime import datetime

# PyQt libraries
try:
    from qt.core import (QDialog, pyqtSignal, Qt, QVBoxLayout, QLabel, QProgressBar, QSize, QPixmap,
                         QIcon, QDialogButtonBox, QHBoxLayout, QApplication, QPushButton)
except ImportError:
    from PyQt5.Qt import (QDialog, pyqtSignal, Qt, QVBoxLayout, QLabel, QProgressBar, QSize, QPixmap,
                          QIcon, QDialogButtonBox, QHBoxLayout, QApplication, QPushButton)

# Calibre libraries
from calibre.constants import iswindows, islinux, isosx, numeric_version
from calibre.utils.config import config_dir, JSONConfig
from calibre.utils.logging import GUILog
from calibre_plugins.Check_Books.__init__ import PLUGIN_NAME
from calibre.gui2.ui import get_gui

# This is where all preferences for this plugin will be stored.
prefs = JSONConfig('plugins/Check_Books')

# Signals when the user cancels running instances of ACE/EPUBCheck
stop_event = threading.Event()

# Signals ACE/EPUBCheck errors
error_event = threading.Event()

# Plugin log
plugin_log = GUILog()

# Auxiliary list to hold the log before processing
# [log_type, log_msg, status]
temp_log = []


def get_icon(icon_name):
    # Check to see whether the icon exists as a Calibre resource
    # This will enable skinning if the user stores icons within a folder like:
    # config_dir\resources\images\Plugin Name\

    # Embedded themed icons
    themed_icons = ['add_column.png', 'config.png', 'down.png', 'icon.png', 'right.png']

    # Check for calibre theme
    try:
        is_dark_theme = QApplication.instance().is_dark_theme
    except:
        is_dark_theme = False

    # Change icon_name based on dark/light theme
    if icon_name.replace('images/', '') in themed_icons:
        if is_dark_theme:
            themed_icon_name = icon_name.replace('.png', '-for-dark-theme.png')
        else:
            themed_icon_name = icon_name.replace('.png', '-for-light-theme.png')
    else:
        themed_icon_name = icon_name

    if numeric_version < (5, 99, 0):
        # First, look for the icon on the 'config_dir\resources\images\Plugin Name\' folder
        icon_path = os.path.join(config_dir, 'resources', 'images', PLUGIN_NAME, themed_icon_name.replace('images/', ''))
        if os.path.exists(icon_path):
            pixmap = QPixmap()
            pixmap.load(icon_path)
            return QIcon(pixmap)
        else:
            # Then, look for it on the 'config_dir\resources\images\' folder
            if not themed_icon_name.startswith('images/'):  # Image does not come with the zip file
                icon_path = os.path.join(config_dir, 'resources', 'images', themed_icon_name)
                if os.path.exists(icon_path):
                    pixmap = QPixmap()
                    pixmap.load(icon_path)
                    return QIcon(pixmap)
                # Use the general icon, in case there is no themed version of it
                else:
                    icon = QIcon(I(themed_icon_name))
                    return icon
    else:
        # First, look for the themed icon (Qt resource files)
        tc = 'dark' if QApplication.instance().is_dark_theme else 'light'
        sq, ext = os.path.splitext(themed_icon_name)
        sq = f'{sq}-for-{tc}-theme{ext}'
        icon = QIcon.ic(PLUGIN_NAME + '/' + sq.replace('images/', ''))
        if icon.isNull():
            # Then, look for the regular icon
            icon = QIcon.ic(PLUGIN_NAME + '/' + themed_icon_name.replace('images/', ''))
            if icon.isNull():
                # Then, look for it on general icons (Qt resource files)
                if not themed_icon_name.startswith('images/'):  # Image does not come with the zip file
                    return QIcon.ic(themed_icon_name)
            else:
                return icon
        else:
            return icon

    # As we did not find an icon elsewhere, look within our zip resources
    return get_icons(themed_icon_name)


# Simple wrapper for commands
def wrapper(self, *args, **kwargs):
    import subprocess
    import psutil
    startupinfo = None

    # We can't use PIPE to run multiple threads (if it outputs too much information), because of its size limit.
    # When the limit is reached, all the processes halt.
    # This only happens when polling the processes for too long, without using 'process.communicate()'.
    # Using 'process.communicate()' drains the PIPE data, so it can continue to receive more info.
    # To keep using PIPE, we are running ACE and EPUBCheck in silent/quiet mode.
    # The 'npm outdated' command only outputs to PIPE. Using stdout didn't work.

    stdout = stderr = subprocess.PIPE

    if islinux:
        shell = False
    else:
        shell = True

    # Stop the Windows console popping up every time the program is run
    if iswindows:
        startupinfo = subprocess.STARTUPINFO()
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        startupinfo.wShowWindow = subprocess.SW_HIDE

    if kwargs['origin'] in ('run_epubcheck', 'java_architecture'):
        # We need to redirect stderr to a file, because when big errors come up, like stackoverflow
        # errors, there is too much output and PIPE gets full, so the execution halts
        stderr_file = os.path.join(config_dir, 'plugins', 'Check_Books', 'stderr_file.txt')
        with open(stderr_file, 'w') as stderr:
            process = subprocess.Popen(list(args), stdout=stdout, stderr=stderr, startupinfo=startupinfo, shell=False)
    else:
        process = subprocess.Popen(list(args), stdout=stdout, stderr=stderr, startupinfo=startupinfo, shell=shell)

    # Get process ID
    pid = process.pid
    p = psutil.Process(pid)

    # Check if the process is still running
    while process.poll() is None:
        # Check if the user canceled the job
        if stop_event.is_set():
            try:
                # Terminate child processes (e.g.: node.js for ACE)
                for child in p.children(recursive=True):
                    child.terminate()
                # Terminate parent process
                p.terminate()
            except psutil.NoSuchProcess:
                pass
            ret, return_code = (None, None), None
            return ret, return_code
        time.sleep(1)

    ret = process.communicate()
    return_code = process.returncode
    return ret, return_code


# Get JVM architecture (https://stackoverflow.com/questions/2062020)
def get_arch(java_path):
    self = get_gui()
    arch = '64'
    arch_info = None
    args = [java_path, '-XshowSettings:properties', '-version']
    try:
        result = wrapper(self, *args, origin='java_architecture')[0]
        arch_pattern = re.compile(r'sun.arch.data.model = (\d+)')
        arch_info = arch_pattern.search(result[1].decode('utf-8'))
    except:
        return None

    if arch_info:
        if len(arch_info.groups()) == 1:
            arch = arch_info.group(1)
            msg = _('Java architecture detected: ') + '\n'
            temp_log.append(('info', msg))
    else:
        msg = '<span style="color:red">' + _('Java architecture not detected!') + '</span>' + '\n'
        temp_log.append(('info', msg))
    return arch


# Get epubcheck.jar version number
def get_epc_version(epc_path):
    version = ''

    # Unpack EPUBCheck java files for the first run of the plugin
    if not os.path.exists(epc_path):
        check_books_dir = os.path.join(config_dir, 'plugins', 'Check_Books')
        plugin_zip = os.path.join(config_dir, 'plugins', PLUGIN_NAME + '.zip')
        with zipfile.ZipFile(plugin_zip, 'r') as zf:
            # Get a list of all archived file names from the zip
            file_list = zf.namelist()
            # Iterate over the file names
            for file_name in file_list:
                if file_name.endswith('.jar'):
                    # Extract the file from zip
                    try:
                        zf.extract(file_name, check_books_dir)
                    except FileExistsError:
                        pass

    # Make sure that epubcheck.jar actually exists
    if os.path.exists(epc_path):
        # Read .jar file as zip file
        archive = zipfile.ZipFile(epc_path)
        # Make sure that pom.xml exists
        if 'META-INF/maven/org.w3c/epubcheck/pom.xml' in archive.namelist():
            pom_data = archive.read('META-INF/maven/org.w3c/epubcheck/pom.xml')
            archive.close()
            # Parse pom.xml as ElementTree
            from xml.etree import ElementTree as ET
            root = ET.fromstring(pom_data)
            tag = root.find('*//{http://maven.apache.org/POM/4.0.0}tag')
            # Look for <tag>
            if tag is not None:
                version = tag.text
            else:
                # Look for <version>
                project_version = root.find('{http://maven.apache.org/POM/4.0.0}version')
                if project_version is not None:
                    version = 'v' + project_version.text
        else:
            msg = '<span style="color:red">' + _('pom.xml not found!') + '</span>' + '\n'
            temp_log.append(('info', msg))
    else:
        msg = '<span style="color:red">' + _('epubcheck.jar not found!') + '</span>' + '\n'
        temp_log.append(('info', msg))

    return version


# Get latest EPUBCheck version
def latest_epc_version(github_url):
    latest_version = ''
    browser_download_url = ''

    if is_connected():
        # Check for GitHub updates
        response = urlopen(github_url).read().decode('utf-8')
        parsed_json = json.loads(response)
        latest_version = parsed_json[0]['tag_name']
        browser_download_url = parsed_json[0]['assets'][0]['browser_download_url']

    return latest_version, browser_download_url


def string_to_date(date_string):
    return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f')


def is_connected():
    try:
        sock = socket.create_connection(('8.8.8.8', 53), 1)
        sock.close()
        return True
    except:
        pass


@contextmanager
def make_temp_directory():
    import tempfile
    import shutil
    temp_dir = tempfile.mkdtemp()
    yield temp_dir
    shutil.rmtree(temp_dir)


def get_title_authors_text(db, book_id):
    mi = db.get_metadata(book_id)
    authors = mi.authors
    title = mi.title
    from calibre.ebooks.metadata import authors_to_string
    return '%s / %s' % (title, authors_to_string(authors))


def show_invalid_rows(ids, marked_text=_('Fail')):
    gui = get_gui()
    marked_ids = dict.fromkeys(ids, marked_text)
    gui.current_db.set_marked_ids(marked_ids)
    if len(ids) > 0:
        gui.search.set_search_string('marked:%s' % marked_text)


def check_custom_columns(self, custom_column_name):
    custom_columns = self.gui.library_view.model().custom_columns
    for key, column in custom_columns.items():
        lookup_name = '#' + column['label']
        if lookup_name == custom_column_name:
            return True
    return False


# Dialog adapted from 'calibre.gui2.dialogs.progress.py'
class ProgressDialog(QDialog):

    canceled_signal = pyqtSignal()

    def __init__(self, title, list_widget, min=0, max=99, parent=None, cancelable=True, window_icon=None, icon=None):
        QDialog.__init__(self, parent)
        self.main = main = QVBoxLayout(self)
        self.h = h = QHBoxLayout()
        main.addLayout(h)
        self.icon = i = QLabel(self)
        i.setPixmap(icon.pixmap(60))
        h.addWidget(i, alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter)
        self.l = l = QVBoxLayout()
        h.addLayout(l)
        self.setWindowIcon(window_icon)

        self.first_paint = True

        self.title_label = t = QLabel(title)
        self.setWindowTitle(title)
        t.setStyleSheet('QLabel { font-weight: bold }'), t.setAlignment(Qt.AlignmentFlag.AlignCenter),\
            t.setTextFormat(Qt.TextFormat.PlainText)
        l.addWidget(t)

        self.bar = b = QProgressBar(self)
        b.setMinimum(min), b.setMaximum(max), b.setValue(min)
        l.addWidget(b)

        main.addSpacing(10)

        self.details = list_widget
        self.details_button = QPushButton(self)
        self.details_button.setText(_('&Details'))
        self.details_button.setFlat(True)
        self.details_button.setStyleSheet('text-align: left; border: none; outline:none')
        icon = get_icon('images/right.png')
        self.details_button.setIcon(icon)
        self.details_button.clicked.connect(self.on_details)
        self.main.addWidget(self.details_button)

        self.details.setVisible(False)
        self.main.addWidget(self.details)
        self.main.addStretch()

        self.button_box = bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Cancel, self)
        bb.rejected.connect(self._canceled)
        main.addWidget(bb)

        self.setWindowModality(Qt.WindowModality.ApplicationModal)
        self.canceled = False

        if not cancelable:
            bb.setVisible(False)
        self.cancelable = cancelable
        self.resize(QSize(700, 100))

    def on_details(self):
        QApplication.processEvents()
        self.details.setVisible(not self.details.isVisible())
        icon = get_icon('images/down.png' if self.details.isVisible() else 'images/right.png')
        self.details_button.setIcon(icon)
        # QTimer.singleShot(0, self.resize_me)
        self.resize_me()

    def resize_me(self):
        QApplication.processEvents()
        self.resize(QSize(700, 100))

    def set_title(self, title=''):
        self.title = title

    def set_value(self, val):
        self.value = val
        self.resize_me()

    @property
    def value(self):
        return self.bar.value()

    @value.setter
    def value(self, val):
        self.bar.setValue(val)

    def set_min(self, min):
        self.min = min

    def set_max(self, max):
        self.max = max

    @property
    def max(self):
        return self.bar.maximum()

    @max.setter
    def max(self, val):
        self.bar.setMaximum(val)

    @property
    def min(self):
        return self.bar.minimum()

    @min.setter
    def min(self, val):
        self.bar.setMinimum(val)

    @property
    def title(self):
        return self.title_label.text()

    @title.setter
    def title(self, val):
        self.title_label.setText(str(val or ''))

    def _canceled(self, *args):
        self.canceled = True
        self.button_box.setDisabled(True)
        icon = get_icon('images/right.png')
        self.details_button.setIcon(icon)
        self.details.setVisible(False)
        self.resize_me()
        self.title = _('Aborting...')
        self.canceled_signal.emit()

    def reject(self):
        if not self.cancelable:
            return
        QDialog.reject(self)

    def closeEvent(self, event):
        event.ignore()
        self._canceled()

    def keyPressEvent(self, ev):
        if ev.key() == Qt.Key.Key_Escape:
            if self.cancelable:
                self._canceled()
        else:
            QDialog.keyPressEvent(self, ev)

    def paintEvent(self, event):
        if self.first_paint:
            if 'main_dialog_geometry' in prefs:
                self.restoreGeometry(prefs['main_dialog_geometry'])
            self.first_paint = False
        return QDialog.paintEvent(self, event)
