"""dynamic dispatch objects for AX Script.

This is an IDispatch object that a scripting host may use to
query and invoke methods on the main script.  Not may hosts use
this yet, so it is not well tested!
"""

from __future__ import annotations

import types

import pythoncom
import win32com.server.policy
import win32com.server.util
import winerror
from win32com.client import Dispatch
from win32com.server.exception import COMException

debugging = 0

PyIDispatchType = pythoncom.TypeIIDs[pythoncom.IID_IDispatch]

# ignore hasattr(obj, "__call__") as this means all COM objects!
_CallableTypes = (types.FunctionType, types.MethodType)


class ScriptDispatch:
    _public_methods_: list[str] = []

    def __init__(self, engine, scriptNamespace):
        self.engine = engine
        self.scriptNamespace = scriptNamespace

    def _dynamic_(self, name, lcid, wFlags, args):
        # Ensure any newly added items are available.
        self.engine.RegisterNewNamedItems()
        self.engine.ProcessNewNamedItemsConnections()
        if wFlags & pythoncom.INVOKE_FUNC:
            # attempt to call a function
            try:
                func = getattr(self.scriptNamespace, name)
                if not isinstance(func, _CallableTypes):
                    raise AttributeError(name)  # Not a function.
                realArgs = []
                for arg in args:
                    if isinstance(arg, PyIDispatchType):
                        realArgs.append(Dispatch(arg))
                    else:
                        realArgs.append(arg)
                # xxx - todo - work out what code block to pass???
                return self.engine.ApplyInScriptedSection(None, func, tuple(realArgs))

            except AttributeError:
                if not wFlags & pythoncom.DISPATCH_PROPERTYGET:
                    raise COMException(scode=winerror.DISP_E_MEMBERNOTFOUND)
        if wFlags & pythoncom.DISPATCH_PROPERTYGET:
            # attempt to get a property
            try:
                ret = getattr(self.scriptNamespace, name)
                if isinstance(ret, _CallableTypes):
                    raise AttributeError(name)  # Not a property.
            except AttributeError:
                raise COMException(scode=winerror.DISP_E_MEMBERNOTFOUND)
            except COMException as instance:
                raise
            except:
                ret = self.engine.HandleException()
            return ret

        raise COMException(scode=winerror.DISP_E_MEMBERNOTFOUND)


class StrictDynamicPolicy(win32com.server.policy.DynamicPolicy):
    def _wrap_(self, object):
        win32com.server.policy.DynamicPolicy._wrap_(self, object)
        if hasattr(self._obj_, "scriptNamespace"):
            for name in dir(self._obj_.scriptNamespace):
                self._dyn_dispid_to_name_[self._getdispid_(name, 0)] = name

    def _getmembername_(self, dispid):
        try:
            return str(self._dyn_dispid_to_name_[dispid])
        except KeyError:
            raise COMException(scode=winerror.DISP_E_UNKNOWNNAME, desc="Name not found")

    def _getdispid_(self, name, fdex):
        try:
            func = getattr(self._obj_.scriptNamespace, str(name))
        except AttributeError:
            raise COMException(scode=winerror.DISP_E_MEMBERNOTFOUND)
        # if not isinstance(func, _CallableTypes):
        return win32com.server.policy.DynamicPolicy._getdispid_(self, name, fdex)


def _wrap(obj):
    useDispatcher = win32com.server.policy.DispatcherWin32trace if debugging else None
    return win32com.server.util.wrap(
        obj, usePolicy=StrictDynamicPolicy, useDispatcher=useDispatcher
    )


def MakeScriptDispatch(engine, namespace):
    return _wrap(ScriptDispatch(engine, namespace))
