import re
from pathlib import Path   # For OS-agnostic path manipulation
from typing import Iterable
from click import secho
from SCons.Script import Action, Exit
from platformio.builder.tools.piolib import LibBuilderBase


def is_wled_module(env, dep: LibBuilderBase) -> bool:
  """Returns true if the specified library is a wled module
  """
  usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods"
  return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-")


def read_lines(p: Path):
    """ Read in the contents of a file for analysis """
    with p.open("r", encoding="utf-8", errors="ignore") as f:
        return f.readlines()


def check_map_file_objects(map_file: list[str], dirs: Iterable[str]) -> set[str]:
    """ Identify which dirs contributed to the final build

        Returns the (sub)set of dirs that are found in the output ELF
    """
    # Pattern to match symbols in object directories
    # Join directories into alternation
    usermod_dir_regex = "|".join([re.escape(dir) for dir in dirs])
    # Matches nonzero address, any size, and any path in a matching directory
    object_path_regex = re.compile(r"0x0*[1-9a-f][0-9a-f]*\s+0x[0-9a-f]+\s+\S+[/\\](" + usermod_dir_regex + r")[/\\]\S+\.o")

    found = set()
    for line in map_file:
        matches = object_path_regex.findall(line)
        for m in matches:
            found.add(m)
    return found


def count_usermod_objects(map_file: list[str]) -> int:
    """ Returns the number of usermod objects in the usermod list """
    # Count the number of entries in the usermods table section
    return len([x for x in map_file if ".dtors.tbl.usermods.1" in x])


def validate_map_file(source, target, env):
    """ Validate that all modules appear in the output build """
    build_dir = Path(env.subst("$BUILD_DIR"))
    map_file_path = build_dir /  env.subst("${PROGNAME}.map")

    if not map_file_path.exists():
        secho(f"ERROR: Map file not found: {map_file_path}", fg="red", err=True)
        Exit(1)

    # Identify the WLED module builders, set by load_usermods.py
    module_lib_builders = env['WLED_MODULES']

    # Extract the values we care about
    modules = {Path(builder.build_dir).name: builder.name for builder in module_lib_builders}
    secho(f"INFO: {len(modules)} libraries linked as WLED optional/user modules")

    # Now parse the map file
    map_file_contents = read_lines(map_file_path)
    usermod_object_count = count_usermod_objects(map_file_contents)
    secho(f"INFO: {usermod_object_count} usermod object entries")

    confirmed_modules = check_map_file_objects(map_file_contents, modules.keys())
    missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules]
    if missing_modules:
        secho(
            f"ERROR: No object files from {missing_modules} found in linked output!",
            fg="red",
            err=True)
        Exit(1)
    return None

Import("env")
env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")])
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file'))
