"""
The Debugger Widget is an uneditable Card that gives you feedback on errors
thrown by your Panel callbacks.
"""
import param
import logging

from ..layout.card import Card
from ..reactive import ReactiveHTML
from ..io.state import state
from ..layout import Row, HSpacer
from .terminal import Terminal


class TermFormatter(logging.Formatter):

    def __init__(self, *args, only_last=True, **kwargs):
        """
        Standard logging.Formatter with the default option of prompting
        only the last stack. Does not cache exc_text.

        Parameters
        ----------
        only_last : BOOLEAN, optional
            Whether the full stack trace or only the last file should be shown.
            The default is True.

        Returns
        -------
        None.

        """

        super().__init__(*args, **kwargs)
        self.only_last = only_last

    def format(self, record):

        record.message = record.getMessage()
        if self.usesTime():
            record.asctime = self.formatTime(record, self.datefmt)
        s = self.formatMessage(record)
        exc_text = None
        if record.exc_info:
            exc_text = super().formatException(record.exc_info)
            last = exc_text.rfind('File')
            if last >0 and self.only_last:
                exc_text = exc_text[last:]
        if exc_text:
            if s[-1:] != "\n":
                s = s + "\n"
            s = s + exc_text
        if record.stack_info:
            if s[-1:] != "\n":
                s = s + "\n"
            s = s + self.formatStack(record.stack_info)
        return s


class CheckFilter(logging.Filter):

    def add_debugger(self, debugger):
        """
        Add a debugger to this logging filter.

        Parameters
        ----------
        widg : panel.widgets.Debugger
            The widget displaying the logs.

        Returns
        -------
        None.
        """
        self.debugger = debugger

    def _update_debugger(self, record):
        if not hasattr(self, 'debugger'):
            return
        if record.levelno >= 40:
            self.debugger._number_of_errors += 1
        elif 40 > record.levelno >= 30:
            self.debugger._number_of_warnings += 1
        elif record.levelno < 30:
            self.debugger._number_of_infos += 1

    def filter(self,record):
        """
        Will filter out messages coming from a different bokeh document than
        the document where the debugger is embedded in server mode.
        Returns True if no debugger was added.
        """
        if not hasattr(self, 'debugger'):
            return True


        if state.curdoc and state.curdoc.session_context:
            session_id = state.curdoc.session_context.id
            widget_session_ids = set(m.document.session_context.id
                                     for m in sum(self.debugger._models.values(),
                                                  tuple()) if m.document.session_context)

            if session_id not in widget_session_ids:
                return False
        self._update_debugger(record)
        return True


class DebuggerButtons(ReactiveHTML):

    terminal_output = param.String()

    debug_name = param.String()

    clears = param.Integer(default=0)

    _template = """
    <div class="bk" style="display: flex;">
      <button class='special_btn clear_btn'
         id="clear_btn"
         onclick="${script('click_clear')}"
         style="width: ${model.width}px;">
        <span class="shown">☐</span>
        <span class="tooltiptext">Acknowledge logs and clear</span>
      </button>
      <button class='special_btn'
              id="save_btn"
              onclick="${script('click')}"
              style="width: ${model.width}px;">💾
        <span class="tooltiptext">Save logs</span>
      </button>
    </div>
    """

    js_cb = """
        var filename = data.debug_name+'.txt'
        console.log('saving debugger terminal output to '+filename)
        var blob = new Blob([data.terminal_output],
            { type: "text/plain;charset=utf-8" });
        if (navigator.msSaveBlob) {
            navigator.msSaveBlob(blob, filename);
        } else {
            var link = document.createElement('a');
            var url = URL.createObjectURL(blob);
            link.href = url;
            link.download = filename;
            document.body.appendChild(link);
            link.click();
            setTimeout(function() {
                document.body.removeChild(link);
                window.URL.revokeObjectURL(url);
            }, 0);
        }
        """

    _scripts = {
        'click': js_cb,
        'click_clear': "data.clears += 1"
    }

    _dom_events = {'clear_btn': ['click']}


class Debugger(Card):
    """
    A uneditable Card layout holding a terminal printing out logs from your
    callbacks. By default, it will only print exceptions. If you want to add
    your own log, use the `panel.callbacks` logger within your callbacks:
    `logger = logging.getLogger('panel.callbacks')`
    """

    _number_of_errors = param.Integer(bounds=(0, None), precedence=-1, doc="""
        Number of logged errors since last acknowledged.""")

    _number_of_warnings = param.Integer(bounds=(0, None), precedence=-1, doc="""
        Number of logged warnings since last acknowledged.""")

    _number_of_infos = param.Integer(bounds=(0, None), precedence=-1, doc="""
        Number of logged informations since last acknowledged.""")

    only_last = param.Boolean(default=True, doc="""
        Whether only the last stack is printed or the full.""")

    level = param.Integer(default=logging.ERROR, doc="""
        Logging level to print in the debugger terminal.""")

    formatter_args = param.Dict(
        default={'fmt':"%(asctime)s [%(name)s - %(levelname)s]: %(message)s"},
        precedence=-1,
        doc="""
        Arguments to pass to the logging formatter. See
        the standard python logging libraries."""
        )

    logger_names = param.List(
        default=['panel'],
        item_type=str,
        bounds=(1, None),
        precedence=-1,
        doc="""
        Loggers which will be prompted in the debugger terminal."""
    )

    _rename = Card._rename.copy()

    _rename.update({
        '_number_of_errors': None,
        '_number_of_warnings': None,
        '_number_of_infos': None,
        'only_last': None,
        'level': None,
        'formatter_args': None,
        'logger_names': None,
    })

    def __init__(self, **params):
        super().__init__(**params)
        #change default css
        self.button_css_classes = ['debugger-card-button']
        self.css_classes = ['debugger-card']
        self.header_css_classes = ['debugger-card-header']
        self.title_css_classes = ['debugger-card-title']

        if self.width and self.height:
            smode = 'fixed'
        elif self.width:
            smode = 'stretch_height'
        elif self.height:
            smode = 'stretch_width'
        else:
            smode = 'stretch_both'
        height = self.height or self.min_height
        width = self.width or self.min_width
        terminal = Terminal(
            min_height=200, sizing_mode=smode, name=self.name,
            margin=5,
            width=(width-10) if width else None,
            height=(height-70) if height else None
        )

        stream_handler = logging.StreamHandler(terminal)
        stream_handler.terminator = "  \n"

        formatter = TermFormatter(
            **self.formatter_args,
            only_last=self.only_last
        )

        stream_handler.setFormatter(formatter)
        stream_handler.setLevel(self.level)
        curr_filter = CheckFilter()

        curr_filter.add_debugger(self)

        stream_handler.addFilter(curr_filter)

        for logger_name in self.logger_names:
            logger = logging.getLogger(logger_name)
            logger.addHandler(stream_handler)

        self.terminal = terminal

        #callbacks for header
        self.param.watch(self.update_log_counts,'_number_of_errors')
        self.param.watch(self.update_log_counts,'_number_of_warnings')
        self.param.watch(self.update_log_counts,'_number_of_infos')

        #buttons
        self.btns = DebuggerButtons()
        inc = """
        target.data.terminal_output += source.output
        """
        clr = """
        target.data.terminal_output = ''
        """
        self.terminal.jslink(self.btns,code={'_output': inc})
        self.terminal.jslink(self.btns,code={'_clears': clr})
        self.btns.jslink(self.terminal,clears='_clears')
        self.terminal.param.watch(self.acknowledge_errors, ['_clears'])

        self.jslink(self.btns,name='debug_name')

        #set header
        self.title = ''

        #body
        self.append(
            Row(
                self.name, HSpacer(), self.btns,
                sizing_mode='stretch_width', align=('end','start')
            )
        )
        self.append(terminal)

        #make it an uneditable card
        self.param['objects'].constant = True

        #by default it should be collapsed and small.
        self.collapsed = True

    def update_log_counts(self, event):
        title = []
        if self._number_of_errors:
            title.append(f'<span style="color:rgb(190,0,0);">errors: </span>{self._number_of_errors}')
        if self._number_of_warnings:
            title.append(f'<span style="color:rgb(190,160,20);">w: </span>{self._number_of_warnings}')
        if self._number_of_infos:
            title.append(f'i: {self._number_of_infos}')

        self.title = ', '.join(title)

    def acknowledge_errors(self,event):
        self._number_of_errors = 0
        self._number_of_warnings = 0
        self._number_of_infos = 0
