"""
Descriptors to define properties on your widget, screen, or App.

"""

from __future__ import annotations

from inspect import isclass
from typing import TYPE_CHECKING, Callable, Generic, TypeVar, overload

from textual._context import NoActiveAppError, active_app
from textual.css.query import NoMatches, QueryType, WrongType
from textual.widget import Widget

if TYPE_CHECKING:
    from textual.app import App
    from textual.dom import DOMNode
    from textual.message_pump import MessagePump


AppType = TypeVar("AppType", bound="App")


class app(Generic[AppType]):
    """Create a property to return the active app.

    All widgets have a default `app` property which returns an App instance.
    Type checkers will complain if you try to access attributes defined on your App class, which aren't
    present in the base class. To keep the type checker happy you can add this property to get your
    specific App subclass.

    Example:
        ```python
        class MyWidget(Widget):
            app = getters.app(MyApp)
        ```

    Args:
        app_type: The App subclass, or a callable which returns an App subclass.
    """

    def __init__(self, app_type: type[AppType] | Callable[[], type[AppType]]) -> None:
        self._app_type = app_type if isclass(app_type) else app_type()

    def __get__(self, obj: MessagePump, obj_type: type[MessagePump]) -> AppType:
        try:
            app = active_app.get()
        except LookupError:
            from textual.app import App

            node: MessagePump | None = obj
            while not isinstance(node, App):
                if node is None:
                    raise NoActiveAppError()
                node = node._parent
            app = node

        assert isinstance(app, self._app_type)
        return app


class query_one(Generic[QueryType]):
    """Create a query one property.

    A query one property calls [Widget.query_one][textual.dom.DOMNode.query_one] when accessed, and returns
    a widget. If the widget doesn't exist, then the property will raise the same exceptions as `Widget.query_one`.


    Example:
        ```python
        from textual import getters

        class MyScreen(screen):

            # Note this is at the class level
            output_log = getters.query_one("#output", RichLog)

            def compose(self) -> ComposeResult:
                with containers.Vertical():
                    yield RichLog(id="output")

            def on_mount(self) -> None:
                self.output_log.write("Screen started")
                # Equivalent to the following line:
                # self.query_one("#output", RichLog).write("Screen started")
        ```

    Args:
        selector: A TCSS selector, e.g. "#mywidget". Or a widget type, i.e. `Input`.
        expect_type: The type of the expected widget, e.g. `Input`, if the first argument is a selector.

    """

    selector: str
    expect_type: type["Widget"]

    @overload
    def __init__(self, selector: str) -> None:
        """

        Args:
            selector: A TCSS selector, e.g. "#mywidget"
        """

    @overload
    def __init__(self, selector: type[QueryType]) -> None: ...

    @overload
    def __init__(self, selector: str, expect_type: type[QueryType]) -> None: ...

    @overload
    def __init__(
        self, selector: type[QueryType], expect_type: type[QueryType]
    ) -> None: ...

    def __init__(
        self,
        selector: str | type[QueryType],
        expect_type: type[QueryType] | None = None,
    ) -> None:
        if expect_type is None:
            from textual.widget import Widget

            self.expect_type = Widget
        else:
            self.expect_type = expect_type
        if isinstance(selector, str):
            self.selector = selector
        else:
            self.selector = selector.__name__
            self.expect_type = selector

    @overload
    def __get__(
        self: "query_one[QueryType]", obj: DOMNode, obj_type: type[DOMNode]
    ) -> QueryType: ...

    @overload
    def __get__(
        self: "query_one[QueryType]", obj: None, obj_type: type[DOMNode]
    ) -> "query_one[QueryType]": ...

    def __get__(
        self: "query_one[QueryType]", obj: DOMNode | None, obj_type: type[DOMNode]
    ) -> QueryType | Widget | "query_one":
        """Get the widget matching the selector and/or type."""
        if obj is None:
            return self
        query_node = obj.query_one(self.selector, self.expect_type)
        return query_node


class child_by_id(Generic[QueryType]):
    """Create a child_by_id property, which returns the child with the given ID.

    This is similar using [query_one][textual.getters.query_one] with an id selector, except that
    only the immediate children are considered. It is also more efficient as it doesn't need to search the DOM.


    Example:
        ```python
        from textual import getters

        class MyScreen(screen):

            # Note this is at the class level
            output_log = getters.child_by_id("output", RichLog)

            def compose(self) -> ComposeResult:
                yield RichLog(id="output")

            def on_mount(self) -> None:
                self.output_log.write("Screen started")
        ```

    Args:
        child_id: The `id` of the widget to get (not a selector).
        expect_type: The type of the expected widget, e.g. `Input`.

    """

    child_id: str
    expect_type: type[Widget]

    @overload
    def __init__(self, child_id: str) -> None: ...

    @overload
    def __init__(self, child_id: str, expect_type: type[QueryType]) -> None: ...

    def __init__(
        self,
        child_id: str,
        expect_type: type[QueryType] | None = None,
    ) -> None:
        if expect_type is None:
            self.expect_type = Widget
        else:
            self.expect_type = expect_type
        self.child_id = child_id

    @overload
    def __get__(
        self: "child_by_id[QueryType]", obj: DOMNode, obj_type: type[DOMNode]
    ) -> QueryType: ...

    @overload
    def __get__(
        self: "child_by_id[QueryType]", obj: None, obj_type: type[DOMNode]
    ) -> "child_by_id[QueryType]": ...

    def __get__(
        self: "child_by_id[QueryType]", obj: DOMNode | None, obj_type: type[DOMNode]
    ) -> QueryType | Widget | "child_by_id":
        """Get the widget matching the selector and/or type."""
        if obj is None:
            return self
        child = obj._get_dom_base()._nodes._get_by_id(self.child_id)
        if child is None:
            raise NoMatches(f"No child found with id={self.child_id!r}")
        if not isinstance(child, self.expect_type):
            raise WrongType(
                f"Child with id={self.child_id!r} is the wrong type; expected type {self.expect_type.__name__!r}, found {child}"
            )
        return child
