
def _needs(f, d, key):
    try: return d[key]
    except KeyError:
        from . import BonnibelError
        raise BonnibelError(f"module file '{f}' missing '{key}' attribute")

class Module:
    filename = "module.toml"

    def __init__(self, desc, proj_root):
        import toml
        from pathlib import Path

        data = toml.load(desc)
        self.root = desc.parent

        # Required fields
        self.name = _needs(desc, data, "name")

        # Optional fields
        self.deps = data.get("deps", tuple())
        self.defines = data.get("defines", tuple())
        self.kind = data.get("kind", "exe")
        self.variables = data.get("variables", dict())
        self.default = data.get("default", False)

        self.includes = set()
        for include in data.get("includes", tuple()):
            p = Path(include)
            if p.is_absolute():
                self.includes.add(p)
            else:
                self.includes.add(proj_root / p)

        self.targets = set()
        for target in data.get("targets", tuple()):
            self.targets.add(target)

        self.output = data.get("output", None) or \
                self.__default_output()

        self.depmods = set()

        from .source import Source
        self.sources = [Source(f, self.root) for f in 
                data.get("sources", tuple())]

    def __default_output(self):
        if self.kind == "lib":
            return f"lib{self.name}.a"
        else:
            return f"{self.name}.elf"

    def __str__(self):
        return "Module {} {}\n\t{}".format(self.kind, self.name, "\n\t".join(map(str, self.sources)))

    @classmethod
    def update(cls, mods):
        from . import BonnibelError

        for mod in mods.values():
            for dep in mod.deps:
                if not dep in mods:
                    raise BonnibelError(f"module '{mod.name}' references unknown module '{dep}'")

                for dep in mod.deps:
                    depmod = mods[dep]
                    mod.depmods.add(depmod)

        target_mods = [mod for mod in mods.values() if mod.targets]
        for mod in target_mods:
            closed = set()
            children = set(mod.depmods)
            while children:
                child = children.pop()
                closed.add(child)
                child.targets |= mod.targets
                children |= {m for m in child.depmods if not m in closed}

    def generate(self, bc):
        from ninja.ninja_syntax import Writer

        filename = str(bc.output / f"{self.name}.ninja")
        with open(filename, "w") as buildfile:
            build = Writer(buildfile)

            build.comment("This file is automatically generated by bonnibel")
            build.newline()

            build.variable("module_dir", f"${{target_dir}}/{self.name}.dir")

            for key, value in self.variables.items():
                build.variable(key, value)
            build.newline()

            includes = [f"-I{i}" for i in self.includes]

            libs = []
            closed = set()
            children = set(self.depmods)
            while children:
                child = children.pop()
                closed.add(child)
                includes += [f"-I{i}" for i in child.includes]
                if child.kind == "lib":
                    libs.append(f"${{target_dir}}/{child.output}")
                children |= {m for m in child.depmods if not m in closed}

            if includes:
                build.variable("ccflags", ["${ccflags}"] + includes)

            if libs:
                build.variable("libs", ["${libs}"] + libs)

            inputs = []
            for source in self.sources:
                output = f"${{module_dir}}/{source.output}"
                inputs.append(output)

                build.build(
                    rule = source.action,
                    outputs = output,
                    inputs = str(source.input),
                    variables = {"name": str(source.name)},
                )

            build.newline()

            output = f"${{target_dir}}/{self.output}"
            build.build(
                rule = self.kind,
                outputs = output,
                inputs = inputs,
                implicit = libs,
            )

            if self.default:
                build.default(output)

