
from types import TracebackType
from typing import Any, Callable, Dict, List, Optional, overload, Tuple, Type, TypeVar, Union
_T = TypeVar('_T')
_EventCB = Callable[[int, int, int, _T], None]
_EventAddHandleFunc = Callable[[int, int, _EventCB, _T], int]
_EventUpdateHandleFunc = Callable[[int, int], None]
_EventRemoveHandleFunc = Callable[[int], int]
_TimerCB = Callable[[int, _T], None]
_EventAddTimeoutFunc = Callable[[int, _TimerCB, _T], int]
_EventUpdateTimeoutFunc = Callable[[int, int], None]
_EventRemoveTimeoutFunc = Callable[[int], int]
_DomainCB = Callable[['virConnect', 'virDomain', int, int, _T], Optional[int]]
_BlkioParameter = Dict[str, Any]
_MemoryParameter = Dict[str, Any]
_SchedParameter = Dict[str, Any]
_TypedParameter = Dict[str, Any]


# The root of all libvirt errors.
class libvirtError(Exception):
    def __init__(self, defmsg: str) -> None:

        # Never call virConnGetLastError().
        # virGetLastError() is now thread local
        err = libvirtmod.virGetLastError()  # type: Optional[Tuple[int, int, str, int, str, Optional[str], Optional[str], int, int]]
        if err is None:
            msg = defmsg
        else:
            msg = err[2]

        Exception.__init__(self, msg)

        self.err = err

    def get_error_code(self) -> Optional[int]:
        if self.err is None:
            return None
        return self.err[0]

    def get_error_domain(self) -> Optional[int]:
        if self.err is None:
            return None
        return self.err[1]

    def get_error_message(self) -> Optional[str]:
        if self.err is None:
            return None
        return self.err[2]

    def get_error_level(self) -> Optional[int]:
        if self.err is None:
            return None
        return self.err[3]

    def get_str1(self) -> Optional[str]:
        if self.err is None:
            return None
        return self.err[4]

    def get_str2(self) -> Optional[str]:
        if self.err is None:
            return None
        return self.err[5]

    def get_str3(self) -> Optional[str]:
        if self.err is None:
            return None
        return self.err[6]

    def get_int1(self) -> Optional[int]:
        if self.err is None:
            return None
        return self.err[7]

    def get_int2(self) -> Optional[int]:
        if self.err is None:
            return None
        return self.err[8]


#
# register the libvirt global error handler
#
def registerErrorHandler(f: Callable[[_T, List], None], ctx: _T) -> int:
    """Register a Python function for error reporting.
       The function is called back as f(ctx, error), with error
       being a list of information about the error being raised.
       Returns 1 in case of success."""
    return libvirtmod.virRegisterErrorHandler(f, ctx)


def openAuth(uri: str, auth: List, flags: int = 0) -> 'virConnect':
    # TODO: The C code rquires a List and there is not *Mutable*Tuple for a better description such as
    # auth: Tuple[List[int], Callable[[List[MutableTuple[int, str, str, str, Any]], _T], int], _T]
    """
    This function should be called first to get a connection to the
    Hypervisor. If necessary, authentication will be performed fetching
    credentials via the callback.

    See :py:func:`open` for notes about environment variables which can
    have an effect on opening drivers and freeing the connection resources.

    :param str uri: (Optional) connection URI, see https://libvirt.org/uri.html
    :param auth: a list that contains 3 items:
        - a list of supported credential types
        - a callable that takes 2 arguments (credentials, user-data) and returns 0 on succcess and -1 on errors.
            The credentials argument is a list of credentials that libvirt (actually
            the ESX driver) would like to request. An element of this list is itself a
            list containing 5 items (4 inputs, 1 output):
                - the credential type, e.g. :py:const:`libvirt.VIR_CRED_AUTHNAME`
                - a prompt to be displayed to the user
                - a challenge, the ESX driver sets this to the hostname to allow automatic
                    distinction between requests for ESX and vCenter credentials
                - a default result for the request
                - a place to store the actual result for the request
        - user data that will be passed to the callable as second argument
    :param int flags: bitwise-OR of virConnectFlags
    :returns: a :py:class:`virConnect` instance on success.
    :raises libvirtError: on errors.
    """
    ret = libvirtmod.virConnectOpenAuth(uri, auth, flags)
    if ret is None:
        raise libvirtError('virConnectOpenAuth() failed')
    return virConnect(_obj=ret)


#
# Return library version.
#
def getVersion(name: Optional[str] = None) -> int:
    """If no name parameter is passed (or name is None) then the
    version of the libvirt library is returned as an integer.

    If a name is passed and it refers to a driver linked to the
    libvirt library, then this returns a tuple of (library version,
    driver version).

    If the name passed refers to a non-existent driver, then you
    will get the exception 'no support for hypervisor'.

    Versions numbers are integers: 1000000*major + 1000*minor + release."""
    if name is None:
        ret = libvirtmod.virGetVersion()
    else:
        ret = libvirtmod.virGetVersion(name)
    if ret is None:
        raise libvirtError("virGetVersion() failed")
    return ret


#
# Invoke an EventHandle callback
#
@overload
def _eventInvokeHandleCallback(watch: int, fd: int, event: int, opaque: Tuple[_EventCB, _T], opaquecompat: None = None) -> None: ...  # noqa E704
@overload  # noqa F811
def _eventInvokeHandleCallback(watch: int, fd: int, event: int, opaque: _EventCB, opaquecompat: _T = None) -> None: ...  # noqa E704
def _eventInvokeHandleCallback(watch: int, fd: int, event: int, opaque: Union[Tuple[_EventCB, _T], _EventCB], opaquecompat: Optional[_T] = None) -> None:  # noqa F811
    """
    Invoke the Event Impl Handle Callback in C
    """
    # libvirt 0.9.2 and earlier required custom event loops to know
    # that opaque=(cb, original_opaque) and pass the values individually
    # to this wrapper. This should handle the back compat case, and make
    # future invocations match the virEventHandleCallback prototype
    if opaquecompat:
        callback = opaque
        opaque_ = opaquecompat
    else:
        assert isinstance(opaque, tuple)
        callback = opaque[0]
        opaque_ = opaque[1]

    libvirtmod.virEventInvokeHandleCallback(watch, fd, event, callback, opaque_)


#
# Invoke an EventTimeout callback
#
def _eventInvokeTimeoutCallback(timer: int, opaque: Union[Tuple[_TimerCB, _T], _TimerCB], opaquecompat: Optional[_T] = None) -> None:
    """
    Invoke the Event Impl Timeout Callback in C
    """
    # libvirt 0.9.2 and earlier required custom event loops to know
    # that opaque=(cb, original_opaque) and pass the values individually
    # to this wrapper. This should handle the back compat case, and make
    # future invocations match the virEventTimeoutCallback prototype
    if opaquecompat:
        callback = opaque
        opaque_ = opaquecompat
    else:
        assert isinstance(opaque, tuple)
        callback = opaque[0]
        opaque_ = opaque[1]

    libvirtmod.virEventInvokeTimeoutCallback(timer, callback, opaque_)


def _dispatchEventHandleCallback(watch: int, fd: int, events: int, cbData: Dict[str, Any]) -> int:
    cb = cbData["cb"]
    opaque = cbData["opaque"]

    cb(watch, fd, events, opaque)
    return 0


def _dispatchEventTimeoutCallback(timer: int, cbData: Dict[str, Any]) -> int:
    cb = cbData["cb"]
    opaque = cbData["opaque"]

    cb(timer, opaque)
    return 0


def virEventAddHandle(fd: int, events: int, cb: _EventCB, opaque: _T) -> int:
    """
    register a callback for monitoring file handle events

    @fd: file handle to monitor for events
    @events: bitset of events to watch from virEventHandleType constants
    @cb: callback to invoke when an event occurs
    @opaque: user data to pass to callback

    Example callback prototype is:
        def cb(watch,   # int id of the handle
               fd,      # int file descriptor the event occurred on
               events,  # int bitmap of events that have occurred
               opaque): # opaque data passed to eventAddHandle
    """
    cbData = {"cb": cb, "opaque": opaque}
    ret = libvirtmod.virEventAddHandle(fd, events, cbData)
    if ret == -1:
        raise libvirtError('virEventAddHandle() failed')
    return ret


def virEventAddTimeout(timeout: int, cb: _TimerCB, opaque: _T) -> int:
    """
    register a callback for a timer event

    @timeout: time between events in milliseconds
    @cb: callback to invoke when an event occurs
    @opaque: user data to pass to callback

    Setting timeout to -1 will disable the timer. Setting the timeout
    to zero will cause it to fire on every event loop iteration.

    Example callback prototype is:
        def cb(timer,   # int id of the timer
               opaque): # opaque data passed to eventAddTimeout
    """
    cbData = {"cb": cb, "opaque": opaque}
    ret = libvirtmod.virEventAddTimeout(timeout, cbData)
    if ret == -1:
        raise libvirtError('virEventAddTimeout() failed')
    return ret


#
# a caller for the ff callbacks for custom event loop implementations
#

def virEventInvokeFreeCallback(opaque: Any) -> None:
    """
    Execute callback which frees the opaque buffer

    @opaque: the opaque object passed to addHandle or addTimeout

    WARNING: This function should not be called from any call by libvirt's
    core. It will most probably cause deadlock in C-level libvirt code.
    Instead it should be scheduled and called from implementation's stack.

    See https://libvirt.org/html/libvirt-libvirt-event.html#virEventAddHandleFunc
    for more information.

    This function is not dependent on any event loop implementation.
    """

    libvirtmod.virEventInvokeFreeCallback(opaque[2], opaque[1])
