# Copyright (c) 2019 Sean Vig
#
# 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.

"""
The objects in the command graph and command resolution on the objects
"""

from __future__ import annotations

import abc
import inspect
import sys
import traceback
from typing import TYPE_CHECKING

from libqtile.log_utils import logger

if TYPE_CHECKING:
    from typing import Callable, Optional

    from libqtile.command.graph import SelectorType

    ItemT = Optional[tuple[bool, list[str | int]]]


class SelectError(Exception):
    """Error raised in resolving a command graph object"""

    def __init__(self, err_string: str, name: str, selectors: list[SelectorType]):
        super().__init__("{}, name: {}, selectors: {}".format(err_string, name, selectors))
        self.name = name
        self.selectors = selectors


class CommandError(Exception):
    """Error raised in resolving a command"""


class CommandException(Exception):
    """Error raised while executing a command"""


class CommandObject(metaclass=abc.ABCMeta):
    """Base class for objects that expose commands

    Each command should be a method named `cmd_X`, where X is the command name.
    A CommandObject should also implement `._items()` and `._select()` methods
    (c.f. docstring for `.items()` and `.select()`).
    """

    def select(self, selectors: list[SelectorType]) -> CommandObject:
        """Return a selected object

        Recursively finds an object specified by a list of `(name, selector)`
        items.

        Raises SelectError if the object does not exist.
        """
        obj: CommandObject = self
        for name, selector in selectors:
            root, items = obj.items(name)
            # if non-root object and no selector given
            if root is False and selector is None:
                raise SelectError("", name, selectors)
            # if no items in container, but selector is given
            if items is None and selector is not None:
                raise SelectError("", name, selectors)
            # if selector is not in the list of contained items
            if items is not None and selector and selector not in items:
                raise SelectError("", name, selectors)

            maybe_obj = obj._select(name, selector)
            if maybe_obj is None:
                raise SelectError("", name, selectors)
            obj = maybe_obj
        return obj

    def items(self, name: str) -> tuple[bool, list[str | int] | None]:
        """Build a list of contained items for the given item class

        Returns a tuple `(root, items)` for the specified item class, where:

            root: True if this class accepts a "naked" specification without an
            item seletion (e.g. "layout" defaults to current layout), and False
            if it does not (e.g. no default "widget").

            items: a list of contained items
        """
        ret = self._items(name)
        if ret is None:
            # Not finding information for a particular item class is OK here;
            # we don't expect layouts to have a window, etc.
            return False, None
        return ret

    @abc.abstractmethod
    def _items(self, name) -> ItemT:
        """Generate the items for a given

        Same return as `.items()`. Return `None` if name is not a valid item
        class.
        """

    @abc.abstractmethod
    def _select(self, name: str, sel: str | int | None) -> CommandObject | None:
        """Select the given item of the given item class

        This method is called with the following guarantees:
            - `name` is a valid selector class for this item
            - `sel` is a valid selector for this item
            - the `(name, sel)` tuple is not an "impossible" combination (e.g. a
              selector is specified when `name` is not a containment object).

        Return None if no such object exists
        """

    def command(self, name: str) -> Callable | None:
        """Return the command with the given name

        Parameters
        ----------
        name: str
            The name of the command to fetch.

        Returns
        -------
        Callable
            The command function that can be invoked.
        """
        return getattr(self, "cmd_" + name, None)

    @property
    def commands(self) -> list[str]:
        """All of the commands on the given object"""
        cmds = [i[4:] for i in dir(self) if i.startswith("cmd_")]
        return cmds

    def cmd_commands(self) -> list[str]:
        """Returns a list of possible commands for this object

        Used by __qsh__ for command completion and online help
        """
        return self.commands

    def cmd_items(self, name) -> tuple[bool, list[str | int] | None]:
        """Returns a list of contained items for the specified name

        Used by __qsh__ to allow navigation of the object graph.
        """
        return self.items(name)

    def cmd_doc(self, name) -> str:
        """Returns the documentation for a specified command name

        Used by __qsh__ to provide online help.
        """
        if name in self.commands:
            command = self.command(name)
            assert command
            signature = self._get_command_signature(command)
            spec = name + signature
            htext = inspect.getdoc(command) or ""
            return spec + "\n" + htext
        raise CommandError("No such command: %s" % name)

    def _get_command_signature(self, command: Callable) -> str:
        signature = inspect.signature(command)
        args = list(signature.parameters)
        if args and args[0] == "self":
            args = args[1:]
            parameters = [signature.parameters[arg] for arg in args]
            signature = signature.replace(parameters=parameters)
        return str(signature)

    def cmd_eval(self, code: str) -> tuple[bool, str | None]:
        """Evaluates code in the module namespace of the command object

        Return value is tuple `(success, result)`, success being a boolean and
        result being a string representing the return value of eval, or None if
        exec was used instead.
        """
        try:
            globals_ = vars(sys.modules[self.__module__])
            try:
                return True, str(eval(code, globals_, locals()))
            except SyntaxError:
                exec(code, globals_, locals())
                return True, None
        except Exception:
            error = traceback.format_exc().strip().split("\n")[-1]
            return False, error

    def cmd_function(self, function, *args, **kwargs) -> None:
        """Call a function with current object as argument"""
        try:
            function(self, *args, **kwargs)
        except Exception:
            error = traceback.format_exc()
            logger.error('Exception calling "%s":\n%s', function, error)
