#!/usr/bin/env python

# ###########################################################################
#
# This file is part of Taurus
#
# http://taurus-scada.org
#
# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
#
# Taurus is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Taurus is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Taurus.  If not, see <http://www.gnu.org/licenses/>.
#
# ###########################################################################

"""This module provides a panel to display taurus messages
"""

import sys
import traceback
import datetime

try:
    import pygments
    from pygments import highlight
    from pygments.formatters import HtmlFormatter
    from pygments.lexers import PythonTracebackLexer
except Exception:
    pygments = None

from taurus.core.util.report import TaurusMessageReportHandler
from taurus.external.qt import Qt
from taurus.qt.qtgui.util.ui import UILoadable


__docformat__ = "restructuredtext"


class TaurusMessageErrorHandler(object):
    """This class is designed to handle a generic error into a
    :class:`TaurusMessagePanel`
    """

    def __init__(self, msgbox):
        self._msgbox = msgbox
        msgbox.setWindowTitle("Taurus Error")
        self.setError(*msgbox.getError())

    def setError(self, err_type=None, err_value=None, err_traceback=None):
        """Translates the given error object into an HTML string and places it
        in the message panel

        :param error: an error object (typically an exception object)
        :type error: object
        """

        msgbox = self._msgbox
        error = "".join(traceback.format_exception_only(err_type, err_value))
        msgbox.setText(error)
        msg = "<html><body><pre>%s</pre></body></html>" % error
        msgbox.setDetailedHtml(msg)

        html_orig = (
            '<html><head><style type="text/css">{style}</style>'
            "</head><body>"
        )
        exc_info = "".join(
            traceback.format_exception(err_type, err_value, err_traceback)
        )
        style = ""
        if pygments is not None:
            formatter = HtmlFormatter()
            style = formatter.get_style_defs()
        html = html_orig.format(style=style)
        if pygments is None:
            html += "<pre>%s</pre>" % exc_info
        else:
            formatter = HtmlFormatter()
            html += highlight(exc_info, PythonTracebackLexer(), formatter)
        html += "</body></html>"
        msgbox.setOriginHtml(html)


class TangoMessageErrorHandler(TaurusMessageErrorHandler):
    """This class is designed to handle :class:`PyTango.DevFailed` error into
    a :class:`TaurusMessagePanel`
    """

    # TODO: tango-centric

    def setError(self, err_type=None, err_value=None, err_traceback=None):
        """Translates the given error object into an HTML string and places it
        it the message panel

        :param error: an error object (typically an exception object)
        :type error: object
        """

        msgbox = self._msgbox
        html_orig = (
            '<html><head><style type="text/css">{style}</style>'
            "</head><body>"
        )
        style, formatter = "", None
        if pygments is not None:
            formatter = HtmlFormatter()
            style = formatter.get_style_defs()
        html = html_orig.format(style=style)
        for de in err_value.args:
            e_html = """<pre>{reason}: {desc}</pre>{origin}<hr>"""
            origin, reason, desc = de.origin, de.reason, de.desc
            if reason.startswith("PyDs_") and pygments is not None:
                origin = highlight(origin, PythonTracebackLexer(), formatter)
            else:
                origin = "<pre>%s</pre>" % origin
            html += e_html.format(desc=desc, origin=origin, reason=reason)
        html += "</body></html>"
        msgbox.setText(err_value.args[0].desc)
        msgbox.setDetailedHtml(html)

        exc_info = "".join(
            traceback.format_exception(err_type, err_value, err_traceback)
        )
        html = html_orig.format(style=style)
        if pygments is None:
            html += "<pre>%s</pre>" % exc_info
        else:
            html += highlight(exc_info, PythonTracebackLexer(), formatter)
        html += "</body></html>"
        msgbox.setOriginHtml(html)


class MacroServerMessageErrorHandler(TaurusMessageErrorHandler):
    def setError(self, err_type=None, err_value=None, err_traceback=None):
        """Translates the given error object into an HTML string and places it
        in the message panel

        :param error: an error object (typically an exception object)
        :type error: object
        """

        msgbox = self._msgbox
        msgbox.setText(err_value)
        msg = "<html><body><pre>%s</pre></body></html>" % err_value
        msgbox.setDetailedHtml(msg)

        html_orig = """<html><head><style type="text/css">{style}</style></head><body>"""  # noqa
        exc_info = "".join(err_traceback)
        style = ""
        if pygments is not None:
            formatter = HtmlFormatter()
            style = formatter.get_style_defs()
        html = html_orig.format(style=style)
        if pygments is None:
            html += "<pre>%s</pre>" % exc_info
        else:
            formatter = HtmlFormatter()
            html += highlight(exc_info, PythonTracebackLexer(), formatter)
        html += "</body></html>"
        msgbox.setOriginHtml(html)


def is_report_handler(report, abs_file=None):
    """Helper function to determine if a certain python object is a valid
    report handler
    """
    import inspect

    if not inspect.isclass(report):
        return False
    if not issubclass(report, TaurusMessageReportHandler):
        return False
    try:
        if inspect.getabsfile(report) != abs_file:
            return False
    except Exception:
        return False
    return True


_REPORT_HANDLERS = None


def get_report_handlers():
    global _REPORT_HANDLERS
    if _REPORT_HANDLERS is not None:
        return _REPORT_HANDLERS

    import os.path
    import functools
    import inspect

    this = os.path.abspath(__file__)
    report_path = os.path.join(os.path.dirname(this), "report")
    sys.path.insert(0, report_path)

    _REPORT_HANDLERS = {}
    try:
        for elem in os.listdir(report_path):
            if not elem[0].isalpha():
                continue
            full_elem = os.path.join(report_path, elem)
            if not os.path.isfile(full_elem):
                continue
            if not elem.endswith(".py"):
                continue
            elem, _ = os.path.splitext(elem)
            _is_report_handler = functools.partial(
                is_report_handler, abs_file=full_elem
            )
            report_lib = __import__(elem, globals(), locals(), [], 0)
            for name, obj in inspect.getmembers(
                report_lib, _is_report_handler
            ):
                _REPORT_HANDLERS[name] = obj
    finally:
        sys.path.pop(0)

    return _REPORT_HANDLERS


_REPORT = """\
-- Description ---------------------------------------------------------------
An error occured in '{appName} {appVersion}' on {time}
{text}

-- Details -------------------------------------------------------------------
{detail}

-- Origin --------------------------------------------------------------------
{origin}
------------------------------------------------------------------------------
"""


@UILoadable(with_ui="_ui")
class TaurusMessagePanel(Qt.QWidget):
    """A panel intended to display a taurus error.
    Example::

        dev = taurus.Device("sys/tg_test/1")
        try:
            print(dev.read_attribute("throw_exception"))
        except PyTango.DevFailed, df:
            msgbox = TaurusMessagePanel()
            msgbox.show()

    You can show the error outside the exception handling code. If you do this,
    you should keep a record of the exception information as given by
    :func:`sys.exc_info`::

        dev = taurus.Device("sys/tg_test/1")
        exc_info = None
        try:
            print(dev.read_attribute("throw_exception"))
        except PyTango.DevFailed, df:
            exc_info = sys.exc_info()

        if exc_info:
            msgbox = TaurusMessagePanel(*exc_info)
            msgbox.show()"""

    toggledDetails = Qt.pyqtSignal(bool)

    def __init__(
        self,
        err_type=None,
        err_value=None,
        err_traceback=None,
        parent=None,
        designMode=False,
    ):
        Qt.QWidget.__init__(self, parent)
        self.loadUi()
        self._exc_info = err_type, err_value, err_traceback

        self._ui._detailsWidget.setVisible(False)
        self._ui._checkBox.setVisible(False)
        self._ui._checkBox.setCheckState(Qt.Qt.Unchecked)
        self._initReportCombo()

        self._ui._showDetailsButton.toggled.connect(self._onShowDetails)
        self._ui._reportComboBox.activated.connect(self._onReportTriggered)

        pixmap = Qt.QIcon.fromTheme("emblem-important").pixmap(48, 48)
        self.setIconPixmap(pixmap)

        if err_value is not None:
            self.setError(*self._exc_info)
        self.adjustSize()

    def _initReportCombo(self):
        report_handlers = get_report_handlers()
        combo = self.reportComboBox()
        combo.addItem("Choose report...", None)
        for name, report_handler in report_handlers.items():
            combo.addItem(report_handler.Label, name)

    def _onReportTriggered(self, index):
        report_handlers = get_report_handlers()
        combo = self.reportComboBox()
        name = combo.itemData(index)
        if name is None:
            return
        report_handler = report_handlers[name]
        report = report_handler(self)
        app = Qt.QApplication.instance()
        txt = _REPORT.format(
            appName=app.applicationName(),
            appVersion=app.applicationVersion(),
            time=datetime.datetime.now().ctime(),
            text=self.getText(),
            detail=self.getDetailedText(),
            origin=self.getOriginText(),
        )
        report.report(txt)

    def _onShowDetails(self, show):
        self._ui._detailsWidget.setVisible(show)
        if show:
            text = "Hide details..."
        else:
            text = "Show details..."
        self._ui._showDetailsButton.setText(text)
        self.adjustSize()
        self.toggledDetails.emit(show)

    def reportComboBox(self):
        return self._ui._reportComboBox

    def checkBox(self):
        """Returns the check box from this panel

        :return: the check box from this panel
        :rtype: PyQt5.Qt.QCheckBox
        """
        return self._ui._checkBox

    def checkBoxState(self):
        """Returns the check box state

        :return: the check box state
        :rtype: PyQt5.Qt.CheckState
        """
        return self.checkBox().checkState()

    def checkBoxText(self):
        """Returns the check box text

        :return: the check box text
        :rtype: str
        """
        return str(self.checkBox().text())

    def setCheckBoxText(self, text):
        """Sets the checkbox text.

        :param text: new checkbox text
        :type text: str
        """
        self.checkBox().setText(text)

    def setCheckBoxState(self, state):
        """Sets the checkbox state.

        :param text: new checkbox state
        :type text: PyQt5.Qt.CheckState
        """
        self.checkBox().setCheckState(state)

    def setCheckBoxVisible(self, visible):
        """Sets the checkbox visibility.

        :param visible: True makes checkbox visible, False hides it
        :type visible: bool
        """
        self.checkBox().setVisible(visible)

    def buttonBox(self):
        """Returns the button box from this panel

        :return: the button box from this panel
        :rtype: PyQt5.Qt.QDialogButtonBox
        """
        return self._ui._buttonBox

    def addButton(self, button, role=Qt.QDialogButtonBox.ActionRole):
        """Adds the given button with the given to the button box

        :param button: the button to be added
        :type button: PyQt5.Qt.QPushButton
        :param role: button role
        :type role: PyQt5.Qt.QDialogButtonBox.ButtonRole
        """
        self._ui._buttonBox.addButton(button, role)

    def setIconPixmap(self, pixmap):
        """Sets the icon to the dialog

        :param pixmap: the icon pixmap
        :type pixmap: PyQt5.Qt.QPixmap
        """
        self._ui._iconLabel.setPixmap(pixmap)

    def setText(self, text):
        """Sets the text of this panel

        :param text: the new text
        :type text: str
        """
        self._ui._textLabel.setText(text)

    def getText(self):
        """Returns the current text of this panel

        :return: the text for this panel
        :rtype: str
        """
        return self._ui._textLabel.text()

    def setDetailedText(self, text):
        """Sets the detailed text of the dialog

        :param text: the new text
        :type text: str
        """
        self._ui._detailsTextEdit.setPlainText(text)

    def setDetailedHtml(self, html):
        """Sets the detailed HTML of the dialog

        :param html: the new HTML text
        :type html: str
        """
        self._ui._detailsTextEdit.setHtml(html)

    def getDetailedText(self):
        """Returns the current detailed text of this panel

        :return: the detailed text for this panel
        :rtype: str
        """
        return self._ui._detailsTextEdit.toPlainText()

    def getDetailedHtml(self):
        """Returns the current detailed HTML of this panel

        :return: the detailed HTML for this panel
        :rtype: str
        """
        return self._ui._detailsTextEdit.toHtml()

    def setOriginText(self, text):
        """Sets the origin text of the dialog

        :param text: the new text
        :type text: str
        """
        self._ui._originTextEdit.setPlainText(text)

    def setOriginHtml(self, html):
        """Sets the origin HTML of the dialog

        :param html: the new HTML text
        :type html: str
        """
        self._ui._originTextEdit.setHtml(html)

    def getOriginText(self):
        """Returns the current origin text of this panel

        :return: the origin text for this panel
        :rtype: str
        """
        return self._ui._originTextEdit.toPlainText()

    def getOriginHtml(self):
        """Returns the current origin HTML of this panel

        :return: the origin HTML for this panel
        :rtype: str
        """
        return self._ui._originTextEdit.toHtml()

    def setError(self, err_type=None, err_value=None, err_traceback=None):
        """Sets the exception object.
        Example usage::

            dev = taurus.Device("sys/tg_test/1")
            exc_info = None
            msgbox = TaurusMessagePanel()
            try:
                print(dev.read_attribute("throw_exception"))
            except PyTango.DevFailed, df:
                exc_info = sys.exc_info()

            if exc_info:
                msgbox.setError(*exc_info)
                msgbox.show()

        :param err_type: the exception type of the exception being handled (a
            class object)
        :type error: class object
        :param err_value: exception object
        :type err_value: object
        :param err_traceback: a traceback object which encapsulates the call
            stack at the point where the exception originally occurred
        :type err_traceback: TracebackType
        """
        i = sys.exc_info()
        self._exc_info = [
            err_type or i[0],
            err_value or i[1],
            err_traceback or i[2],
        ]

        handler_klass = self.findErrorHandler(self._exc_info[0])
        handler_klass(self)

    def getError(self):
        """Returns the current exception information of this panel

        :return: the current exception information (same as type as returned by
            :func:`sys.exc_info`)
        :rtype: tuple<type, Exception, traceback>
        """
        return self._exc_info

    try:
        import PyTango

        ErrorHandlers = {PyTango.DevFailed: TangoMessageErrorHandler}
    except Exception:
        ErrorHandlers = {}

    @classmethod
    def registerErrorHandler(klass, err_type, err_handler):
        klass.ErrorHandlers[err_type] = err_handler

    @classmethod
    def findErrorHandler(klass, err_type):
        """Finds the proper error handler class for the given error

        :param err_type: error class
        :type err_type: class object
        :return: a message box error handler
        :rtype: TaurusMessageBoxErrorHandler class object
        """

        for exc, h_klass in klass.ErrorHandlers.items():
            if issubclass(err_type, exc):
                return h_klass
        return TaurusMessageErrorHandler


class DemoException(Exception):
    """Just a plain python exception for demo purposes"""

    pass


def s1():
    """Just a function to make the stack more interesting"""
    return s2()


def s2():
    """Just a function to make the stack more interesting"""
    return s3()


def s3():
    """Just a function to make the stack more interesting"""
    raise DemoException("A demo exception occurred")


class QMessageDialog(Qt.QDialog):
    """Helper class for the demo"""

    def __init__(self, msgbox, parent=None):
        Qt.QDialog.__init__(self, parent)
        lyt = Qt.QVBoxLayout()
        lyt.setContentsMargins(0, 0, 0, 0)
        lyt.addWidget(msgbox)
        lyt.addStretch(1)
        self.setLayout(lyt)


def py_exc():
    """Shows a python exception in a TaurusMessagePanel"""
    try:
        s1()
    except Exception:
        msgbox = TaurusMessagePanel(*sys.exc_info())
        QMessageDialog(msgbox).exec_()


def tg_exc():
    """Shows a tango exception in a TaurusMessagePanel"""
    # TODO: This function is Tango centric
    import PyTango

    try:
        PyTango.Except.throw_exception(
            "TangoException", "A simple tango exception", "right here"
        )
    except PyTango.DevFailed:
        msgbox = TaurusMessagePanel(*sys.exc_info())
        QMessageDialog(msgbox).exec_()


def tg_serv_exc():
    """Shows a tango exception from a server in a TaurusMessagePanel"""
    # TODO: This function is Tango centric
    import PyTango
    import taurus

    dev = taurus.Device("sys/tg_test/1")
    try:
        dev.read_attribute("throw_exception")
    except PyTango.DevFailed:
        msgbox = TaurusMessagePanel(*sys.exc_info())
        QMessageDialog(msgbox).exec_()
    except Exception:
        msgbox = TaurusMessagePanel(*sys.exc_info())
        QMessageDialog(msgbox).exec_()


def py_tg_serv_exc():
    """Shows a tango exception from a python server in a TaurusMessagePanel"""
    # TODO: This function is Tango centric
    import PyTango

    try:
        PyTango.Except.throw_exception(
            "TangoException", "A simple tango exception", "right here"
        )
    except PyTango.DevFailed as df1:
        try:
            import traceback
            import io

            origin = io.StringIO()
            traceback.print_stack(file=origin)
            origin.seek(0)
            origin = origin.read()
            PyTango.Except.re_throw_exception(
                df1,
                "PyDs_Exception",
                "DevFailed: A simple tango exception",
                origin,
            )
        except PyTango.DevFailed:
            msgbox = TaurusMessagePanel(*sys.exc_info())
            QMessageDialog(msgbox).exec_()


def demo():
    """Message panel"""
    panel = Qt.QWidget()
    layout = Qt.QVBoxLayout()
    panel.setLayout(layout)

    m1 = Qt.QPushButton("Python exception")
    layout.addWidget(m1)
    m1.clicked.connect(py_exc)
    m2 = Qt.QPushButton("Tango exception")
    layout.addWidget(m2)
    m2.clicked.connect(tg_exc)
    layout.addWidget(m2)
    m3 = Qt.QPushButton("Tango server exception")
    layout.addWidget(m3)
    m3.clicked.connect(tg_serv_exc)
    layout.addWidget(m3)
    m4 = Qt.QPushButton("Python tango server exception")
    layout.addWidget(m4)
    m4.clicked.connect(py_tg_serv_exc)
    layout.addWidget(m4)
    return panel


def main():

    import sys
    import taurus.qt.qtgui.application

    Application = taurus.qt.qtgui.application.TaurusApplication

    app = Application.instance()
    owns_app = app is None

    if owns_app:
        app = Qt.QApplication([])
        app.setApplicationName("Taurus message demo")
        app.setApplicationVersion("1.0")

    w = demo()
    w.show()
    if owns_app:
        sys.exit(app.exec_())
    else:
        return w


if __name__ == "__main__":
    main()
