from . import BonnibelError

class Project:
    def __init__(self, root):
        from .version import git_version

        self.root = root
        self.version = git_version(root)

    def __str__(self):
        return f"{self.name} {self.version.major}.{self.version.minor}.{self.version.patch}-{self.version.sha}"

    def generate(self, root, output, modules, config, manifest_file):
        import sys
        from os.path import join
        from ninja.ninja_syntax import Writer

        targets = set()
        kinds = set()
        for mod in modules.values():
            targets.update(mod.targets)
            kinds.add(mod.kind)

        from .config import generate_configs
        config_deps = generate_configs(root, output, config, targets, kinds)

        with open(output / "build.ninja", "w") as buildfile:
            build = Writer(buildfile)

            build.comment("This file is automatically generated by bonnibel")
            build.variable("ninja_required_version", "1.3")
            build.variable("build_root", output)
            build.variable("source_root", root)
            build.variable("build_config", config)
            build.newline()

            build.include(root / "assets/build/rules.ninja")
            build.newline()

            build.variable("version_major", self.version.major)
            build.variable("version_minor", self.version.minor)
            build.variable("version_patch", self.version.patch)
            build.variable("version_sha", self.version.sha)
            build.newline()

            build.variable("cogflags", [
                "-I", "${source_root}/scripts",
                "-D", "definitions_path=${source_root}/definitions",
                ])
            build.newline()

            for target in targets:
                build.subninja(output / target / "target.ninja")
            build.newline()

            build.build(
                rule = "touch",
                outputs = "${build_root}/.all_headers",
                implicit = [f"${{build_root}}/include/{m.name}/.headers.phony"
                    for m in modules.values() if m.public_headers],
                )
            build.build(
                rule = "phony",
                outputs = ["all-headers"],
                inputs = ["${build_root}/.all_headers"])

            from .manifest import Manifest
            manifest = Manifest(manifest_file, modules)

            debugroot = output / ".debug"
            debugroot.mkdir(exist_ok=True)

            fatroot = output / "fatroot"
            fatroot.mkdir(exist_ok=True)

            (fatroot / manifest.location).mkdir(exist_ok=True)

            initrdroot = output / "initrd_root"
            initrdroot.mkdir(exist_ok=True)

            fatroot_content = []
            initrd_content = []

            def add_fatroot(source, name):
                output = join(manifest.location, name)
                fatroot_output = f"${{build_root}}/fatroot/{output}"

                build.build(
                    rule = "cp",
                    outputs = [fatroot_output],
                    inputs = [source],
                    variables = {
                        "description": f"Installing {output}",
                    })

                fatroot_content.append(fatroot_output)
                build.newline()

            def add_fatroot_exe(entry):
                input_path = f"${{build_root}}/{entry.target}/{entry.output}"
                intermediary = f"${{build_root}}/{entry.output}"

                build.build(
                    rule = "strip",
                    outputs = [intermediary],
                    inputs = [input_path],
                    implicit = [f"{input_path}.dump"],
                    variables = {
                        "name": f"Stripping {entry.module}",
                        "debug": f"${{build_root}}/.debug/{entry.output}.debug",
                    })

                add_fatroot(intermediary, entry.output)

            def add_initrd_content(root, name):
                output = join(root, name)
                initrd_output = f"${{build_root}}/initrd_root/{output}"

                build.build(
                    rule = "cp",
                    outputs = [initrd_output],
                    inputs = [f"${{build_root}}/{name}"],
                    variables = {
                        "description": f"Installing {name}",
                    })

                initrd_content.append(initrd_output)
                build.newline()

            def add_initrd_stripped(root, entry):
                input_path = f"${{build_root}}/{entry.target}/{entry.output}"
                intermediary = f"${{build_root}}/{entry.output}"

                build.build(
                    rule = "strip",
                    outputs = [intermediary],
                    inputs = [input_path],
                    implicit = [f"{input_path}.dump"],
                    variables = {
                        "name": f"Stripping {entry.module}",
                        "debug": f"${{build_root}}/.debug/{entry.output}.debug",
                    })

                add_initrd_content(root, entry.output)

            add_fatroot_exe(manifest.kernel)
            add_fatroot_exe(manifest.init)
            for program in manifest.panics:
                add_fatroot_exe(program)

            for program in manifest.services:
                add_initrd_stripped("jsix/services", program)

            for program in manifest.drivers:
                add_initrd_stripped("jsix/drivers", program)

            for program in manifest.libs:
                add_initrd_stripped("jsix/lib", program)

            syms = manifest.add_data("symbol_table.dat",
                    "Symbol table", ("symbols",))

            syms_file = "jsix.symbols"
            syms_path = join(manifest.location, syms_file );
            syms_out = join(fatroot, syms_path)
            build.build(
                rule = "makest",
                outputs = [syms_out],
                inputs = [f"${{build_root}}/kernel/{modules['kernel'].get_output(static=True)}"],
                )
            fatroot_content.append(syms_out)
            manifest.symbols = syms_file

            bootloader = "${build_root}/fatroot/efi/boot/bootx64.efi"
            build.build(
                rule = "cp",
                outputs = [bootloader],
                inputs = ["${build_root}/boot/boot.efi"],
                variables = {
                    "description": "Installing bootloader",
                })
            build.newline()

            boot_config = join(fatroot, "jsix", "boot.conf")
            manifest.write_boot_config(boot_config)

            initrd = str(fatroot / manifest.location / manifest.initrd["name"])
            build.build(
                rule = "makeinitrd",
                outputs = [initrd],
                inputs = [str(initrdroot)],
                implicit = initrd_content + ["${source_root}/scripts/mkj6romfs.py"],
                variables = {"format": manifest.initrd["format"]},
            )
            build.newline()

            fatroot_content.append(initrd)

            build.build(
                rule = "makefat",
                outputs = ["${build_root}/jsix.img"],
                inputs = ["${source_root}/assets/diskbase.img"],
                implicit = fatroot_content + [bootloader],
                variables = {"name": "jsix.img"},
                )
            build.newline()

            default_assets = {
                "UEFI Variables": ("ovmf/x64/ovmf_vars.fd", "ovmf_vars.fd"),
                "GDB Debug Helpers": ("debugging/jsix.elf-gdb.py", "jsix.elf-gdb.py"),
            }

            for name, assets in default_assets.items():
                p = root / "assets" / assets[0]
                out = f"${{build_root}}/{assets[1]}"
                build.build(
                    rule = "cp",
                    outputs = [out],
                    inputs = [str(p)],
                    variables = {"name": name},
                    )
                build.default([out])
                build.newline()

            compdb = "${source_root}/compile_commands.json"

            build.rule("regen",
                command = " ".join([str(root / 'configure')] + sys.argv[1:]),
                description = "Regenerate build files",
                generator = True,
                )
            build.newline()

            regen_implicits = \
                [f"{self.root}/configure", str(manifest_file)] + \
                [str(mod.modfile) for mod in modules.values()]

            regen_implicits += list(map(str, config_deps))

            build.build(
                rule = "compdb",
                outputs = [compdb],
                implicit = regen_implicits,
                )
            build.default([compdb])
            build.newline()

            build.build(
                rule = "regen",
                outputs = ['build.ninja'],
                implicit = regen_implicits,
                implicit_outputs = 
                    [f"module.{mod.name}.ninja" for mod in modules.values()] +
                    [f"{target}/target.ninja" for target in targets] +
                    [boot_config],
                )

            build.newline()
            build.default(["${build_root}/jsix.img"])

        for target in targets:
            mods = [m.name for m in modules.values() if target in m.targets]

            targetdir = output / target
            targetdir.mkdir(exist_ok=True)

            buildfilename = str(targetdir / "target.ninja")
            with open(buildfilename, "w") as buildfile:
                build = Writer(buildfile)
                build.comment("This file is automatically generated by bonnibel")
                build.newline()

                build.variable("target", target)
                build.variable("target_dir", output / target)
                build.newline()

                build.include(f"{target}/config.ninja")

                build.newline()
                for kind in ('defs', 'run'):
                    for lang in ('c', 'cpp'):
                        deffile = str(output / target / f"{lang}.{kind}")

                        build.build(
                            rule = f"dump_{lang}_{kind}",
                            outputs = [deffile],
                            implicit = [buildfilename],
                        )
                        build.default(deffile)
                        build.newline()

                for mod in mods:
                    build.subninja(f"module.{mod}.ninja")
