# Copyright (c) 2013-2022 khal contributors
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


"""this module should contain classes that are specific to ikhal, more
general widgets should go in widgets.py"""

from __future__ import annotations

import logging
import threading
import time
from collections.abc import Callable

import urwid

from .widgets import NColumns

logger = logging.getLogger("khal")


class Pane(urwid.WidgetWrap):
    """An abstract Pane to be used in a Window object."""

    def __init__(self, widget, title=None, description=None) -> None:
        self.widget = widget
        urwid.WidgetWrap.__init__(self, widget)
        self._title = title or ""
        self._description = description or ""
        self.window = None

    @property
    def title(self):
        return self._title

    def selectable(self):
        """mark this widget as selectable"""
        return True

    @property
    def description(self):
        return self._description

    def dialog(self, text: str, buttons: list[tuple[str, Callable]]) -> None:
        """Open a dialog box.

        :param text: Text to appear as the body of the Dialog box
        :param buttons: list of tuples button labels and functions to call
            when the button is pressed
        """
        lines = [urwid.Text(line) for line in text.splitlines()]

        buttons = NColumns(
            [urwid.Button(label, on_press=func) for label, func in buttons],
            outermost=True,
        )
        lines.append(buttons)
        content = urwid.LineBox(urwid.Pile(lines))
        overlay = urwid.Overlay(content, self, "center", ("relative", 70), ("relative", 70), None)
        assert self.window is not None
        self.window.open(overlay)

    def scrollable_dialog(
        self,
        text: str | list[urwid.Text],
        buttons: list[tuple[str, Callable]] | None = None,
        title: str = "Press `ESC` to close this window",
    ) -> None:
        """Open a scrollable dialog box.

        :param text: Text to appear as the body of the Dialog box
        :param buttons: button labels and functions to call when the button is pressed
        """
        if isinstance(text, str):
            body = urwid.ListBox([urwid.Text(line) for line in text.splitlines()])
        else:
            body = urwid.ListBox(text)
        if buttons:
            buttons = NColumns(
                [urwid.Button(label, on_press=func) for label, func in buttons],
                outermost=True,
            )
            content = urwid.LineBox(urwid.Pile([body, ("pack", buttons)]))
        else:
            content = urwid.LineBox(urwid.Pile([body]))

        # put the title on the upper line
        over = urwid.Overlay(
            urwid.Text(" " + title + " "),
            content,
            "center",
            len(title) + 2,
            "top",
            None,
        )
        overlay = urwid.Overlay(
            over, self, "center", ("relative", 70), "middle", ("relative", 70), None
        )
        assert self.window is not None
        self.window.open(overlay)

    def keypress(self, size, key):
        """Handle application-wide key strokes."""
        if key in ["f1", "?"]:
            self.show_keybindings()
        elif key in ["L"]:
            self.show_log()
        else:
            return super().keypress(size, key)

    def show_keybindings(self):
        lines = []
        lines.append(urwid.AttrMap(urwid.Text("  Command              Keys"), "alt header"))
        for command, keys in self._conf["keybindings"].items():
            lines.append(urwid.Text(f"  {command:20} {', '.join(keys)}"))
        self.scrollable_dialog(
            lines,
            title="Press `ESC` to close this window, arrows to scroll",
        )

    def show_log(self):
        self.scrollable_dialog(
            "\n".join(self.window._log),
            title="Press `ESC` to close this window, arrows to scroll",
        )


class Window(urwid.Frame):
    """The main user interface frame.

    A window is a frame which displays a header, a footer and a body.
    The header and the footer are handled by this object, and the body
    is the space where Panes can be displayed.

    Each Pane is an interface to interact with the database in one
    way: list the VCards, edit one VCard, and so on. The Window
    provides a mechanism allowing the panes to chain themselves, and
    to carry data between them.
    """

    def __init__(self, footer="", quit_keys=None) -> None:
        quit_keys = quit_keys or ["q"]
        self._track: list[urwid.Overlay] = []

        header = urwid.AttrWrap(urwid.Text(""), "header")
        footer = urwid.AttrWrap(urwid.Text(footer), "footer")
        urwid.Frame.__init__(
            self,
            urwid.Text(""),
            header=header,
            footer=footer,
        )
        self.update_header()
        self._original_w = None
        self.quit_keys = quit_keys

        def alert(message):
            self.update_header(message, warn=True)

        self._alert_daemon = AlertDaemon(alert)
        self._alert_daemon.start()
        self.alert = self._alert_daemon.alert
        self.loop = None
        self._log: list[str] = []
        self._header_is_warning = False

    def open(self, pane, callback=None):
        """Open a new pane.

        The given pane is added to the track and opened. If the given
        callback is not None, it will be called when this new pane
        will be closed.
        """
        pane.window = self
        self._track.append((pane, callback))
        self._update(pane)

    def backtrack(self, data=None):
        """Unstack the displayed pane.

        The current pane is discarded, and the previous one is
        displayed. If the current pane was opened with a callback,
        this callback is called with the given data (if any) before
        the previous pane gets redrawn.
        """
        _old_pane, cb = self._track.pop()
        if cb:
            cb(data)

        if self._track:
            self._update(self._get_current_pane())
        else:
            raise urwid.ExitMainLoop()

    def is_top_level(self):
        """Is the current pane the top-level one?"""
        return len(self._track) == 1

    def on_key_press(self, key):
        """Handle application-wide key strokes."""
        if key in self.quit_keys:
            self.backtrack()
        elif key == "esc" and not self.is_top_level():
            self.backtrack()
        return key

    def _update(self, pane):
        self.set_body(pane)
        self.clear_header()

    def log(self, record: str):
        self._log.append(record)

    def _get_current_pane(self):
        return self._track[-1][0] if self._track else None

    def clear_header(self):
        """clears header if we are not currently showing a warning"""
        if not self._header_is_warning:
            pane_title = getattr(self._get_current_pane(), "title", "")
            self.header.w.set_text(pane_title)

    def update_header(self, alert=None, warn=False):
        """Update the Windows header line.

        :param alert: additional text to show in header, additionally to
            the current title. If `alert` is a tuple, the first entry must
            be a valid palette entry
        :type alert: str or (palette_entry, str)
        """
        self._header_is_warning = warn
        pane_title = getattr(self._get_current_pane(), "title", None)
        text = []

        for part in (pane_title, alert):
            if part:
                text.append(part)
                text.append(("black", " | "))

        self.header.w.set_text(text[:-1] or "")


class AlertDaemon(threading.Thread):
    def __init__(self, set_msg_func) -> None:
        threading.Thread.__init__(self)
        self._set_msg_func = set_msg_func
        self.daemon = True
        self._start_countdown = threading.Event()

    def alert(self, msg):
        self._set_msg_func(msg)
        self._start_countdown.set()

    def run(self):
        # This is a daemon thread. Since the global namespace is going to
        # vanish on interpreter shutdown, redefine everything from the global
        # namespace here.
        _sleep = time.sleep
        _exception = Exception
        _event = self._start_countdown
        _set_msg = self._set_msg_func

        while True:
            _event.wait()
            _sleep(3)
            try:
                _set_msg(None)
            except _exception:
                pass
            _event.clear()
