import os
import io
import logging
import traceback
import warnings
import pickle
from collections import OrderedDict
from enum import IntEnum

from typing import Optional

import pkg_resources

from AnyQt.QtCore import Qt, QObject, pyqtSlot, QSize
from AnyQt.QtGui import QIcon, QCursor, QStandardItemModel, QStandardItem
from AnyQt.QtWidgets import (
    QApplication, QDialog, QFileDialog, QTableView, QHeaderView,
    QMessageBox)
from AnyQt.QtPrintSupport import QPrinter, QPrintDialog


from orangewidget import gui
from orangewidget.widget import OWBaseWidget
from orangewidget.settings import Setting

# Importing WebviewWidget can fail if neither QWebKit or QWebEngine
# are available
try:
    from orangewidget.utils.webview import WebviewWidget
except ImportError:  # pragma: no cover
    WebviewWidget = None
    HAVE_REPORT = False
else:
    HAVE_REPORT = True


log = logging.getLogger(__name__)

class Column(IntEnum):
    item = 0
    remove = 1
    scheme = 2


class ReportItem(QStandardItem):
    def __init__(self, name, html, scheme, module, icon_name, comment=""):
        self.name = name
        self.html = html
        self.scheme = scheme
        self.module = module
        self.icon_name = icon_name
        self.comment = comment
        try:
            path = pkg_resources.resource_filename(module, icon_name)
        except ImportError:
            path = ""
        except ValueError:
            path = ""
        icon = QIcon(path)
        self.id = id(icon)
        super().__init__(icon, name)

    def __getnewargs__(self):
        return (self.name, self.html, self.scheme, self.module, self.icon_name,
                self.comment)


class ReportItemModel(QStandardItemModel):
    def __init__(self, rows, columns, parent=None):
        super().__init__(rows, columns, parent)

    def add_item(self, item):
        row = self.rowCount()
        self.setItem(row, Column.item, item)
        self.setItem(row, Column.remove, self._icon_item("Remove"))
        self.setItem(row, Column.scheme, self._icon_item("Open Scheme"))

    def get_item_by_id(self, item_id):
        for i in range(self.rowCount()):
            item = self.item(i)
            if str(item.id) == item_id:
                return item
        return None

    @staticmethod
    def _icon_item(tooltip):
        item = QStandardItem()
        item.setEditable(False)
        item.setToolTip(tooltip)
        return item


class ReportTable(QTableView):
    def __init__(self, parent):
        super().__init__(parent)
        self._icon_remove = QIcon(pkg_resources.resource_filename(
            __name__, "icons/delete.svg"))
        self._icon_scheme = QIcon(pkg_resources.resource_filename(
            __name__, "icons/scheme.svg"))

    def mouseMoveEvent(self, event):
        self._clear_icons()
        self._repaint(self.indexAt(event.pos()))

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            super().mouseReleaseEvent(event)
        self._clear_icons()
        self._repaint(self.indexAt(event.pos()))

    def leaveEvent(self, _):
        self._clear_icons()

    def _repaint(self, index):
        row, column = index.row(), index.column()
        if column in (Column.remove, Column.scheme):
            self.setCursor(QCursor(Qt.PointingHandCursor))
        else:
            self.setCursor(QCursor(Qt.ArrowCursor))
        if row >= 0:
            self.model().item(row, Column.remove).setIcon(self._icon_remove)
            self.model().item(row, Column.scheme).setIcon(self._icon_scheme)

    def _clear_icons(self):
        model = self.model()
        for i in range(model.rowCount()):
            model.item(i, Column.remove).setIcon(QIcon())
            model.item(i, Column.scheme).setIcon(QIcon())


class OWReport(OWBaseWidget):
    name = "Report"
    save_dir = Setting("")
    open_dir = Setting("")

    def __init__(self):
        super().__init__()
        self._setup_ui_()
        self.report_changed = False
        self.have_report_warning_shown = False

        index_file = pkg_resources.resource_filename(__name__, "index.html")
        with open(index_file, "r") as f:
            self.report_html_template = f.read()

    def _setup_ui_(self):
        self.table_model = ReportItemModel(0, len(Column.__members__))
        self.table = ReportTable(self.controlArea)
        self.table.setModel(self.table_model)
        self.table.setShowGrid(False)
        self.table.setSelectionBehavior(QTableView.SelectRows)
        self.table.setSelectionMode(QTableView.SingleSelection)
        self.table.setWordWrap(False)
        self.table.setMouseTracking(True)
        self.table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed)
        self.table.verticalHeader().setDefaultSectionSize(20)
        self.table.verticalHeader().setVisible(False)
        self.table.horizontalHeader().setVisible(False)
        self.table.setFixedWidth(250)
        self.table.setColumnWidth(Column.item, 200)
        self.table.setColumnWidth(Column.remove, 23)
        self.table.setColumnWidth(Column.scheme, 25)
        self.table.clicked.connect(self._table_clicked)
        self.table.selectionModel().selectionChanged.connect(
            self._table_selection_changed)
        self.controlArea.layout().addWidget(self.table)

        self.last_scheme = None
        self.scheme_button = gui.button(
            self.controlArea, self, "Back to Last Scheme",
            callback=self._show_last_scheme
        )
        box = gui.hBox(self.controlArea)
        box.setContentsMargins(-6, 0, -6, 0)
        self.save_button = gui.button(
            box, self, "Save", callback=self.save_report, disabled=True
        )
        self.print_button = gui.button(
            box, self, "Print", callback=self._print_report, disabled=True
        )

        class PyBridge(QObject):
            @pyqtSlot(str)
            def _select_item(myself, item_id):
                item = self.table_model.get_item_by_id(item_id)
                self.table.selectRow(self.table_model.indexFromItem(item).row())
                self._change_selected_item(item)

            @pyqtSlot(str, str)
            def _add_comment(myself, item_id, value):
                item = self.table_model.get_item_by_id(item_id)
                item.comment = value
                self.report_changed = True

        if WebviewWidget is not None:
            self.report_view = WebviewWidget(self.mainArea, bridge=PyBridge(self))
            self.mainArea.layout().addWidget(self.report_view)
        else:
            self.report_view = None

    def sizeHint(self):
        return QSize(850, 500)

    def _table_clicked(self, index):
        if index.column() == Column.remove:
            self._remove_item(index.row())
            indexes = self.table.selectionModel().selectedIndexes()
            if indexes:
                item = self.table_model.item(indexes[0].row())
                self._scroll_to_item(item)
                self._change_selected_item(item)
        if index.column() == Column.scheme:
            self._show_scheme(index.row())

    def _table_selection_changed(self, new_selection, _):
        if new_selection.indexes():
            item = self.table_model.item(new_selection.indexes()[0].row())
            self._scroll_to_item(item)
            self._change_selected_item(item)

    def _remove_item(self, row):
        self.table_model.removeRow(row)
        self._empty_report()
        self.report_changed = True
        self._build_html()

    def clear(self):
        self.table_model.clear()
        self._empty_report()
        self.report_changed = True
        self._build_html()

    def _add_item(self, widget):
        name = widget.get_widget_name_extension()
        name = "{} - {}".format(widget.name, name) if name else widget.name
        item = ReportItem(name, widget.report_html, self._get_scheme(),
                          widget.__module__, widget.icon)
        self.table_model.add_item(item)
        self._empty_report()
        self.report_changed = True
        return item

    def _empty_report(self):
        # disable save and print if no reports
        self.save_button.setEnabled(self.table_model.rowCount())
        self.print_button.setEnabled(self.table_model.rowCount())

    def _build_html(self, selected_id=None):
        if not self.report_view:
            return
        html = self.report_html_template
        if selected_id is not None:
            onload = f"(function (id) {{" \
                     f"     setSelectedId(id); scrollToId(id); " \
                     f"}}" \
                     f"(\"{selected_id}\"));" \
                     f""
            html += f"<body onload='{onload}'>"
        else:
            html += "<body>"
        for i in range(self.table_model.rowCount()):
            item = self.table_model.item(i)
            html += "<div id='{}' class='normal' " \
                    "onClick='pybridge._select_item(this.id)'>{}<div " \
                    "class='textwrapper'><textarea " \
                    "placeholder='Write a comment...'" \
                    "onInput='this.innerHTML = this.value;" \
                    "pybridge._add_comment(this.parentNode.parentNode.id, this.value);'" \
                    ">{}</textarea></div>" \
                    "</div>".format(item.id, item.html, item.comment)
        html += "</body></html>"
        self.report_view.setHtml(html)

    def _scroll_to_item(self, item):
        if not self.report_view:
            return
        self.report_view.runJavaScript(
            f"scrollToId('{item.id}')",
            lambda res: log.debug("scrollToId returned %s", res)
        )

    def _change_selected_item(self, item):
        if not self.report_view:
            return
        self.report_view.runJavaScript(
            f"setSelectedId('{item.id}');",
            lambda res: log.debug("setSelectedId returned %s", res)
        )
        self.report_changed = True

    def make_report(self, widget):
        item = self._add_item(widget)
        self._build_html(item.id)
        self.table.selectionModel().selectionChanged.disconnect(
            self._table_selection_changed
        )
        self.table.selectRow(self.table_model.rowCount() - 1)
        self.table.selectionModel().selectionChanged.connect(
            self._table_selection_changed
        )

    def _get_scheme(self):
        canvas = self.get_canvas_instance()
        if canvas is None:
            return None
        scheme = canvas.current_document().scheme()
        return self._get_scheme_str(scheme)

    def _get_scheme_str(self, scheme):
        buffer = io.BytesIO()
        scheme.save_to(buffer, pickle_fallback=True)
        return buffer.getvalue().decode("utf-8")

    def _show_scheme(self, row):
        scheme = self.table_model.item(row).scheme
        canvas = self.get_canvas_instance()
        if canvas is None:
            return
        document = canvas.current_document()
        if document.isModifiedStrict():
            self.last_scheme = self._get_scheme_str(document.scheme())
        self._load_scheme(scheme)

    def _show_last_scheme(self):
        if self.last_scheme:
            self._load_scheme(self.last_scheme)

    def _load_scheme(self, contents):
        # forcibly load the contents into the associated CanvasMainWindow
        # instance if one exists. Preserve `self` as the designated report.
        canvas = self.get_canvas_instance()
        if canvas is not None:
            document = canvas.current_document()
            scheme = document.scheme()
            # Clear the undo stack as it will no longer apply to the new
            # workflow.
            document.undoStack().clear()
            scheme.clear()
            scheme.load_from(io.StringIO(contents))

    def save_report(self):
        """Save report"""
        formats = (('HTML (*.html)', '.html'),
                   ('PDF (*.pdf)', '.pdf')) if self.report_view else tuple()
        formats = formats + (('Report (*.report)', '.report'),)
        formats = OrderedDict(formats)

        filename, selected_format = QFileDialog.getSaveFileName(
            self, "Save Report", self.save_dir, ';;'.join(formats.keys()))
        if not filename:
            return QDialog.Rejected

        # Set appropriate extension if not set by the user
        expect_ext = formats[selected_format]
        if not filename.endswith(expect_ext):
            filename += expect_ext

        self.save_dir = os.path.dirname(filename)
        self.saveSettings()
        _, extension = os.path.splitext(filename)
        if extension == ".pdf":
            printer = QPrinter()
            printer.setPageSize(QPrinter.A4)
            printer.setOutputFormat(QPrinter.PdfFormat)
            printer.setOutputFileName(filename)
            self._print_to_printer(printer)
        elif extension == ".report":
            self.save(filename)
        else:
            def save_html(contents):
                try:
                    with open(filename, "w", encoding="utf-8") as f:
                        f.write(contents)
                except PermissionError:
                    self.permission_error(filename)

            if self.report_view:
                save_html(self.report_view.html())
        self.report_changed = False
        return QDialog.Accepted

    def _print_to_printer(self, printer):
        if not self.report_view:
            return
        filename = printer.outputFileName()
        if filename:
            try:
                # QtWebEngine
                return self.report_view.page().printToPdf(filename)
            except AttributeError:
                try:
                    # QtWebKit
                    return self.report_view.print_(printer)
                except AttributeError:
                    # QtWebEngine 5.6
                    pass
        # Fallback to printing widget as an image
        self.report_view.render(printer)

    def _print_report(self):
        printer = QPrinter()
        print_dialog = QPrintDialog(printer, self)
        print_dialog.setWindowTitle("Print report")
        if print_dialog.exec() != QDialog.Accepted:
            return
        self._print_to_printer(printer)

    def save(self, filename):
        attributes = {}
        for key in ('last_scheme', 'open_dir'):
            attributes[key] = getattr(self, key, None)
        items = [self.table_model.item(i)
                 for i in range(self.table_model.rowCount())]
        report = dict(__version__=1,
                      attributes=attributes,
                      items=items)

        try:
            with open(filename, 'wb') as f:
                pickle.dump(report, f)
        except PermissionError:
            self.permission_error(filename)

    @classmethod
    def load(cls, filename):
        with open(filename, 'rb') as f:
            report = pickle.load(f)

        if not isinstance(report, dict):
            return report

        self = cls()
        self.__dict__.update(report['attributes'])
        for item in report['items']:
            self.table_model.add_item(
                ReportItem(item.name, item.html, item.scheme,
                           item.module, item.icon_name, item.comment)
            )
        return self

    def permission_error(self, filename):
        log.error("PermissionError when trying to write report.", exc_info=True)
        mb = QMessageBox(
            self,
            icon=QMessageBox.Critical,
            windowTitle=self.tr("Error"),
            text=self.tr("Permission error when trying to write report."),
            informativeText=self.tr("Permission error occurred "
                                    "while saving '{}'.").format(filename),
            detailedText=traceback.format_exc(limit=20)
        )
        mb.setWindowModality(Qt.WindowModal)
        mb.setAttribute(Qt.WA_DeleteOnClose)
        mb.exec()

    def is_empty(self):
        return not self.table_model.rowCount()

    def is_changed(self):
        return self.report_changed

    @staticmethod
    def set_instance(report):
        warnings.warn(
            "OWReport.set_instance is deprecated",
            DeprecationWarning, stacklevel=2
        )
        app_inst = QApplication.instance()
        app_inst._report_window = report

    @staticmethod
    def get_instance():
        warnings.warn(
            "OWReport.get_instance is deprecated",
            DeprecationWarning, stacklevel=2
        )
        app_inst = QApplication.instance()
        if not hasattr(app_inst, "_report_window"):
            report = OWReport()
            app_inst._report_window = report
        return app_inst._report_window

    def get_canvas_instance(self):
        # type: () -> Optional[CanvasMainWindow]
        """
        Return a CanvasMainWindow instance to which this report is attached.

        Return None if not associated with any window.

        Returns
        -------
        window : Optional[CanvasMainWindow]
        """
        try:
            from orangewidget.workflow.mainwindow import OWCanvasMainWindow
        except ImportError:
            return None
        # Run up the parent/window chain
        parent = self.parent()
        if parent is not None:
            window = parent.window()
            if isinstance(window, OWCanvasMainWindow):
                return window

    def copy_to_clipboard(self):
        if self.report_view:
            self.report_view.triggerPageAction(self.report_view.page().Copy)

