###
# This file is a part of the NVDA project.
# URL: http://www.nvda-project.org/
# Copyright 2013-2017 NV Access Limited.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2.0, as published by
# the Free Software Foundation.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# This license can be found at:
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
###
from typing import Callable, List
import re
from SCons.Node.FS import File

Import(
	"env",
)

import importlib.util  # noqa: E402

# Monkeypatch comtypes to clear the importlib cache when importing a new module
import comtypes.client._generate  # noqa: E402

old_my_import = comtypes.client._generate._my_import


def new_my_import(fullname):
	importlib.invalidate_caches()
	return old_my_import(fullname)


comtypes.client._generate._my_import = new_my_import


def codeReplacer(
	template: str,
	indentedGroups: tuple[int | str] | None = None,
) -> Callable[[re.Match[str]], str]:
	"""
	Return a function for the `repl` parameter of `re.sub()`,
	in order to do some preprocessing before actual replacement.

	:param template: Template strings for replacement.
		Placeholders are in `str.format`'s style, such as `{1}` or `{name}`.
	:param indentedGroups: All groups that needs to have one more level of indentation.
		Specify indices for unnamed groups, or names for named groups.
	"""

	def repl(match: re.Match[str]) -> str:
		groups = list(match.groups(""))
		groupdict = match.groupdict("")
		if indentedGroups:
			for grp in indentedGroups:
				if isinstance(grp, int) and 0 <= grp < len(groups):
					collection = groups
				else:
					collection = groupdict
				# Add one level of indentation before each line
				lines = collection[grp].splitlines()
				for i in range(len(lines)):
					if lines[i]:  # Add indentation for non-empty lines
						lines[i] = "    " + lines[i]
				collection[grp] = "\n".join(lines)
		return template.format(*groups, **groupdict)

	return repl


def makeIDEFriendly(path: str) -> None:
	"""
	Add local imports so that tools and IDE's can find definitions.
	Prefer to import from comtypes.gen, at runtime behavior will not have changed.

	:param path: Path to the friendly name comInterfaces module.
	"""
	importTemplate = """from typing import TYPE_CHECKING
if TYPE_CHECKING:
	from .{libId} import (
{importList}
	)
else:
	from comtypes.gen.{libId} import (
{importList}
	)
"""
	# Matches:
	# from comtypes.gen._SomeLibId_ import (
	#     aaa, bbb, ccc,
	#     xxx, yyy, zzz,   # these lines should be indented
	# )
	importPattern = re.compile(
		r"^from comtypes\.gen\.(?P<libId>\w+) import \(\n"
		r"(?P<importList>(?:    [\w, ]+\n)+)"
		r"\)\n",
		re.MULTILINE,
	)

	with open(path, "r") as interfaceFile:
		fileContent = interfaceFile.read()
		fileContent = importPattern.sub(
			codeReplacer(importTemplate, indentedGroups=("importList",)), fileContent
		)

	with open(path, "w") as interfaceFile:
		interfaceFile.write(fileContent)


def interfaceAction(target: List[File], source, env):
	clsid = env.get("clsid")
	if clsid:
		source = (clsid, env["majorVersion"], env["minorVersion"])
	else:
		source = str(source[0])
	comtypes.client.GetModule(source)
	# re-write the the "friendlyNameFile" so that tools/IDEs can find the
	# definitions
	for t in target:
		path: str = t.abspath
		if path.endswith(".py"):
			makeIDEFriendly(path)


interfaceBuilder = env.Builder(
	action=env.Action(interfaceAction),
)
env["BUILDERS"]["comtypesInterface"] = interfaceBuilder

# Force comtypes generated interfaces in to our directory
import comtypes.client  # noqa: E402

comtypes.client.gen_dir = Dir("comInterfaces").abspath

COM_INTERFACES = {
	"IAccessible2Lib.py": "typelibs/ia2.tlb",
	"ISimpleDOM.py": "typelibs/ISimpleDOMNode.tlb",
	"Scripting.py": ("{420B2830-E718-11CF-893D-00A0C9054228}", 1, 0),  # Used for browseable messages
	"MathPlayer.py": "typelibs/mathPlayerDLL.tlb",
	"Accessibility.py": ("{1EA4DBF0-3C3B-11CF-810C-00AA00389B71}", 1, 0),
	"tom.py": ("{8CC497C9-A1DF-11CE-8098-00AA0047BE5D}", 1, 0),
	"SpeechLib.py": ("{C866CA3A-32F7-11D2-9602-00C04F8EE628}", 5, 0),
	"AcrobatAccessLib.py": "typelibs/AcrobatAccess.tlb",
	# As long as GitHub Actions is on a Windows version that has an acceptable version of UIAutomationClient,
	# we generate it at build time.
	"UIAutomationClient.py": ("{944DE083-8FB8-45CF-BCB7-C477ACB2F897}", 1, 0),
}

for k, v in COM_INTERFACES.items():
	targets = [
		Dir("comInterfaces").File(k),
		# This builds a .pyc file as well.
		Dir("comInterfaces").File(importlib.util.cache_from_source(k)),
	]
	source = clsid = majorVersion = None
	if isinstance(v, str):
		env.comtypesInterface(targets, v)
	else:
		env.comtypesInterface(
			targets,
			Dir("comInterfaces").File("__init__.py"),
			clsid=v[0],
			majorVersion=v[1],
			minorVersion=v[2],
		)


# When cleaning comInterfaces get rid of everything
# except for the readme.md and __init__.py files tracked by Git
env.Clean(
	Dir("comInterfaces"),
	Glob("comInterfaces/*", exclude=["comInterfaces/readme.md", "comInterfaces/__init__.py"]),
)
