# -*- coding: utf-8 -*-
"""High level wrapper for a Resource.

This file is part of PyVISA.

:copyright: 2014-2022 by PyVISA Authors, see AUTHORS for more details.
:license: MIT, see LICENSE for more details.

"""
import contextlib
import time
from functools import update_wrapper
from typing import (
    Any,
    Callable,
    ContextManager,
    Iterator,
    Optional,
    Set,
    Type,
    TypeVar,
    Union,
    cast,
)

from typing_extensions import ClassVar, Literal

from .. import attributes, constants, errors, highlevel, logger, rname, typing, util
from ..attributes import Attribute
from ..events import Event
from ..typing import VISAEventContext, VISAHandler, VISASession


class WaitResponse:
    """Class used in return of wait_on_event.

    It properly closes the context upon delete.

    A call with event_type of 0 (normally used when timed_out is True) will store
    None as the event and event type, otherwise it records the proper Event.

    """

    #: Reference to the event object that was waited for.
    event: Event

    #: Status code returned by the VISA library
    ret: constants.StatusCode

    #: Did a timeout occurs
    timed_out: bool

    def __init__(
        self,
        event_type: constants.EventType,
        context: Optional[VISAEventContext],
        ret: constants.StatusCode,
        visalib: highlevel.VisaLibraryBase,
        timed_out: bool = False,
    ):
        self.event = Event(visalib, event_type, context)
        self._event_type = constants.EventType(event_type)
        self._context = context
        self.ret = ret
        self._visalib = visalib
        self.timed_out = timed_out

    def __del__(self) -> None:
        if self.event._context is not None:
            try:
                self._visalib.close(self.event._context)
                self.event.close()
            except errors.VisaIOError:
                pass


T = TypeVar("T", bound="Resource")


class Resource(object):
    """Base class for resources.

    Do not instantiate directly, use
    :meth:`pyvisa.highlevel.ResourceManager.open_resource`.

    """

    #: Reference to the resource manager used by this resource
    resource_manager: highlevel.ResourceManager

    #: Reference to the VISA library instance used by the resource
    visalib: highlevel.VisaLibraryBase

    #: VISA attribute descriptor classes that can be used to introspect the
    #: supported attributes and the possible values. The "often used" ones
    #: are generally directly available on the resource.
    visa_attributes_classes: ClassVar[Set[Type[attributes.Attribute]]]

    @classmethod
    def register(
        cls, interface_type: constants.InterfaceType, resource_class: str
    ) -> Callable[[Type[T]], Type[T]]:
        """Create a decorator to register a class.

        The class is associated to an interface type, resource class pair.

        Parameters
        ----------
        interface_type : constants.InterfaceType
            Interface type for which to register a wrapper class.
        resource_class : str
            Resource class for which to register a wrapper class.

        Returns
        -------
        Callable[[Type[T]], Type[T]]
            Decorator registering the class. Raises TypeError if some VISA
            attributes are missing on the registered class.

        """

        def _internal(python_class):

            highlevel.ResourceManager.register_resource_class(
                interface_type, resource_class, python_class
            )

            return python_class

        return _internal

    def __init__(
        self, resource_manager: highlevel.ResourceManager, resource_name: str
    ) -> None:
        self._resource_manager = resource_manager
        self.visalib = self._resource_manager.visalib

        # We store the resource name and use preferably the private attr over
        # the public descriptor internally because the public descriptor
        # requires a live instance the VISA library, which means it is much
        # slower but also can cause issue in error reporting when accessing the
        # repr
        self._resource_name: str
        try:
            # Attempt to normalize the resource name. Can fail for aliases
            self._resource_name = str(rname.ResourceName.from_string(resource_name))
        except rname.InvalidResourceName:
            self._resource_name = resource_name

        self._logging_extra = {
            "library_path": self.visalib.library_path,
            "resource_manager.session": self._resource_manager.session,
            "resource_name": self._resource_name,
            "session": None,
        }

        #: Session handle.
        self._session: Optional[VISASession] = None

    @property
    def session(self) -> VISASession:
        """Resource session handle.

        Raises
        ------
        errors.InvalidSession
            Raised if session is closed.

        """
        if self._session is None:
            raise errors.InvalidSession()
        return self._session

    @session.setter
    def session(self, value: Optional[VISASession]) -> None:
        self._session = value

    def __del__(self) -> None:
        if self._session is not None:
            self.close()

    def __str__(self) -> str:
        return "%s at %s" % (self.__class__.__name__, self._resource_name)

    def __repr__(self) -> str:
        return "<%r(%r)>" % (self.__class__.__name__, self._resource_name)

    def __enter__(self) -> "Resource":
        return self

    def __exit__(self, *args) -> None:
        self.close()

    @property
    def last_status(self) -> constants.StatusCode:
        """Last status code for this session."""
        return self.visalib.get_last_status_in_session(self.session)

    @property
    def resource_info(self) -> highlevel.ResourceInfo:
        """Get the extended information of this resource."""
        return self.visalib.parse_resource_extended(
            self._resource_manager.session, self._resource_name
        )[0]

    # --- VISA attributes --------------------------------------------------------------

    #: VISA attributes require the resource to be opened in order to get accessed.
    #: Please have a look at the attributes definition for more details

    #: Interface type of the given session.
    interface_type: Attribute[
        constants.InterfaceType
    ] = attributes.AttrVI_ATTR_INTF_TYPE()

    #: Board number for the given interface.
    interface_number: Attribute[int] = attributes.AttrVI_ATTR_INTF_NUM()

    #: Resource class (for example, "INSTR") as defined by the canonical resource name.
    resource_class: Attribute[str] = attributes.AttrVI_ATTR_RSRC_CLASS()

    #: Unique identifier for a resource compliant with the address structure.
    resource_name: Attribute[str] = attributes.AttrVI_ATTR_RSRC_NAME()

    #: Resource version that identifies the revisions or implementations of a resource.
    implementation_version: Attribute[int] = attributes.AttrVI_ATTR_RSRC_IMPL_VERSION()

    #: Current locking state of the resource.
    lock_state: Attribute[
        constants.AccessModes
    ] = attributes.AttrVI_ATTR_RSRC_LOCK_STATE()

    #: Version of the VISA specification to which the implementation is compliant.
    spec_version: Attribute[int] = attributes.AttrVI_ATTR_RSRC_SPEC_VERSION()

    #: Manufacturer name of the vendor that implemented the VISA library.
    resource_manufacturer_name: Attribute[str] = attributes.AttrVI_ATTR_RSRC_MANF_NAME()

    #: Timeout in milliseconds for all resource I/O operations.
    timeout: Attribute[float] = attributes.AttrVI_ATTR_TMO_VALUE()

    def ignore_warning(
        self, *warnings_constants: constants.StatusCode
    ) -> ContextManager:
        """Ignoring warnings context manager for the current resource.

        Parameters
        ----------
        warnings_constants : constants.StatusCode
            Constants identifying the warnings to ignore.

        """
        return self.visalib.ignore_warning(self.session, *warnings_constants)

    def open(
        self,
        access_mode: constants.AccessModes = constants.AccessModes.no_lock,
        open_timeout: int = 5000,
    ) -> None:
        """Opens a session to the specified resource.

        Parameters
        ----------
        access_mode : constants.AccessModes, optional
            Specifies the mode by which the resource is to be accessed.
            Defaults to constants.AccessModes.no_lock.
        open_timeout : int, optional
            If the ``access_mode`` parameter requests a lock, then this parameter
            specifies the absolute time period (in milliseconds) that the
            resource waits to get unlocked before this operation returns an error.
            Defaults to 5000.

        """
        logger.debug("%s - opening ...", self._resource_name, extra=self._logging_extra)
        with self._resource_manager.ignore_warning(
            constants.StatusCode.success_device_not_present
        ):
            self.session, status = self._resource_manager.open_bare_resource(
                self._resource_name, access_mode, open_timeout
            )

            if status == constants.StatusCode.success_device_not_present:
                # The device was not ready when we opened the session.
                # Now it gets five seconds more to become ready.
                # Every 0.1 seconds we probe it with viClear.
                start_time = time.time()
                sleep_time = 0.1
                try_time = 5
                while time.time() - start_time < try_time:
                    time.sleep(sleep_time)
                    try:
                        self.clear()
                        break
                    except errors.VisaIOError as error:
                        if error.error_code != constants.StatusCode.error_no_listeners:
                            raise

        self._logging_extra["session"] = self.session
        logger.debug(
            "%s - is open with session %s",
            self._resource_name,
            self.session,
            extra=self._logging_extra,
        )

    def before_close(self) -> None:
        """Called just before closing an instrument."""
        self.__switch_events_off()

    def close(self) -> None:
        """Closes the VISA session and marks the handle as invalid."""
        try:
            logger.debug("%s - closing", self._resource_name, extra=self._logging_extra)
            self.before_close()
            self.visalib.close(self.session)
            logger.debug(
                "%s - is closed", self._resource_name, extra=self._logging_extra
            )
            # Mypy is confused by the idea that we can set a value we cannot get
            self.session = None  # type: ignore
        except errors.InvalidSession:
            pass

    def __switch_events_off(self) -> None:
        """Switch off and discrads all events."""
        self.disable_event(
            constants.EventType.all_enabled, constants.EventMechanism.all
        )
        self.discard_events(
            constants.EventType.all_enabled, constants.EventMechanism.all
        )
        self.visalib.uninstall_all_visa_handlers(self.session)

    def get_visa_attribute(self, name: constants.ResourceAttribute) -> Any:
        """Retrieves the state of an attribute in this resource.

        One should prefer the dedicated descriptor for often used attributes
        since those perform checks and automatic conversion on the value.

        Parameters
        ----------
        name : constants.ResourceAttribute
            Resource attribute for which the state query is made.

        Returns
        -------
        Any
            The state of the queried attribute for a specified resource.

        """
        return self.visalib.get_attribute(self.session, name)[0]

    def set_visa_attribute(
        self, name: constants.ResourceAttribute, state: Any
    ) -> constants.StatusCode:
        """Set the state of an attribute.

        One should prefer the dedicated descriptor for often used attributes
        since those perform checks and automatic conversion on the value.

        Parameters
        ----------
        name : constants.ResourceAttribute
            Attribute for which the state is to be modified.
        state : Any
            The state of the attribute to be set for the specified object.

        Returns
        -------
        constants.StatusCode
            Return value of the library call.

        """
        return self.visalib.set_attribute(self.session, name, state)

    def clear(self) -> None:
        """Clear this resource."""
        self.visalib.clear(self.session)

    def install_handler(
        self, event_type: constants.EventType, handler: VISAHandler, user_handle=None
    ) -> Any:
        """Install handlers for event callbacks in this resource.

        Parameters
        ----------
        event_type : constants.EventType
            Logical event identifier.
        handler : VISAHandler
            Handler function to be installed by a client application.
        user_handle :
            A value specified by an application that can be used for identifying
            handlers uniquely for an event type. Depending on the backend they
            may be restriction on the possible values. Look at the backend
            `install_visa_handler` for more details.

        Returns
        -------
        Any
            User handle in a format amenable to the backend. This is this
            representation of the handle that should be used when unistalling
            a handler.

        """
        return self.visalib.install_visa_handler(
            self.session, event_type, handler, user_handle
        )

    def wrap_handler(
        self, callable: Callable[["Resource", Event, Any], None]
    ) -> VISAHandler:
        """Wrap an event handler to provide the signature expected by VISA.

        The handler is expected to have the following signature:
        handler(resource: Resource, event: Event, user_handle: Any) -> None.

        The wrapped handler should be used only to handle events on the resource
        used to wrap the handler.

        """

        def event_handler(
            session: VISASession,
            event_type: constants.EventType,
            event_context: typing.VISAEventContext,
            user_handle: Any,
        ) -> None:
            if session != self.session:
                raise RuntimeError(
                    "When wrapping a handler, the resource used to wrap the handler"
                    "must be the same on which the handler will be installed."
                    f"Wrapping session: {self.session}, event on session: {session}"
                )
            event = Event(self.visalib, event_type, event_context)
            try:
                return callable(self, event, user_handle)
            finally:
                event.close()

        update_wrapper(event_handler, callable)

        return event_handler

    def uninstall_handler(
        self, event_type: constants.EventType, handler: VISAHandler, user_handle=None
    ) -> None:
        """Uninstalls handlers for events in this resource.

        Parameters
        ----------
        event_type : constants.EventType
            Logical event identifier.
        handler : VISAHandler
            Handler function to be uninstalled by a client application.
        user_handle : Any
            The user handle returned by install_handler.

        """
        self.visalib.uninstall_visa_handler(
            self.session, event_type, handler, user_handle
        )

    def disable_event(
        self, event_type: constants.EventType, mechanism: constants.EventMechanism
    ) -> None:
        """Disable notification for an event type(s) via the specified mechanism(s).

        Parameters
        ----------
        event_type : constants.EventType
            Logical event identifier.
        mechanism : constants.EventMechanism
            Specifies event handling mechanisms to be disabled.

        """
        self.visalib.disable_event(self.session, event_type, mechanism)

    def discard_events(
        self, event_type: constants.EventType, mechanism: constants.EventMechanism
    ) -> None:
        """Discards event occurrences for an event type and mechanism in this resource.

        Parameters
        ----------
        event_type : constants.EventType
            Logical event identifier.
        mechanism : constants.EventMechanism
            Specifies event handling mechanisms to be disabled.

        """
        self.visalib.discard_events(self.session, event_type, mechanism)

    def enable_event(
        self,
        event_type: constants.EventType,
        mechanism: constants.EventMechanism,
        context: None = None,
    ) -> None:
        """Enable event occurrences for specified event types and mechanisms in this resource.

        Parameters
        ----------
        event_type : constants.EventType
            Logical event identifier.
        mechanism : constants.EventMechanism
            Specifies event handling mechanisms to be enabled
        context : None
            Not currently used, leave as None.

        """
        self.visalib.enable_event(self.session, event_type, mechanism, context)

    def wait_on_event(
        self,
        in_event_type: constants.EventType,
        timeout: int,
        capture_timeout: bool = False,
    ) -> WaitResponse:
        """Waits for an occurrence of the specified event in this resource.

        in_event_type : constants.EventType
            Logical identifier of the event(s) to wait for.
        timeout : int
            Absolute time period in time units that the resource shall wait for
            a specified event to occur before returning the time elapsed error.
            The time unit is in milliseconds. None means waiting forever if
            necessary.
        capture_timeout : bool, optional
            When True will not produce a VisaIOError(VI_ERROR_TMO) but instead
            return a WaitResponse with timed_out=True.

        Returns
        -------
        WaitResponse
            Object that contains event_type, context and ret value.

        """
        try:
            event_type, context, ret = self.visalib.wait_on_event(
                self.session, in_event_type, timeout
            )
        except errors.VisaIOError as exc:
            if capture_timeout and exc.error_code == constants.StatusCode.error_timeout:
                return WaitResponse(
                    in_event_type,
                    None,
                    constants.StatusCode.error_timeout,
                    self.visalib,
                    timed_out=True,
                )
            raise
        return WaitResponse(event_type, context, ret, self.visalib)

    def lock(
        self,
        timeout: Union[float, Literal["default"]] = "default",
        requested_key: Optional[str] = None,
    ) -> str:
        """Establish a shared lock to the resource.

        Parameters
        ----------
        timeout : Union[float, Literal["default"]], optional
            Absolute time period (in milliseconds) that a resource waits to get
            unlocked by the locking session before returning an error.
            Defaults to "default" which means use self.timeout.
        requested_key : Optional[str], optional
            Access key used by another session with which you want your session
            to share a lock or None to generate a new shared access key.

        Returns
        -------
        str
            A new shared access key if requested_key is None, otherwise, same
            value as the requested_key

        """
        tout = cast(float, self.timeout if timeout == "default" else timeout)
        clean_timeout = util.cleanup_timeout(tout)
        return self.visalib.lock(
            self.session, constants.Lock.shared, clean_timeout, requested_key
        )[0]

    def lock_excl(self, timeout: Union[float, Literal["default"]] = "default") -> None:
        """Establish an exclusive lock to the resource.

        Parameters
        ----------
        timeout : Union[float, Literal["default"]], optional
            Absolute time period (in milliseconds) that a resource waits to get
            unlocked by the locking session before returning an error.
            Defaults to "default" which means use self.timeout.

        """
        tout = cast(float, self.timeout if timeout == "default" else timeout)
        clean_timeout = util.cleanup_timeout(tout)
        self.visalib.lock(self.session, constants.Lock.exclusive, clean_timeout, None)

    def unlock(self) -> None:
        """Relinquishes a lock for the specified resource."""
        self.visalib.unlock(self.session)

    @contextlib.contextmanager
    def lock_context(
        self,
        timeout: Union[float, Literal["default"]] = "default",
        requested_key: Optional[str] = "exclusive",
    ) -> Iterator[Optional[str]]:
        """A context that locks

        Parameters
        ----------
        timeout : Union[float, Literal["default"]], optional
            Absolute time period (in milliseconds) that a resource waits to get
            unlocked by the locking session before returning an error.
            Defaults to "default" which means use self.timeout.
        requested_key : Optional[str], optional
            When using default of 'exclusive' the lock is an exclusive lock.
            Otherwise it is the access key for the shared lock or None to
            generate a new shared access key.

        Yields
        ------
        Optional[str]
            The access_key if applicable.

        """
        if requested_key == "exclusive":
            self.lock_excl(timeout)
            access_key = None
        else:
            access_key = self.lock(timeout, requested_key)
        try:
            yield access_key
        finally:
            self.unlock()


Resource.register(constants.InterfaceType.unknown, "")(Resource)
