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

from typing import Optional

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


from orangewidget import gui
from orangewidget.utils import load_styled_icon
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:
            icon = load_styled_icon(module, icon_name)
        except (ImportError, FileNotFoundError):
            icon = QIcon()
        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 = load_styled_icon(__package__, "icons/delete.svg")
        self._icon_scheme = load_styled_icon(__package__, "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 = pkgutil.get_data(__name__, "index.html")
        self.report_html_template = index_file.decode("utf-8")

    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(QPageSize(QPageSize.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)

