def resolve(path):
    if path.startswith('/') or path.startswith('$'):
        return path
    from pathlib import Path
    return str(Path(path).resolve())

class Module:
    __fields = {
        # name: (type, default)
        "kind": (str, "exe"),
        "output": (str, None),
        "targets": (set, ()),
        "deps":  (set, ()),
        "includes": (tuple, ()),
        "sources": (tuple, ()),
        "variables": (dict, ()),
        "default": (bool, False),
    }

    def __init__(self, name, modfile, root, **kwargs):
        from .source import Source

        # Required fields
        self.root = root
        self.name = name
        self.modfile = modfile

        for name, data in self.__fields.items():
            ctor, default = data
            value = kwargs.get(name, default)
            if value is not None:
                value = ctor(value)

            setattr(self, name, value)

        for name in kwargs:
            if not name in self.__fields:
                raise AttributeError(f"No attribute named {name} on Module")

        # Turn strings into real Source objects
        self.sources = [Source(root, f) for f in self.sources]

        # Filled by Module.update
        self.depmods = set()

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

    @property
    def output(self):
        if self.__output is not None:
            return self.__output

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

    @output.setter
    def output(self, value):
        self.__output = value

    @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}'")

                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, output):
        filename = str(output / f"{self.name}.ninja")

        with open(filename, "w") as buildfile:
            from pathlib import Path
            from ninja.ninja_syntax import Writer

            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 = [self.root, "${module_dir}"]
            for include in self.includes:
                p = Path(include)
                if p.is_absolute():
                    if not p in includes:
                        includes.append(str(p.resolve()))
                elif include != ".":
                    includes.append(str(self.root / p))
                    includes.append(f"${{module_dir}}/{p}")

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

            if includes:
                build.variable("ccflags", ["${ccflags}"] + [f"-I{i}" for i in includes])
                build.variable("asflags", ["${asflags}"] + [f"-I{i}" for i in includes])

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

            inputs = []
            implicits = []

            for start in self.sources:
                source = start

                while source and source.action:
                    output = source.output

                    if source.action.rule:
                        build.build(
                            rule = source.action.rule,
                            outputs = output.input,
                            inputs = source.input,
                            implicit = list(map(resolve, source.deps)),
                            variables = {"name": source.name},
                        )

                    elif source.action.implicit:
                        implicits.append(source.input)

                    else:
                        inputs.append(source.input)

                    source = output
                    build.newline()

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

            build.newline()
            build.build(
                rule = "dump",
                outputs = dump,
                inputs = output,
                variables = {"name": self.name},
            )

            if self.default:
                build.newline()
                build.default(output)
                build.default(dump)

    def add_input(self, path, **kwargs):
        from .source import Source
        s = Source(self.root, path, **kwargs)
        self.sources.append(s)
        return str(s.output)
