"""The Window class.

This class is the main graphical content of any window.  With
[Dialog](dialog.md), chances are you won't need anything else as a
container for your windows.

The Window class contains the controls of your window, as user-defined
methods prefixed with `on`..  It can also contain data models and controls
of the contained widgets (graphical elements) in this window.  Optionally,
it can contain the window layout, though it's recommended to place this
layout in a separate [file](../layout/file.md).

"""

import inspect
from pathlib import Path
import sys
from typing import Optional, Sequence, Tuple, Type, Union

from bui.control.exceptions import StopControl
from bui.control.log import logger as control_logger
from bui.layout.parser import BUILayoutParser
from bui.tasks import cancel_all, run_remaining
from bui.widget.base import Widget
from bui import widget as wg

class MetaWindow(type):

    """
    Metaclass for the window class.

    It only traces the layout attribute for debugging purposes.

    """

    _marks = {}

    def __prepare__(name, bases, **kwargs):
        namespace = {
                "mark": MetaWindow.mark
        }

        # If one of the bases has a `close` method, put it in the namespace
        for base in bases:
            close = getattr(base, "close", None)
            if close:
                namespace["close"] = close
                namespace["stop_control"] = base.stop_control
                break

        return namespace

    @staticmethod
    def mark(layout: str) -> str:
        """Mark layout in the window."""
        frame = sys._getframe().f_back
        file = frame.f_globals["__file__"]
        line = frame.f_lineno
        line = line - len(layout.splitlines())
        qualname = frame.f_locals["__qualname__"]
        MetaWindow._marks[qualname] = (file, line)
        return layout


class Window(Widget, metaclass=MetaWindow):

    """
    The Window main class.

    Instanciating it (with `Window()`) will attempt to create and return an
    object of a specific GUI toolkit.  This object will inherit from `Window`
    and will have the exact same methods and attributes.  You should remain
    blessfully ignorant of the real class you use.  BUI is designed in such
    a way that you can ignore how it does its magic for a particular
    GUI toolkit, except if you're coding for such a toolkit.

    Class attributes:
        layout (str, optional): the optional window layout, if it is not
                defined in a separate [file](../layout/file.md).  It is
                recommended to not use this class attribute, but you still
                can do so, if you have your reasons.
        bui (str, optional): the path leading to the `.bui` file
                containing your layout.  By default, this is in the same
                folder and has the same name as the Python file containing
                your Window class, except it has the `.bui` extension.  If
                you use this naming convention, you don't have to override
                the `bui` class attribute.  Otherwise, give it a relative
                or absolute path: absolute paths consider their root to be
                the user directory when the Python application started
                (which might not be the same as the directory in which
                your Python file is contained).

    [Controls]../control/overview.md] are defined as methods on this
    class.  By convention, their name starts with `on_`, like `on_focus` or
    `on_quit`.  You can also watch for widget controls (execute a method
    when a button is pressed, for instance).  In this case, you can create
    `on_{control verb}_{control ID}` like `on_click_button`.  BUI will
    attempt to connect `on*` methods with controls and will tell you if
    it fails, or has a doubt.

    """

    widget = "window"
    class_name = "Window"
    default_controls = {
        "close": "The window is ready to be closed",
        "init": "The window is ready to be displayed",
        "press": "The user presses a key anywhere in the window",
        "release": "The user releases a key anywhere in the window",
        "right_click": "The user right clicks in the window",
        "type": "The user types a character anywhere in the window",
    }

    # Can be overrideen by subclasses
    layout = ""
    bui = ""

    # Class attributes, to be overridden by instance attributes
    parsed_layout = None
    _bui_parent = None

    def __init__(self, leaf):
        super().__init__(leaf)
        self.control_methods = {}
        self.title = leaf.title
        self.width = leaf.width
        self.height = leaf.height
        self._ids = {}

    def __getitem__(self, item):
        try:
            return self._ids[item]
        except KeyError:
            raise KeyError(f"{item!r} isn't a known or valid "
                    "widget identifier") from None

    @property
    def usable_surface(self):
        """
        Return the screen size that can be used, in pixels.

        This size is returned in a tuple: (width, height), so that
        (x, y) follow the exact same pattern.  Both components are integers.

        Note that this is the screen surface being "free", that is,
        not counting the taskbar on some operating systems, since
        we cannot draw on that.  Therefore, the usable surface tends
        to be somewhat narrower than the screen resolution.

        """
        return self.specific.usable_surface

    @classmethod
    def parse_layout(cls, Window, tag_name="window", **kwargs):
        """
        Determine where the layout is and try to parse it, return a window.

        Raises:
            ValueError: the layout couldn't be parsed.

        """
        if cls.layout:
            layout = cls.layout

            # Try to find the first line and the proper file,
            # since the layout is defined in code
            qualname = cls.__qualname__
            file, line = MetaWindow._marks.get(qualname, ("__unknown__", 0))
            parser = BUILayoutParser(file, line)
        else:
            bui = cls.bui
            if not bui:
                filename = Path(inspect.getsourcefile(cls))
                if filename.is_absolute():
                    relative = filename.relative_to(Path().absolute())
                    bui = f"{relative.parent}/{relative.stem}.bui"
                else:
                    bui = f"{filename.parent}/{filename.stem}.bui"

            with open(bui, 'r', encoding="utf-8") as file:
                layout = file.read()

            parser = BUILayoutParser(bui)

        parser.feed(layout)
        parsed_layout = parser.layout
        window_leaf = parsed_layout.get(tag_name)
        if window_leaf is None:
            raise ValueError("the specified layout doesn't contain a <window> description")

        # Creates all the leafs
        from bui.widget import WIDGETS
        widgets = []
        ids = {}
        for leaf in parsed_layout.flat:
            leaf.complete()
            if not leaf.has_widget:
                continue
            elif leaf is window_leaf:
                Generic = Window
            else:
                Generic = WIDGETS[leaf.tag_name]

            widget = Generic(leaf)
            if leaf is window_leaf:
                for key, value in kwargs.items():
                    setattr(widget, key, value)

            leaf.widget = widget
            widget.create_specific()
            widgets.append(widget)
            if leaf is window_leaf:
                window = widget

            widget_id = getattr(widget, "id", None)
            if widget_id:
                ids[widget_id] = widget

        window._ids = ids

        # Call the `_init` method on all generic widgets
        control_logger.debug("  Binding control methods...")

        for widget in widgets:
            widget._bind_controls(window)
            widget._init()

        window.parsed_layout = parsed_layout
        for widget in widgets:
            widget.specific.process_control(None, "init")

        return window

    def _init(self):
        """Private method to initialize the generic and specific window."""
        self.specific._init()

    def _start(self, loop):
        """
        Start the window, watch events and allow async loop.

        Args:
            loop (AsyncLoop): the asynchronous event loop (see asyncio).

        """
        self.specific.show()
        return self.specific._start(loop)

    def _stop(self):
        """Stop the window toolkit."""
        cancel_all()
        self._process_control("close")
        run_remaining()
        self.close()

    def close(self):
        """Close this window."""
        self.specific.close()

    def stop_control(self):
        """Stop the control, and the control method that called it."""
        raise StopControl()

    def pop_open_file(self, message: str, location: Path = None,
            filters: Sequence[Union[str, Tuple[str, str]]] = (),
            default: str = None, multiple: bool = False,
            preview: bool = True, hidden: bool = False
            ) -> Optional[Union[Path, Tuple[Path]]]:
        """
        Display a system dialog to select one or several files.

        This method displays a file system dialog, where the user can browse directories and select one or several files.  The selected file(s) will be returned if the user presses on the 'open' button in the dialog.  You can catch the result of this dialog to perform whatever operation you need.

        Args:
            message (str): the message to display to the user.
            location (Path, optional): if not set, use the current
                    directory.  Otherwise, you need to specify a
                    `pathlib.Path` object.
            filters (sequence): a sequence of filters to apply to the file
                    system list.  Each filter can be a string containing,
                    between parenthesis, the pattern to apply.  Optionally
                    a filter can be a tuple of two information:
                    the pattern, and what to display to the user.  See
                    the examples below of valid filters.
            default (str, optional): the default (selected) file, if any.
            multiple (bool, optional): allow to select several files
                    (default False).
            preview (bool, optional): display a previoew of the file (default
                    True).
            hidden (bool, optional): display hidden files (default False).

        Returns:
            If `multiple` is not set, returns either `None` or the
            selected file, as a `pathlib.Path` object.
            If `multiple` is set to `True`, returns either an empty tuple,
            or a tuple of selected files, where each file is a
            `pathlib.Path` object.
            Returning `None` or an empty tuple indicates the user
            cancelled the operation (pressed on the Cancel button in
            the file system dialog).

        """



    async def pop_alert(self, title: str, message: str,
            danger: Optional[str] = "error",
            ok: Optional[Union[bool, str]] = True,
            cancel: Optional[Union[bool, str]] = False,
            yes: Optional[Union[bool, str]] = False,
            no: Optional[Union[bool, str]] = False,
            default: Optional[str] = "ok"):
        """
        Display a default message box for inforiation or errors.

        Args:
            title (str): the dialog title.
            message (str): the message title, can be on several lines.
            danger (str): the type of the dialog which will influence
                    how noisy it is, what icon it displays and so on.
                    Possible values are:
                    "info": informative dialog, just to be polite.
                    "warning": warning message, danger increases.
                    "error": error message, probably can't go on.
                    "quesiton": just a question to ask the user.
            ok (bool or str, optional): should a OK butotn appear?
            cancel (bool or str, optional): should a cancel button appear?
            yes (bool or str, optional): should a yes butotn appear?
            no (bool or str, optional): should a no butotn appear?
            default (str, optional): the name of the default button.

        The button can either be set to True (only ok is set to True
        by default), or contain a string of the button label to display.  A button with False will not appear.  For isntance:

            answer = await self.pop_alert(..., danger="question",
                    ok="Go on anyway", cancel="Don't even try")
            if answer == "ok":
                # Go on

        Returns:
            clicked button (str): the clicked button as a string,
                    either "ok", "cancel", "yes", "no".
                    Even if a different label has been set for these
                    buttons, the string identifiers do not change.

        """
        buttons = {}
        if ok: buttons["ok"] = ok
        if cancel: buttons["cancel"] = cancel
        if yes: buttons["yes"] = yes
        if no: buttons["no"] = no
        if default not in buttons.keys():
            raise ValueError(f"{default!r} isn't in this alert box buttons")

        return await self.specific.pop_alert(title=title, message=message,
                danger=danger, buttons=buttons, default=default)

    async def pop_dialog(self, dialog: Union[str, Type['wg.dialog.Dialog']],
            **kwargs) -> 'wg.dialog.Dialog':
        """
        Pop up a custom dialog, blocks until the dialog has been closed.

        Args:
            dialog (str or Dialog): the dialog layout (as a str) or the
                    Dialog class to instantiate from.

        Returns:
            dialog (Dialog): the dialog object.  This object could
                    contain "filled" information by the user.

        """
        from bui.widget.dialog import Dialog
        if isinstance(dialog, str):
            class NewDialog(Dialog):
                layout = mark(dialog)
            dialog = NewDialog
        assert issubclass(dialog, wg.dialog.Dialog)
        dialog.window = self
        return await self.specific.pop_dialog(dialog, **kwargs)

    def pop_menu(self, context_id: str):
        """
        Pop a context menu, blocks until the menu is closed.

        Args:
            context_id (str): the registered ID of the context menu.

        """
        context = self[context_id]
        self.specific.pop_menu(context.specific)

    def open_window(self, window: "Window", child=False):
        """
        Open a new window.

        Args:
            window (Window): the window class.
            child (bool): if True, the new window will be a child of the
                    current window (closing self will close the new window).

        """
        if child:
            window._bui_parent = self

        self.specific.open_window(window, child=child)
        return window

    def handle_close(self, control):
        """The window closes."""
        pass
