"""Utilities for makegw - Parse a header file to build an interface

This module contains the core code for parsing a header file describing a
COM interface, and building it into an "Interface" structure.

Each Interface has methods, and each method has arguments.

Each argument knows how to use Py_BuildValue or Py_ParseTuple to
exchange itself with Python.

See the @win32com.makegw@ module for information in building a COM
interface
"""

from __future__ import annotations

import re
import traceback


class error_not_found(Exception):
    def __init__(self, msg="The requested item could not be found"):
        super().__init__(msg)


class error_not_supported(Exception):
    def __init__(self, msg="The required functionality is not supported"):
        super().__init__(msg)


VERBOSE = 0
DEBUG = 0

## NOTE : For interfaces as params to work correctly, you must
## make sure any PythonCOM extensions which expose the interface are loaded
## before generating.


class ArgFormatter:
    """An instance for a specific type of argument. Knows how to convert itself"""

    def __init__(self, arg, builtinIndirection, declaredIndirection=0):
        # print("init:", arg.name, builtinIndirection, declaredIndirection, arg.indirectionLevel)
        self.arg = arg
        self.builtinIndirection = builtinIndirection
        self.declaredIndirection = declaredIndirection
        self.gatewayMode = 0

    def _IndirectPrefix(self, indirectionFrom, indirectionTo):
        """Given the indirection level I was declared at (0=Normal, 1=*, 2=**)
        return a string prefix so I can pass to a function with the
        required indirection (where the default is the indirection of the method's param.

        eg, assuming my arg has indirection level of 2, if this function was passed 1
        it would return "&", so that a variable declared with indirection of 1
        can be prefixed with this to turn it into the indirection level required of 2
        """
        dif = indirectionFrom - indirectionTo
        if dif == 0:
            return ""
        elif dif == -1:
            return "&"
        elif dif == 1:
            return "*"
        else:
            return "?? (%d)" % (dif,)
            raise error_not_supported("Can't indirect this far - please fix me :-)")

    def GetIndirectedArgName(self, indirectFrom, indirectionTo):
        # print(
        #     "get:",
        #     self.arg.name,
        #     indirectFrom,
        #     self._GetDeclaredIndirection() + self.builtinIndirection,
        #     indirectionTo,
        #     self.arg.indirectionLevel,
        # )

        if indirectFrom is None:
            ### ACK! this does not account for [in][out] variables.
            ### when this method is called, we need to know which
            indirectFrom = self._GetDeclaredIndirection() + self.builtinIndirection

        return self._IndirectPrefix(indirectFrom, indirectionTo) + self.arg.name

    def GetBuildValueArg(self):
        "Get the argument to be passes to Py_BuildValue"
        return self.arg.name

    def GetParseTupleArg(self):
        "Get the argument to be passed to PyArg_ParseTuple"
        if self.gatewayMode:
            # use whatever they were declared with
            return self.GetIndirectedArgName(None, 1)
        # local declarations have just their builtin indirection
        return self.GetIndirectedArgName(self.builtinIndirection, 1)

    def GetInterfaceCppObjectInfo(self):
        """Provide information about the C++ object used.

        Simple variables (such as integers) can declare their type (eg an integer)
        and use it as the target of both PyArg_ParseTuple and the COM function itself.

        More complex types require a PyObject * declared as the target of PyArg_ParseTuple,
        then some conversion routine to the C++ object which is actually passed to COM.

        This method provides the name, and optionally the type of that C++ variable.
        If the type if provided, the caller will likely generate a variable declaration.
        The name must always be returned.

        Result is a tuple of (variableName, [DeclareType|None|""])
        """

        # the first return element is the variable to be passed as
        # 	 an argument to an interface method. the variable was
        # 	 declared with only its builtin indirection level. when
        # 	 we pass it, we'll need to pass in whatever amount of
        # 	 indirection was applied (plus the builtin amount)
        # the second return element is the variable declaration; it
        # 	 should simply be builtin indirection
        return (
            self.GetIndirectedArgName(
                self.builtinIndirection,
                self.arg.indirectionLevel + self.builtinIndirection,
            ),
            f"{self.GetUnconstType()} {self.arg.name}",
        )

    def GetInterfaceArgCleanup(self):
        "Return cleanup code for C++ args passed to the interface method."
        if DEBUG:
            return "/* GetInterfaceArgCleanup output goes here: %s */\n" % self.arg.name
        else:
            return ""

    def GetInterfaceArgCleanupGIL(self):
        """Return cleanup code for C++ args passed to the interface
        method that must be executed with the GIL held"""
        if DEBUG:
            return (
                "/* GetInterfaceArgCleanup (GIL held) output goes here: %s */\n"
                % self.arg.name
            )
        else:
            return ""

    def GetUnconstType(self):
        return self.arg.unc_type

    def SetGatewayMode(self):
        self.gatewayMode = 1

    def _GetDeclaredIndirection(self):
        return self.arg.indirectionLevel
        print("declared:", self.arg.name, self.gatewayMode)
        if self.gatewayMode:
            return self.arg.indirectionLevel
        else:
            return self.declaredIndirection

    def DeclareParseArgTupleInputConverter(self):
        "Declare the variable used as the PyArg_ParseTuple param for a gateway"
        # Only declare it??
        # if self.arg.indirectionLevel==0:
        # 	return "\t%s %s;\n" % (self.arg.type, self.arg.name)
        # else:
        if DEBUG:
            return (
                "/* Declare ParseArgTupleInputConverter goes here: %s */\n"
                % self.arg.name
            )
        else:
            return ""

    def GetParsePostCode(self):
        "Get a string of C++ code to be executed after (ie, to finalise) the PyArg_ParseTuple conversion"
        if DEBUG:
            return "/* GetParsePostCode code goes here: %s */\n" % self.arg.name
        else:
            return ""

    def GetBuildForInterfacePreCode(self):
        "Get a string of C++ code to be executed before (ie, to initialise) the Py_BuildValue conversion for Interfaces"
        if DEBUG:
            return "/* GetBuildForInterfacePreCode goes here: %s */\n" % self.arg.name
        else:
            return ""

    def GetBuildForGatewayPreCode(self):
        "Get a string of C++ code to be executed before (ie, to initialise) the Py_BuildValue conversion for Gateways"
        s = self.GetBuildForInterfacePreCode()  # Usually the same
        if DEBUG:
            if s[:4] == "/* G":
                s = "/* GetBuildForGatewayPreCode goes here: %s */\n" % self.arg.name
        return s

    def GetBuildForInterfacePostCode(self):
        "Get a string of C++ code to be executed after (ie, to finalise) the Py_BuildValue conversion for Interfaces"
        if DEBUG:
            return "/* GetBuildForInterfacePostCode goes here: %s */\n" % self.arg.name
        return ""

    def GetBuildForGatewayPostCode(self):
        "Get a string of C++ code to be executed after (ie, to finalise) the Py_BuildValue conversion for Gateways"
        s = self.GetBuildForInterfacePostCode()  # Usually the same
        if DEBUG:
            if s[:4] == "/* G":
                s = "/* GetBuildForGatewayPostCode goes here: %s */\n" % self.arg.name
        return s

    def GetAutoduckString(self):
        return "// @pyparm {}|{}||Description for {}".format(
            self._GetPythonTypeDesc(),
            self.arg.name,
            self.arg.name,
        )

    def _GetPythonTypeDesc(self):
        "Returns a string with the description of the type. Used for doco purposes"
        return None

    def NeedUSES_CONVERSION(self):
        "Determines if this arg forces a USES_CONVERSION macro"
        return 0


# Special formatter for floats since they're smaller than Python floats.
class ArgFormatterFloat(ArgFormatter):
    def GetFormatChar(self):
        return "f"

    def DeclareParseArgTupleInputConverter(self):
        # Declare a double variable
        return "\tdouble dbl%s;\n" % self.arg.name

    def GetParseTupleArg(self):
        return "&dbl" + self.arg.name

    def _GetPythonTypeDesc(self):
        return "float"

    def GetBuildValueArg(self):
        return "&dbl" + self.arg.name

    def GetBuildForInterfacePreCode(self):
        return "\tdbl" + self.arg.name + " = " + self.arg.name + ";\n"

    def GetBuildForGatewayPreCode(self):
        return (
            "\tdbl%s = " % self.arg.name
            + self._IndirectPrefix(self._GetDeclaredIndirection(), 0)
            + self.arg.name
            + ";\n"
        )

    def GetParsePostCode(self):
        s = "\t"
        if self.gatewayMode:
            s += self._IndirectPrefix(self._GetDeclaredIndirection(), 0)
        s += self.arg.name
        s += " = (float)dbl%s;\n" % self.arg.name
        return s


# Special formatter for Shorts because they're
# a different size than Python ints!
class ArgFormatterShort(ArgFormatter):
    def GetFormatChar(self):
        return "i"

    def DeclareParseArgTupleInputConverter(self):
        # Declare a double variable
        return "\tINT i%s;\n" % self.arg.name

    def GetParseTupleArg(self):
        return "&i" + self.arg.name

    def _GetPythonTypeDesc(self):
        return "int"

    def GetBuildValueArg(self):
        return "&i" + self.arg.name

    def GetBuildForInterfacePreCode(self):
        return "\ti" + self.arg.name + " = " + self.arg.name + ";\n"

    def GetBuildForGatewayPreCode(self):
        return (
            "\ti%s = " % self.arg.name
            + self._IndirectPrefix(self._GetDeclaredIndirection(), 0)
            + self.arg.name
            + ";\n"
        )

    def GetParsePostCode(self):
        s = "\t"
        if self.gatewayMode:
            s += self._IndirectPrefix(self._GetDeclaredIndirection(), 0)
        s += self.arg.name
        s += " = i%s;\n" % self.arg.name
        return s


# for types which are 64bits on AMD64 - eg, HWND
class ArgFormatterLONG_PTR(ArgFormatter):
    def GetFormatChar(self):
        return "O"

    def DeclareParseArgTupleInputConverter(self):
        # Declare a PyObject variable
        return "\tPyObject *ob%s;\n" % self.arg.name

    def GetParseTupleArg(self):
        return "&ob" + self.arg.name

    def _GetPythonTypeDesc(self):
        return "int/long"

    def GetBuildValueArg(self):
        return "ob" + self.arg.name

    def GetBuildForInterfacePostCode(self):
        return "\tPy_XDECREF(ob%s);\n" % self.arg.name

    def GetParsePostCode(self):
        return "\tif (bPythonIsHappy && !PyWinLong_AsULONG_PTR(ob{}, (ULONG_PTR *){})) bPythonIsHappy = FALSE;\n".format(
            self.arg.name, self.GetIndirectedArgName(None, 2)
        )

    def GetBuildForInterfacePreCode(self):
        notdirected = self.GetIndirectedArgName(None, 1)
        return f"\tob{self.arg.name} = PyWinObject_FromULONG_PTR({notdirected});\n"

    def GetBuildForGatewayPostCode(self):
        return "\tPy_XDECREF(ob%s);\n" % self.arg.name


class ArgFormatterPythonCOM(ArgFormatter):
    """An arg formatter for types exposed in the PythonCOM module"""

    def GetFormatChar(self):
        return "O"

    # def GetInterfaceCppObjectInfo(self):
    # 	return ArgFormatter.GetInterfaceCppObjectInfo(self)[0], \
    # 		"%s %s%s" % (self.arg.unc_type, "*" * self._GetDeclaredIndirection(), self.arg.name)
    def DeclareParseArgTupleInputConverter(self):
        # Declare a PyObject variable
        return "\tPyObject *ob%s;\n" % self.arg.name

    def GetParseTupleArg(self):
        return "&ob" + self.arg.name

    def _GetPythonTypeDesc(self):
        return "<o Py%s>" % self.arg.type

    def GetBuildValueArg(self):
        return "ob" + self.arg.name

    def GetBuildForInterfacePostCode(self):
        return "\tPy_XDECREF(ob%s);\n" % self.arg.name


class ArgFormatterBSTR(ArgFormatterPythonCOM):
    def _GetPythonTypeDesc(self):
        return "<o unicode>"

    def GetParsePostCode(self):
        return "\tif (bPythonIsHappy && !PyWinObject_AsBstr(ob{}, {})) bPythonIsHappy = FALSE;\n".format(
            self.arg.name, self.GetIndirectedArgName(None, 2)
        )

    def GetBuildForInterfacePreCode(self):
        notdirected = self.GetIndirectedArgName(None, 1)
        return f"\tob{self.arg.name} = MakeBstrToObj({notdirected});\n"

    def GetBuildForInterfacePostCode(self):
        return (
            f"\tSysFreeString({self.arg.name});\n"
            + ArgFormatterPythonCOM.GetBuildForInterfacePostCode(self)
        )

    def GetBuildForGatewayPostCode(self):
        return "\tPy_XDECREF(ob%s);\n" % self.arg.name


class ArgFormatterOLECHAR(ArgFormatterPythonCOM):
    def _GetPythonTypeDesc(self):
        return "<o unicode>"

    def GetUnconstType(self):
        if self.arg.type[:3] == "LPC":
            return self.arg.type[:2] + self.arg.type[3:]
        else:
            return self.arg.unc_type

    def GetParsePostCode(self):
        return "\tif (bPythonIsHappy && !PyWinObject_AsBstr(ob{}, {})) bPythonIsHappy = FALSE;\n".format(
            self.arg.name, self.GetIndirectedArgName(None, 2)
        )

    def GetInterfaceArgCleanup(self):
        return "\tSysFreeString(%s);\n" % self.GetIndirectedArgName(None, 1)

    def GetBuildForInterfacePreCode(self):
        # the variable was declared with just its builtin indirection
        notdirected = self.GetIndirectedArgName(self.builtinIndirection, 1)
        return f"\tob{self.arg.name} = MakeOLECHARToObj({notdirected});\n"

    def GetBuildForInterfacePostCode(self):
        # memory returned into an OLECHAR should be freed
        return (
            f"\tCoTaskMemFree({self.arg.name});\n"
            + ArgFormatterPythonCOM.GetBuildForInterfacePostCode(self)
        )

    def GetBuildForGatewayPostCode(self):
        return "\tPy_XDECREF(ob%s);\n" % self.arg.name


class ArgFormatterTCHAR(ArgFormatterPythonCOM):
    def _GetPythonTypeDesc(self):
        return "string/<o unicode>"

    def GetUnconstType(self):
        if self.arg.type[:3] == "LPC":
            return self.arg.type[:2] + self.arg.type[3:]
        else:
            return self.arg.unc_type

    def GetParsePostCode(self):
        return "\tif (bPythonIsHappy && !PyWinObject_AsTCHAR(ob{}, {})) bPythonIsHappy = FALSE;\n".format(
            self.arg.name, self.GetIndirectedArgName(None, 2)
        )

    def GetInterfaceArgCleanup(self):
        return "\tPyWinObject_FreeTCHAR(%s);\n" % self.GetIndirectedArgName(None, 1)

    def GetBuildForInterfacePreCode(self):
        # the variable was declared with just its builtin indirection
        notdirected = self.GetIndirectedArgName(self.builtinIndirection, 1)
        return f"\tob{self.arg.name} = PyWinObject_FromTCHAR({notdirected});\n"

    def GetBuildForInterfacePostCode(self):
        return "// ??? - TCHAR post code\n"

    def GetBuildForGatewayPostCode(self):
        return "\tPy_XDECREF(ob%s);\n" % self.arg.name


class ArgFormatterIID(ArgFormatterPythonCOM):
    def _GetPythonTypeDesc(self):
        return "<o PyIID>"

    def GetParsePostCode(self):
        return "\tif (!PyWinObject_AsIID(ob{}, &{})) bPythonIsHappy = FALSE;\n".format(
            self.arg.name,
            self.arg.name,
        )

    def GetBuildForInterfacePreCode(self):
        # 		notdirected = self.GetIndirectedArgName(self.arg.indirectionLevel, 0)
        notdirected = self.GetIndirectedArgName(None, 0)
        return f"\tob{self.arg.name} = PyWinObject_FromIID({notdirected});\n"

    def GetInterfaceCppObjectInfo(self):
        return self.arg.name, "IID %s" % (self.arg.name)


class ArgFormatterTime(ArgFormatterPythonCOM):
    def __init__(self, arg, builtinIndirection, declaredIndirection=0):
        # we don't want to declare LPSYSTEMTIME / LPFILETIME objects
        if arg.indirectionLevel == 0 and arg.unc_type[:2] == "LP":
            arg.unc_type = arg.unc_type[2:]
            # reduce the builtin and increment the declaration
            arg.indirectionLevel += 1
            builtinIndirection = 0
        ArgFormatterPythonCOM.__init__(
            self, arg, builtinIndirection, declaredIndirection
        )

    def _GetPythonTypeDesc(self):
        return "<o PyDateTime>"

    def GetParsePostCode(self):
        # variable was declared with only the builtinIndirection
        ### NOTE: this is an [in] ... so use only builtin
        return '\tif (!PyTime_Check(ob{})) {{\n\t\tPyErr_SetString(PyExc_TypeError, "The argument must be a PyTime object");\n\t\tbPythonIsHappy = FALSE;\n\t}}\n\tif (!((PyTime *)ob{})->GetTime({})) bPythonIsHappy = FALSE;\n'.format(
            self.arg.name,
            self.arg.name,
            self.GetIndirectedArgName(self.builtinIndirection, 1),
        )

    def GetBuildForInterfacePreCode(self):
        ### use just the builtinIndirection again...
        notdirected = self.GetIndirectedArgName(self.builtinIndirection, 0)
        return f"\tob{self.arg.name} = new PyTime({notdirected});\n"

    def GetBuildForInterfacePostCode(self):
        ### hack to determine if we need to free stuff
        ret = ""
        if self.builtinIndirection + self.arg.indirectionLevel > 1:
            # memory returned into an OLECHAR should be freed
            ret = "\tCoTaskMemFree(%s);\n" % self.arg.name
        return ret + ArgFormatterPythonCOM.GetBuildForInterfacePostCode(self)


class ArgFormatterSTATSTG(ArgFormatterPythonCOM):
    def _GetPythonTypeDesc(self):
        return "<o STATSTG>"

    def GetParsePostCode(self):
        return "\tif (!PyCom_PyObjectAsSTATSTG(ob{}, {}, 0/*flags*/)) bPythonIsHappy = FALSE;\n".format(
            self.arg.name, self.GetIndirectedArgName(None, 1)
        )

    def GetBuildForInterfacePreCode(self):
        notdirected = self.GetIndirectedArgName(None, 1)
        return "\tob{} = PyCom_PyObjectFromSTATSTG({});\n\t// STATSTG doco says our responsibility to free\n\tif (({}).pwcsName) CoTaskMemFree(({}).pwcsName);\n".format(
            self.arg.name,
            self.GetIndirectedArgName(None, 1),
            notdirected,
            notdirected,
        )


class ArgFormatterGeneric(ArgFormatterPythonCOM):
    def _GetPythonTypeDesc(self):
        return "<o %s>" % self.arg.type

    def GetParsePostCode(self):
        return "\tif (!PyObject_As{}(ob{}, &{}) bPythonIsHappy = FALSE;\n".format(
            self.arg.type,
            self.arg.name,
            self.GetIndirectedArgName(None, 1),
        )

    def GetInterfaceArgCleanup(self):
        return f"\tPyObject_Free{self.arg.type}({self.arg.name});\n"

    def GetBuildForInterfacePreCode(self):
        notdirected = self.GetIndirectedArgName(None, 1)
        return "\tob{} = PyObject_From{}({});\n".format(
            self.arg.name,
            self.arg.type,
            self.GetIndirectedArgName(None, 1),
        )


class ArgFormatterIDLIST(ArgFormatterPythonCOM):
    def _GetPythonTypeDesc(self):
        return "<o PyIDL>"

    def GetParsePostCode(self):
        return "\tif (bPythonIsHappy && !PyObject_AsPIDL(ob{}, &{})) bPythonIsHappy = FALSE;\n".format(
            self.arg.name, self.GetIndirectedArgName(None, 1)
        )

    def GetInterfaceArgCleanup(self):
        return f"\tPyObject_FreePIDL({self.arg.name});\n"

    def GetBuildForInterfacePreCode(self):
        notdirected = self.GetIndirectedArgName(None, 1)
        return "\tob{} = PyObject_FromPIDL({});\n".format(
            self.arg.name,
            self.GetIndirectedArgName(None, 1),
        )


class ArgFormatterHANDLE(ArgFormatterPythonCOM):
    def _GetPythonTypeDesc(self):
        return "<o PyHANDLE>"

    def GetParsePostCode(self):
        return "\tif (!PyWinObject_AsHANDLE(ob{}, &{}, FALSE) bPythonIsHappy = FALSE;\n".format(
            self.arg.name, self.GetIndirectedArgName(None, 1)
        )

    def GetBuildForInterfacePreCode(self):
        notdirected = self.GetIndirectedArgName(None, 1)
        return "\tob{} = PyWinObject_FromHANDLE({});\n".format(
            self.arg.name,
            self.GetIndirectedArgName(None, 0),
        )


class ArgFormatterLARGE_INTEGER(ArgFormatterPythonCOM):
    def GetKeyName(self):
        return "LARGE_INTEGER"

    def _GetPythonTypeDesc(self):
        return "<o %s>" % self.GetKeyName()

    def GetParsePostCode(self):
        return "\tif (!PyWinObject_As{}(ob{}, {})) bPythonIsHappy = FALSE;\n".format(
            self.GetKeyName(),
            self.arg.name,
            self.GetIndirectedArgName(None, 1),
        )

    def GetBuildForInterfacePreCode(self):
        notdirected = self.GetIndirectedArgName(None, 0)
        return "\tob{} = PyWinObject_From{}({});\n".format(
            self.arg.name,
            self.GetKeyName(),
            notdirected,
        )


class ArgFormatterULARGE_INTEGER(ArgFormatterLARGE_INTEGER):
    def GetKeyName(self):
        return "ULARGE_INTEGER"


class ArgFormatterInterface(ArgFormatterPythonCOM):
    def GetInterfaceCppObjectInfo(self):
        return self.GetIndirectedArgName(
            1, self.arg.indirectionLevel
        ), "{} * {}".format(
            self.GetUnconstType(),
            self.arg.name,
        )

    def GetParsePostCode(self):
        # This gets called for out params in gateway mode
        if self.gatewayMode:
            sArg = self.GetIndirectedArgName(None, 2)
        else:
            # vs. in params for interface mode.
            sArg = self.GetIndirectedArgName(1, 2)
        return "\tif (bPythonIsHappy && !PyCom_InterfaceFromPyInstanceOrObject(ob{}, IID_{}, (void **){}, TRUE /* bNoneOK */))\n\t\t bPythonIsHappy = FALSE;\n".format(
            self.arg.name, self.arg.type, sArg
        )

    def GetBuildForInterfacePreCode(self):
        return "\tob{} = PyCom_PyObjectFromIUnknown({}, IID_{}, FALSE);\n".format(
            self.arg.name,
            self.arg.name,
            self.arg.type,
        )

    def GetBuildForGatewayPreCode(self):
        sPrefix = self._IndirectPrefix(self._GetDeclaredIndirection(), 1)
        return "\tob{} = PyCom_PyObjectFromIUnknown({}{}, IID_{}, TRUE);\n".format(
            self.arg.name,
            sPrefix,
            self.arg.name,
            self.arg.type,
        )

    def GetInterfaceArgCleanup(self):
        return f"\tif ({self.arg.name}) {self.arg.name}->Release();\n"


class ArgFormatterVARIANT(ArgFormatterPythonCOM):
    def GetParsePostCode(self):
        return "\tif ( !PyCom_VariantFromPyObject(ob{}, {}) )\n\t\tbPythonIsHappy = FALSE;\n".format(
            self.arg.name, self.GetIndirectedArgName(None, 1)
        )

    def GetBuildForGatewayPreCode(self):
        notdirected = self.GetIndirectedArgName(None, 1)
        return f"\tob{self.arg.name} = PyCom_PyObjectFromVariant({notdirected});\n"

    def GetBuildForGatewayPostCode(self):
        return "\tPy_XDECREF(ob%s);\n" % self.arg.name

        # Key :		, Python Type Description, ParseTuple format char


ConvertSimpleTypes = {
    "BOOL": ("BOOL", "int", "i"),
    "UINT": ("UINT", "int", "i"),
    "BYTE": ("BYTE", "int", "i"),
    "INT": ("INT", "int", "i"),
    "DWORD": ("DWORD", "int", "l"),
    "HRESULT": ("HRESULT", "int", "l"),
    "ULONG": ("ULONG", "int", "l"),
    "LONG": ("LONG", "int", "l"),
    "int": ("int", "int", "i"),
    "long": ("long", "int", "l"),
    "DISPID": ("DISPID", "long", "l"),
    "APPBREAKFLAGS": ("int", "int", "i"),
    "BREAKRESUMEACTION": ("int", "int", "i"),
    "ERRORRESUMEACTION": ("int", "int", "i"),
    "BREAKREASON": ("int", "int", "i"),
    "BREAKPOINT_STATE": ("int", "int", "i"),
    "BREAKRESUME_ACTION": ("int", "int", "i"),
    "SOURCE_TEXT_ATTR": ("int", "int", "i"),
    "TEXT_DOC_ATTR": ("int", "int", "i"),
    "QUERYOPTION": ("int", "int", "i"),
    "PARSEACTION": ("int", "int", "i"),
}


class ArgFormatterSimple(ArgFormatter):
    """An arg formatter for simple integer etc types"""

    def GetFormatChar(self):
        return ConvertSimpleTypes[self.arg.type][2]

    def _GetPythonTypeDesc(self):
        return ConvertSimpleTypes[self.arg.type][1]


AllConverters: dict[
    str, tuple[type[ArgFormatter], int, int] | tuple[type[ArgFormatter], int]
] = {
    "const OLECHAR": (ArgFormatterOLECHAR, 0, 1),
    "WCHAR": (ArgFormatterOLECHAR, 0, 1),
    "OLECHAR": (ArgFormatterOLECHAR, 0, 1),
    "LPCOLESTR": (ArgFormatterOLECHAR, 1, 1),
    "LPOLESTR": (ArgFormatterOLECHAR, 1, 1),
    "LPCWSTR": (ArgFormatterOLECHAR, 1, 1),
    "LPWSTR": (ArgFormatterOLECHAR, 1, 1),
    "LPCSTR": (ArgFormatterOLECHAR, 1, 1),
    "LPTSTR": (ArgFormatterTCHAR, 1, 1),
    "LPCTSTR": (ArgFormatterTCHAR, 1, 1),
    "HANDLE": (ArgFormatterHANDLE, 0),
    "BSTR": (ArgFormatterBSTR, 1, 0),
    "const IID": (ArgFormatterIID, 0),
    "CLSID": (ArgFormatterIID, 0),
    "IID": (ArgFormatterIID, 0),
    "GUID": (ArgFormatterIID, 0),
    "const GUID": (ArgFormatterIID, 0),
    "const IID": (ArgFormatterIID, 0),
    "REFCLSID": (ArgFormatterIID, 0),
    "REFIID": (ArgFormatterIID, 0),
    "REFGUID": (ArgFormatterIID, 0),
    "const FILETIME": (ArgFormatterTime, 0),
    "const SYSTEMTIME": (ArgFormatterTime, 0),
    "const LPSYSTEMTIME": (ArgFormatterTime, 1, 1),
    "LPSYSTEMTIME": (ArgFormatterTime, 1, 1),
    "FILETIME": (ArgFormatterTime, 0),
    "SYSTEMTIME": (ArgFormatterTime, 0),
    "STATSTG": (ArgFormatterSTATSTG, 0),
    "LARGE_INTEGER": (ArgFormatterLARGE_INTEGER, 0),
    "ULARGE_INTEGER": (ArgFormatterULARGE_INTEGER, 0),
    "VARIANT": (ArgFormatterVARIANT, 0),
    "float": (ArgFormatterFloat, 0),
    "single": (ArgFormatterFloat, 0),
    "short": (ArgFormatterShort, 0),
    "WORD": (ArgFormatterShort, 0),
    "VARIANT_BOOL": (ArgFormatterShort, 0),
    "HWND": (ArgFormatterLONG_PTR, 1),
    "HMENU": (ArgFormatterLONG_PTR, 1),
    "HOLEMENU": (ArgFormatterLONG_PTR, 1),
    "HICON": (ArgFormatterLONG_PTR, 1),
    "HDC": (ArgFormatterLONG_PTR, 1),
    "LPARAM": (ArgFormatterLONG_PTR, 1),
    "WPARAM": (ArgFormatterLONG_PTR, 1),
    "LRESULT": (ArgFormatterLONG_PTR, 1),
    "UINT": (ArgFormatterShort, 0),
    "SVSIF": (ArgFormatterShort, 0),
    "Control": (ArgFormatterInterface, 0, 1),
    "DataObject": (ArgFormatterInterface, 0, 1),
    "_PropertyBag": (ArgFormatterInterface, 0, 1),
    "AsyncProp": (ArgFormatterInterface, 0, 1),
    "DataSource": (ArgFormatterInterface, 0, 1),
    "DataFormat": (ArgFormatterInterface, 0, 1),
    "void **": (ArgFormatterInterface, 2, 2),
    "ITEMIDLIST": (ArgFormatterIDLIST, 0, 0),
    "LPITEMIDLIST": (ArgFormatterIDLIST, 0, 1),
    "LPCITEMIDLIST": (ArgFormatterIDLIST, 0, 1),
    "const ITEMIDLIST": (ArgFormatterIDLIST, 0, 1),
    "PITEMID_CHILD": (ArgFormatterIDLIST, 1),
    "const PITEMID_CHILD": (ArgFormatterIDLIST, 0),
    "PCITEMID_CHILD": (ArgFormatterIDLIST, 0),
    "PUITEMID_CHILD": (ArgFormatterIDLIST, 1),
    "PCUITEMID_CHILD": (ArgFormatterIDLIST, 0),
    "const PUITEMID_CHILD": (ArgFormatterIDLIST, 0),
    "PCUITEMID_CHILD_ARRAY": (ArgFormatterIDLIST, 2),
    "const PCUITEMID_CHILD_ARRAY": (ArgFormatterIDLIST, 2),
    # Auto-add all the simple types
    **dict.fromkeys(ConvertSimpleTypes, (ArgFormatterSimple, 0)),
}


def make_arg_converter(arg):
    try:
        clz = AllConverters[arg.type][0]
        bin = AllConverters[arg.type][1]
        decl = 0
        if len(AllConverters[arg.type]) > 2:
            decl = AllConverters[arg.type][2]
        return clz(arg, bin, decl)
    except KeyError:
        if arg.type[0] == "I":
            return ArgFormatterInterface(arg, 0, 1)

        raise error_not_supported(f"The type '{arg.type}' ({arg.name}) is unknown.")


#############################################################
#
# The instances that represent the args, methods and interface
class Argument:
    """A representation of an argument to a COM method

    This class contains information about a specific argument to a method.
    In addition, methods exist so that an argument knows how to convert itself
    to/from Python arguments.
    """

    # 									  in,out					  type			  name			 [	]
    # 								   --------------				--------	  ------------		------
    regex = re.compile(r"/\* \[([^\]]*.*?)] \*/[ \t](.*[* ]+)(\w+)(\[ *])?[\),]")

    @classmethod
    def drop_rpc_metadata(cls, type_):
        return " ".join(e for e in type_.split(" ") if not e.startswith("__RPC_"))

    def __init__(self, good_interface_names):
        self.good_interface_names = good_interface_names
        self.inout = None
        self.name = None
        self.type = None
        self.raw_type = None
        self.unc_type = None
        self.const = 0
        self.arrayDecl = 0
        self.indirectionLevel = 0

    def BuildFromFile(self, file):
        """Parse and build my data from a file

        Reads the next line in the file, and matches it as an argument
        description.  If not a valid argument line, an error_not_found exception
        is raised.
        """
        line = file.readline()
        mo = self.regex.search(line)
        if not mo:
            raise error_not_found
        self.name = mo.group(3)
        self.inout = mo.group(1).split("][")
        typ = mo.group(2).strip()
        self.raw_type = typ
        if mo.group(4):  # Has "[ ]" decl
            self.arrayDecl = 1
            typ_no_rpc = self.drop_rpc_metadata(typ)
            if typ != typ_no_rpc:
                self.indirectionLevel += 1

        typ = self.drop_rpc_metadata(typ)
        while 1:
            try:
                pos = typ.rindex("*")
                self.indirectionLevel += 1
                typ = typ[:pos].strip()
            except ValueError:
                break
        self.type = typ
        if self.type[:6] == "const ":
            self.unc_type = self.type[6:]
        else:
            self.unc_type = self.type

        if VERBOSE:
            print(
                "       Arg {} of type {}{} ({})".format(
                    self.name, self.type, "*" * self.indirectionLevel, self.inout
                )
            )

    def HasAttribute(self, typ):
        """Determines if the argument has the specific attribute.

        Argument attributes are specified in the header file, such as
        "[in][out][retval]" etc.  You can pass a specific string (eg "out")
        to find if this attribute was specified for the argument
        """
        return typ in self.inout

    def GetRawDeclaration(self):
        ret = f"{self.raw_type} {self.name}"
        if self.arrayDecl:
            ret += "[]"
        return ret


class Method:
    """A representation of a C++ method on a COM interface

    This class contains information about a specific method, as well as
    a list of all @Argument@s
    """

    # 										 options	 ret type callconv	 name
    # 								   ----------------- -------- -------- --------
    regex = re.compile(r"virtual (/\*.*?\*/ )?(.*?) (.*?) (.*?)\(\w?")

    def __init__(self, good_interface_names):
        self.good_interface_names = good_interface_names
        self.name = self.result = self.callconv = None
        self.args = []

    def BuildFromFile(self, file):
        """Parse and build my data from a file

        Reads the next line in the file, and matches it as a method
        description.  If not a valid method line, an error_not_found exception
        is raised.
        """
        line = file.readline()
        mo = self.regex.search(line)
        if not mo:
            raise error_not_found
        self.name = mo.group(4)
        self.result = mo.group(2)
        if self.result != "HRESULT":
            if self.result == "DWORD":  # DWORD is for old old stuff?
                print(
                    "Warning: Old style interface detected - compilation errors likely!"
                )
            else:
                print(
                    "Method %s - Only HRESULT return types are supported." % self.name
                )
            # 				raise error_not_supported,		if VERBOSE:
            print(f"     Method {self.result} {self.name}(")
        while 1:
            arg = Argument(self.good_interface_names)
            try:
                arg.BuildFromFile(file)
                self.args.append(arg)
            except error_not_found:
                break


class Interface:
    """A representation of a C++ COM Interface

    This class contains information about a specific interface, as well as
    a list of all @Method@s
    """

    # 									  name				 base
    # 									 --------		   --------
    regex = re.compile(r"(interface|) ([^ ]*) : public (.*)$")

    def __init__(self, mo):
        self.methods = []
        self.name = mo.group(2)
        self.base = mo.group(3)
        if VERBOSE:
            print(f"Interface {self.name} : public {self.base}")

    def BuildMethods(self, file):
        """Build all sub-methods for this interface"""
        # skip the next 2 lines.
        file.readline()
        file.readline()
        while 1:
            try:
                method = Method([self.name])
                method.BuildFromFile(file)
                self.methods.append(method)
            except error_not_found:
                break


def find_interface(interfaceName, file):
    """Find and return an interface in a file

    Given an interface name and file, search for the specified interface.

    Upon return, the interface itself has been built,
    but not the methods.
    """
    interface = None
    line = file.readline()
    while line:
        mo = Interface.regex.search(line)
        if mo:
            name = mo.group(2)
            print(name)
            AllConverters[name] = (ArgFormatterInterface, 0, 1)
            if name == interfaceName:
                interface = Interface(mo)
                interface.BuildMethods(file)
        line = file.readline()
    if interface:
        return interface
    raise error_not_found


def parse_interface_info(interfaceName, file):
    """Find, parse and return an interface in a file

    Given an interface name and file, search for the specified interface.

    Upon return, the interface itself is fully built,
    """
    try:
        return find_interface(interfaceName, file)
    except re.error:
        traceback.print_exc()
        print("The interface could not be built, as the regular expression failed!")


def test():
    f = open("d:\\msdev\\include\\objidl.h")
    try:
        parse_interface_info("IPersistStream", f)
    finally:
        f.close()


def test_regex(r, text):
    res = r.search(text, 0)
    if res == -1:
        print("** Not found")
    else:
        print(
            "%d\n%s\n%s\n%s\n%s" % (res, r.group(1), r.group(2), r.group(3), r.group(4))
        )
