__license__   = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'

import logging
import os
import sys
import time
import traceback

from qt.core import QCoreApplication, QDialog, QIcon, QKeySequence, QPainter, QScrollBar, QSlider, QSpinBox, Qt, QThread

from calibre import __appname__, as_unicode, isbsd, islinux, setup_cli_handlers
from calibre.ebooks.lrf.lrfparser import LRFDocument
from calibre.gui2 import Application, choose_files, error_dialog, gprefs
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
from calibre.gui2.lrf_renderer.config_ui import Ui_ViewerConfig
from calibre.gui2.lrf_renderer.document import Document
from calibre.gui2.lrf_renderer.main_ui import Ui_MainWindow
from calibre.gui2.main_window import MainWindow
from calibre.gui2.search_box import SearchBox2


class RenderWorker(QThread):

    def __init__(self, parent, lrf_stream, logger, opts):
        QThread.__init__(self, parent)
        self.stream, self.logger, self.opts = lrf_stream, logger, opts
        self.aborted = False
        self.lrf = None
        self.document = None
        self.exception = None

    def run(self):
        try:
            self.lrf = LRFDocument(self.stream)
            self.lrf.parse()
            self.stream.close()
            self.stream = None
            if self.aborted:
                self.lrf = None
        except Exception as err:
            self.lrf, self.stream = None, None
            self.exception = err
            self.formatted_traceback = traceback.format_exc()

    def abort(self):
        if self.lrf is not None:
            self.aborted = True
            self.lrf.keep_parsing = False


class Config(QDialog, Ui_ViewerConfig):

    def __init__(self, parent, opts):
        QDialog.__init__(self, parent)
        Ui_ViewerConfig.__init__(self)
        self.setupUi(self)
        self.white_background.setChecked(opts.white_background)
        self.hyphenate.setChecked(opts.hyphenate)


class Main(MainWindow, Ui_MainWindow):

    def create_document(self):
        self.document = Document(self.logger, self.opts)
        self.document.chapter_rendered.connect(self.chapter_rendered)
        self.document.page_changed.connect(self.page_changed)

    def __init__(self, logger, opts, parent=None):
        MainWindow.__init__(self, opts, parent)
        Ui_MainWindow.__init__(self)
        self.setupUi(self)
        self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
        self.setWindowTitle(__appname__ + _(' - LRF viewer'))

        self.logger = logger
        self.opts = opts
        self.create_document()
        self.spin_box_action = self.spin_box = QSpinBox()
        self.tool_bar.addWidget(self.spin_box)
        self.tool_bar.addSeparator()
        self.slider_action = self.slider = QSlider(Qt.Orientation.Horizontal)
        self.tool_bar.addWidget(self.slider)
        self.tool_bar.addSeparator()
        self.search = SearchBox2(self)
        self.search.initialize('lrf_viewer_search_history')
        self.search_action = self.tool_bar.addWidget(self.search)
        self.search.search.connect(self.find)

        self.action_next_page.setShortcuts([QKeySequence.StandardKey.MoveToNextPage, QKeySequence(Qt.Key.Key_Space)])
        self.action_previous_page.setShortcuts([QKeySequence.StandardKey.MoveToPreviousPage, QKeySequence(Qt.Key.Key_Backspace)])
        self.action_next_match.setShortcuts(QKeySequence.StandardKey.FindNext)
        self.addAction(self.action_next_match)
        self.action_next_page.triggered[(bool)].connect(self.next)
        self.action_previous_page.triggered[(bool)].connect(self.previous)
        self.action_back.triggered[(bool)].connect(self.back)
        self.action_forward.triggered[(bool)].connect(self.forward)
        self.action_next_match.triggered[(bool)].connect(self.next_match)
        self.action_open_ebook.triggered[(bool)].connect(self.open_ebook)
        self.action_configure.triggered[(bool)].connect(self.configure)
        self.spin_box.valueChanged[(int)].connect(self.go_to_page)
        self.slider.valueChanged[(int)].connect(self.go_to_page)

        self.graphics_view.setRenderHint(QPainter.RenderHint.Antialiasing, True)
        self.graphics_view.setRenderHint(QPainter.RenderHint.TextAntialiasing, True)
        self.graphics_view.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)

        self.closed = False

    def configure(self, triggered):
        opts = self.opts
        d = Config(self, opts)
        d.exec()
        if d.result() == QDialog.DialogCode.Accepted:
            gprefs['lrf_viewer_white_background'] = opts.white_background = bool(d.white_background.isChecked())
            gprefs['lrf_viewer_hyphenate'] = opts.hyphenate = bool(d.hyphenate.isChecked())

    def set_ebook(self, stream):
        self.progress_bar.setMinimum(0)
        self.progress_bar.setMaximum(0)
        self.progress_bar.setValue(0)
        self.create_document()

        if stream is not None:
            self.file_name = os.path.basename(stream.name) if hasattr(stream, 'name') else ''
            self.progress_label.setText('Parsing '+ self.file_name)
            self.renderer = RenderWorker(self, stream, self.logger, self.opts)
            self.renderer.finished.connect(self.parsed, type=Qt.ConnectionType.QueuedConnection)
            self.search.clear()
            self.last_search = None
        else:
            self.stack.setCurrentIndex(0)
            self.renderer = None

    def open_ebook(self, triggered):
        files = choose_files(self, 'open ebook dialog', 'Choose ebook',
                             [('Ebooks', ['lrf'])], all_files=False,
                             select_only_single_file=True)
        if files:
            file = files[0]
            self.set_ebook(open(file, 'rb'))
            self.render()

    def page_changed(self, num):
        self.slider.setValue(num)
        self.spin_box.setValue(num)

    def render(self):
        if self.renderer is not None:
            self.stack.setCurrentIndex(1)
            self.renderer.start()

    def find(self, search):
        self.last_search = search
        try:
            self.document.search(search)
        except StopIteration:
            error_dialog(self, _('No matches found'), _('<b>No matches</b> for the search phrase <i>%s</i> were found.')%(search,)).exec()
        self.search.search_done(True)

    def parsed(self):
        if not self.renderer.aborted and self.renderer.lrf is not None:
            width, height = self.renderer.lrf.device_info.width, \
                                            self.renderer.lrf.device_info.height
            hdelta = self.tool_bar.height()+3

            s = QScrollBar(self)
            scrollbar_adjust = min(s.width(), s.height())
            self.graphics_view.resize_for(width+scrollbar_adjust, height+scrollbar_adjust)

            screen_height = self.screen().availableSize().height() - 25
            height = min(screen_height, height+hdelta+scrollbar_adjust)
            self.resize(width+scrollbar_adjust, height)
            self.setWindowTitle(self.renderer.lrf.metadata.title + ' - ' + __appname__)
            self.document_title = self.renderer.lrf.metadata.title
            if self.opts.profile:
                import cProfile
                lrf = self.renderer.lrf
                cProfile.runctx('self.document.render(lrf)', globals(), locals(), lrf.metadata.title+'.stats')
                print('Stats written to', self.renderer.lrf.metadata.title+'.stats')
            else:
                start = time.time()
                self.document.render(self.renderer.lrf)
                print('Layout time:', time.time()-start, 'seconds')
            self.renderer.lrf = None

            self.graphics_view.setScene(self.document)
            self.graphics_view.show()
            self.spin_box.setRange(1, self.document.num_of_pages)
            self.slider.setRange(1, self.document.num_of_pages)
            self.spin_box.setSuffix(f' of {self.document.num_of_pages}')
            self.spin_box.updateGeometry()
            self.stack.setCurrentIndex(0)
            self.graphics_view.setFocus(Qt.FocusReason.OtherFocusReason)
        elif self.renderer.exception is not None:
            exception = self.renderer.exception
            print('Error rendering document', file=sys.stderr)
            print(exception, file=sys.stderr)
            print(self.renderer.formatted_traceback, file=sys.stderr)
            msg = f'<p><b>{exception.__class__.__name__}</b>: ' + as_unicode(exception) + '</p>'
            msg += '<p>Failed to render document</p>'
            msg += '<p>Detailed <b>traceback</b>:<pre>'
            msg += self.renderer.formatted_traceback + '</pre>'
            d = ConversionErrorDialog(self, 'Error while rendering file', msg)
            d.exec()

    def chapter_rendered(self, num):
        if num > 0:
            self.progress_bar.setMinimum(0)
            self.progress_bar.setMaximum(num)
            self.progress_bar.setValue(0)
            self.progress_label.setText('Laying out '+ self.document_title)
        else:
            self.progress_bar.setValue(self.progress_bar.value()+1)
        QCoreApplication.processEvents()

    def next(self, triggered):
        self.document.next()

    def next_match(self, triggered):
        try:
            self.document.next_match()
        except StopIteration:
            pass

    def previous(self, triggered):
        self.document.previous()

    def go_to_page(self, num):
        self.document.show_page(num)

    def forward(self, triggered):
        self.document.forward()

    def back(self, triggered):
        self.document.back()

    def wheelEvent(self, ev):
        d = ev.angleDelta().y()
        if d > 0:
            self.document.previous()
        elif d < 0:
            self.document.next()

    def closeEvent(self, event):
        if self.renderer is not None and self.renderer.isRunning():
            self.renderer.abort()
            self.renderer.wait()
        event.accept()


def file_renderer(stream, opts, parent=None, logger=None):
    if logger is None:
        level = logging.DEBUG if opts.verbose else logging.INFO
        logger = logging.getLogger('lrfviewer')
        setup_cli_handlers(logger, level)
    if islinux or isbsd:
        try:  # Set lrfviewer as the default for LRF files for this user
            from subprocess import call
            call('xdg-mime default calibre-lrfviewer.desktop application/lrf', shell=True)
        except Exception:
            pass
    m = Main(logger, opts, parent=parent)
    m.set_ebook(stream)
    return m


def option_parser():
    from calibre.gui2.main_window import option_parser
    parser = option_parser(_('''\
%prog [options] book.lrf

Read the LRF e-book book.lrf
'''))
    parser.add_option('--verbose', default=False, action='store_true', dest='verbose',
                      help=_('Print more information about the rendering process'))
    parser.add_option('--visual-debug', help=_('Turn on visual aids to debugging the rendering engine'),
                      default=False, action='store_true', dest='visual_debug')
    parser.add_option('--disable-hyphenation', dest='hyphenate', default=True, action='store_false',
                      help=_('Disable hyphenation. Should significantly speed up rendering.'))
    parser.add_option('--white-background', dest='white_background', default=False, action='store_true',
                      help=_('By default the background is off white as I find this easier on the eyes. Use this option to make the background pure white.'))
    parser.add_option('--profile', dest='profile', default=False, action='store_true',
                      help=_('Profile the LRF renderer'))
    return parser


def normalize_settings(parser, opts):
    saved_opts = opts
    dh = gprefs.get('lrf_viewer_hyphenate', None)
    if dh is not None:
        opts.hyphenate = bool(dh)
    wb = gprefs.get('lrf_viewer_white_background', None)
    if wb is not None:
        opts.white_background = bool(wb)
    return saved_opts


def main(args=sys.argv, logger=None):
    parser = option_parser()
    opts, args = parser.parse_args(args)
    if hasattr(opts, 'help'):
        parser.print_help()
        return 1
    pid = os.fork() if (islinux or isbsd) else -1
    if pid <= 0:
        override = 'calibre-lrfviewer' if islinux else None
        app = Application(args, override_program_name=override)
        app.setWindowIcon(QIcon.ic('viewer.png'))
        opts = normalize_settings(parser, opts)
        stream = open(args[1], 'rb') if len(args) > 1 else None
        main = file_renderer(stream, opts, logger=logger)
        main.set_exception_handler()
        main.show()
        main.render()
        main.raise_and_focus()
        return app.exec()
    return 0


if __name__ == '__main__':
    sys.exit(main())
