from __future__ import annotations
from typing import TYPE_CHECKING, Any
from qcodes.metadatable import MetadatableWithName
from qcodes.validators import Validator, validate_all
from .command import Command
if TYPE_CHECKING:
    from collections.abc import Callable, Sequence
    from qcodes.instrument import InstrumentBase
[docs]
class Function(MetadatableWithName):
    """
    Defines a function  that an instrument can execute.
    This class is meant for simple cases, principally things that
    map to simple commands like ``*RST`` (reset) or those with just a few
    arguments.
    It requires a fixed argument count, and positional args
    only.
    You execute this function object like a normal function, or use its
    .call method.
    Note:
        Parsers only apply if call_cmd is a string. The function form of
        call_cmd should do its own parsing.
    Note:
        We do not recommend the usage of Function for any new driver.
        Function does not add any significant features over a method
        defined on the class.
    Args:
        name: the local name of this function
        instrument: an instrument that handles this
            function. Default None.
        call_cmd: command to execute on
            the instrument:
            - a string (with positional fields to .format, "{}" or "{0}" etc)
              you can only use a string if an instrument is provided,
              this string will be passed to instrument.write
            - a function (with arg count matching args list)
        args: list of Validator objects, one for
            each arg to the Function
        arg_parser: function to transform the input arg(s)
            to encoded value(s) sent to the instrument.  If there are multiple
            arguments, this function should accept all the arguments in order,
            and return a tuple of values.
        return_parser: function to transform the response
            from the instrument to the final output value.  may be a
            type casting function like `int` or `float`.  If None (default),
            will not wait for or read any response.
        docstring: documentation string for the __doc__
            field of the object. The __doc__ field of the instance is used by
            some help systems, but not all (particularly not builtin `help()`)
        **kwargs: Arbitrary keyword arguments passed to parent class
    """
    def __init__(
        self,
        name: str,
        instrument: InstrumentBase | None = None,
        call_cmd: str | Callable[..., Any] | None = None,
        args: Sequence[Validator[Any]] | None = None,
        arg_parser: Callable[..., Any] | None = None,
        return_parser: Callable[..., Any] | None = None,
        docstring: str | None = None,
        **kwargs: Any,
    ):
        super().__init__(**kwargs)
        self._instrument = instrument
        self.name = name
        if docstring is not None:
            self.__doc__ = docstring
        if args is None:
            args = []
        self._set_args(args)
        self._set_call(call_cmd, arg_parser, return_parser)
    def _set_args(self, args: Sequence[Validator[Any]]) -> None:
        for arg in args:
            if not isinstance(arg, Validator):
                raise TypeError("all args must be Validator objects")
        self._args = args
        self._arg_count = len(args)
    def _set_call(
        self,
        call_cmd: str | Callable[..., Any] | None,
        arg_parser: Callable[..., Any] | None,
        return_parser: Callable[..., Any] | None,
    ) -> None:
        if self._instrument:
            ask_or_write = self._instrument.write
            if isinstance(call_cmd, str) and return_parser:
                ask_or_write = self._instrument.ask
        else:
            ask_or_write = None
        self._call = Command(
            arg_count=self._arg_count,
            cmd=call_cmd,
            exec_str=ask_or_write,
            input_parser=arg_parser,
            output_parser=return_parser,
        )
[docs]
    def validate(self, *args: Any) -> None:
        """
        Check that all arguments to this Function are allowed.
        Args:
            *args: Variable length argument list, passed to the call_cmd
        """
        if self._instrument:
            func_name = (
                (
                    getattr(self._instrument, "name", "")
                    or str(self._instrument.__class__)
                )
                + "."
                + self.name
            )
        else:
            func_name = self.name
        if len(args) != self._arg_count:
            raise TypeError(
                f"{func_name} called with {len(args)} args but requires {self._arg_count}"
            )
        validate_all(*zip(self._args, args), context="Function: " + func_name) 
    def __call__(self, *args: Any) -> Any:
        self.validate(*args)
        return self._call(*args)
[docs]
    def call(self, *args: Any) -> Any:
        """
        Call methods wraps __call__
        Args:
           *args: argument to pass to Command __call__ function
        """
        return self.__call__(*args) 
[docs]
    def get_attrs(self) -> list[str]:
        """
        Attributes recreated as properties in the RemoteFunction proxy.
        Returns (list): __doc__, _args, and _arg_count get proxied
        """
        return ["__doc__", "_args", "_arg_count"] 
    @property
    def short_name(self) -> str:
        """
        Name excluding name of any instrument that this function may be
        bound to.
        """
        return self.name
    @property
    def full_name(self) -> str:
        """
        Name of the function including the name of the instrument and
        submodule that the function may be bound to. The names are separated
        by underscores, like this: ``instrument_submodule_function``.
        """
        return "_".join(self.name_parts)
    @property
    def name_parts(self) -> list[str]:
        """
        List of the parts that make up the full name of this function
        """
        if self.instrument is not None:
            name_parts = getattr(self.instrument, "name_parts", [])
            if name_parts == []:
                # add fallback for the case where someone has bound
                # the function to something that is not an instrument
                # but perhaps it has a name anyway?
                name = getattr(self.instrument, "name", None)
                if name is not None:
                    name_parts = [name]
        else:
            name_parts = []
        name_parts.append(self.short_name)
        return name_parts
    @property
    def instrument(self) -> InstrumentBase | None:
        return self._instrument