"""General Server side utilities"""

import pythoncom
import winerror

from . import policy
from .exception import COMException


def wrap(ob, iid=None, usePolicy=None, useDispatcher=None):
    """Wraps an object in a PyGDispatch gateway.

    Returns a client side PyI{iid} interface.

    Interface and gateway support must exist for the specified IID, as
    the QueryInterface() method is used.

    """
    if usePolicy is None:
        usePolicy = policy.DefaultPolicy
    if useDispatcher == 1:  # True will also work here.
        import win32com.server.dispatcher

        useDispatcher = win32com.server.dispatcher.DefaultDebugDispatcher
    if useDispatcher is None or useDispatcher == 0:
        ob = usePolicy(ob)
    else:
        ob = useDispatcher(usePolicy, ob)

    # get a PyIDispatch, which interfaces to PyGDispatch
    ob = pythoncom.WrapObject(ob)
    if iid is not None:
        ob = ob.QueryInterface(iid)  # Ask the PyIDispatch if it supports it?
    return ob


def unwrap(ob):
    """Unwraps an interface.

    Given an interface which wraps up a Gateway, return the object behind
    the gateway.
    """
    ob = pythoncom.UnwrapObject(ob)
    # see if the object is a dispatcher
    if hasattr(ob, "policy"):
        ob = ob.policy
    return ob._obj_


class ListEnumerator:
    """A class to expose a Python sequence as an EnumVARIANT.

    Create an instance of this class passing a sequence (list, tuple, or
    any sequence protocol supporting object) and it will automatically
    support the EnumVARIANT interface for the object.

    See also the @NewEnum@ function, which can be used to turn the
    instance into an actual COM server.
    """

    _public_methods_ = ["Next", "Skip", "Reset", "Clone"]

    def __init__(self, data, index=0, iid=pythoncom.IID_IEnumVARIANT):
        self._list_ = data
        self.index = index
        self._iid_ = iid

    def _query_interface_(self, iid):
        if iid == self._iid_:
            return 1

    def Next(self, count):
        result = self._list_[self.index : self.index + count]
        self.Skip(count)
        return result

    def Skip(self, count):
        end = self.index + count
        if end > len(self._list_):
            end = len(self._list_)
        self.index = end

    def Reset(self):
        self.index = 0

    def Clone(self):
        return self._wrap(self.__class__(self._list_, self.index))

    def _wrap(self, ob):
        return wrap(ob)


class ListEnumeratorGateway(ListEnumerator):
    """A List Enumerator which wraps a sequence's items in gateways.

    If a sequence contains items (objects) that have not been wrapped for
    return through the COM layers, then a ListEnumeratorGateway can be
    used to wrap those items before returning them (from the Next() method).

    See also the @ListEnumerator@ class and the @NewEnum@ function.
    """

    def Next(self, count):
        result = self._list_[self.index : self.index + count]
        self.Skip(count)
        return list(map(self._wrap, result))


def NewEnum(
    seq,
    cls=ListEnumerator,
    iid=pythoncom.IID_IEnumVARIANT,
    usePolicy=None,
    useDispatcher=None,
):
    """Creates a new enumerator COM server.

    This function creates a new COM Server that implements the
    IID_IEnumVARIANT interface.

    A COM server that can enumerate the passed in sequence will be
    created, then wrapped up for return through the COM framework.
    Optionally, a custom COM server for enumeration can be passed
    (the default is @ListEnumerator@), and the specific IEnum
    interface can be specified.
    """
    ob = cls(seq, iid=iid)
    return wrap(ob, iid, usePolicy=usePolicy, useDispatcher=useDispatcher)


class Collection:
    "A collection of VARIANT values."

    _public_methods_ = ["Item", "Count", "Add", "Remove", "Insert"]

    def __init__(self, data=None, readOnly=0):
        if data is None:
            data = []
        self.data = data

        # disable Add/Remove if read-only. note that we adjust _public_methods_
        # on this instance only.
        if readOnly:
            self._public_methods_ = ["Item", "Count"]

    # This method is also used as the "default" method.
    # Thus "print(ob)" will cause this to be called with zero
    # params.  Handle this slightly more elegantly here.
    # Ideally the  policy should handle this.
    def Item(self, *args):
        if len(args) != 1:
            raise COMException(scode=winerror.DISP_E_BADPARAMCOUNT)

        try:
            return self.data[args[0]]
        except IndexError as desc:
            raise COMException(scode=winerror.DISP_E_BADINDEX, desc=str(desc))

    _value_ = Item

    def Count(self):
        return len(self.data)

    def Add(self, value):
        self.data.append(value)

    def Remove(self, index):
        try:
            del self.data[index]
        except IndexError as desc:
            raise COMException(scode=winerror.DISP_E_BADINDEX, desc=str(desc))

    def Insert(self, index, value):
        try:
            index = int(index)
        except (ValueError, TypeError):
            raise COMException(scode=winerror.DISP_E_TYPEMISMATCH)
        self.data.insert(index, value)

    def _NewEnum(self):
        return NewEnum(self.data)


def NewCollection(seq, cls=Collection):
    """Creates a new COM collection object

    This function creates a new COM Server that implements the
    common collection protocols, including enumeration. (_NewEnum)

    A COM server that can enumerate the passed in sequence will be
    created, then wrapped up for return through the COM framework.
    Optionally, a custom COM server for enumeration can be passed
    (the default is @Collection@).
    """
    return pythoncom.WrapObject(
        policy.DefaultPolicy(cls(seq)), pythoncom.IID_IDispatch, pythoncom.IID_IDispatch
    )


class FileStream:
    _public_methods_ = ["Read", "Write", "Clone", "CopyTo", "Seek"]
    _com_interfaces_ = [pythoncom.IID_IStream]

    def __init__(self, file):
        self.file = file

    def Read(self, amount):
        return self.file.read(amount)

    def Write(self, data):
        self.file.write(data)
        return len(data)

    def Clone(self):
        return self._wrap(self.__class__(self.file))

    def CopyTo(self, dest, cb):
        data = self.file.read(cb)
        cbread = len(data)
        dest.Write(data)  ## ??? Write does not currently return the length ???
        return cbread, cbread

    def Seek(self, offset, origin):
        # how convient that the 'origin' values are the same as the CRT :)
        self.file.seek(offset, origin)
        return self.file.tell()

    def _wrap(self, ob):
        return wrap(ob)
