import os
import re
from collections import OrderedDict

from .debian import (
    Changelog,
    PackageArchitecture,
    PackageDescription,
    PackageRelation,
    Version,
)


class PackagesList(OrderedDict):
    def append(self, package):
        self[package["Package"]] = package

    def extend(self, packages):
        for package in packages:
            self[package["Package"]] = package


class Makefile:
    def __init__(self):
        self.rules = {}
        self.add(".NOTPARALLEL")

    def add(self, name, deps=None, cmds=None):
        if name in self.rules:
            self.rules[name].add(deps, cmds)
        else:
            self.rules[name] = self.Rule(name, deps, cmds)
        if deps is not None:
            for i in deps:
                if i not in self.rules:
                    self.rules[i] = self.Rule(i)

    def write(self, out):
        for i in sorted(self.rules.keys()):
            self.rules[i].write(out)

    class Rule:
        def __init__(self, name, deps=None, cmds=None):
            self.name = name
            self.deps, self.cmds = set(), []
            self.add(deps, cmds)

        def add(self, deps=None, cmds=None):
            if deps is not None:
                self.deps.update(deps)
            if cmds is not None:
                self.cmds.append(cmds)

        def write(self, out):
            deps_string = ""
            if self.deps:
                deps = list(self.deps)
                deps.sort()
                deps_string = " " + " ".join(deps)

            if self.cmds:
                if deps_string:
                    out.write(f"{self.name}::{deps_string}\n")
                for c in self.cmds:
                    out.write(f"{self.name}::\n")
                    for i in c:
                        out.write(f"\t{i}\n")
            else:
                out.write(f"{self.name}:{deps_string}\n")


class MakeFlags(dict):
    def __str__(self):
        return " ".join("{}='{}'".format(*i) for i in sorted(self.items()))

    def copy(self):
        return self.__class__(super().copy())


def iter_featuresets(config):
    for featureset in config["base",]["featuresets"]:
        if config.merge("base", None, featureset).get("enabled", True):
            yield featureset


def iter_arches(config):
    return iter(config["base",]["arches"])


def iter_arch_featuresets(config, arch):
    for featureset in config["base", arch].get("featuresets", []):
        if config.merge("base", arch, featureset).get("enabled", True):
            yield featureset


def iter_flavours(config, arch, featureset):
    return iter(config["base", arch, featureset]["flavours"])


class Gencontrol:
    makefile_targets = ("binary-arch", "build-arch", "setup")
    makefile_targets_indep = ("binary-indep", "build-indep", "setup")

    def __init__(self, config, templates, version=Version):
        self.config, self.templates = config, templates
        self.changelog = Changelog(version=version)
        self.tpl_vars = {}

    def __call__(self):
        packages = PackagesList()
        makefile = Makefile()

        self.do_source(packages)
        self.do_main(packages, makefile)
        self.do_extra(packages, makefile)

        self.merge_build_depends(packages)
        self.write(packages, makefile)

    def do_source(self, packages):
        source = self.templates["control.source"][0]
        if not source.get("Source"):
            source["Source"] = self.changelog[0].source
        packages["source"] = self.process_package(source, self.tpl_vars)

    def do_main(self, packages, makefile):
        tpl_vars = self.tpl_vars.copy()

        makeflags = MakeFlags()
        extra = {}

        self.do_main_setup(tpl_vars, makeflags, extra)
        self.do_main_makefile(makefile, makeflags, extra)
        self.do_main_packages(packages, tpl_vars, makeflags, extra)
        self.do_main_recurse(packages, makefile, tpl_vars, makeflags, extra)

    def do_main_setup(self, tpl_vars, makeflags, extra):
        del tpl_vars, makeflags, extra

    def do_main_makefile(self, makefile, makeflags, extra):
        del extra
        makefile.add(
            "build-indep",
            cmds=[f"$(MAKE) -f debian/rules.real build-indep {makeflags}"],
        )
        makefile.add(
            "binary-indep",
            cmds=[f"$(MAKE) -f debian/rules.real binary-indep {makeflags}"],
        )

    def do_main_packages(self, packages, tpl_vars, makeflags, extra):
        del packages, tpl_vars, makeflags, extra

    def do_main_recurse(self, packages, makefile, tpl_vars, makeflags, extra):
        for featureset in iter_featuresets(self.config):
            self.do_indep_featureset(
                packages, makefile, featureset, tpl_vars.copy(), makeflags.copy(), extra
            )
        for arch in iter_arches(self.config):
            self.do_arch(
                packages, makefile, arch, tpl_vars.copy(), makeflags.copy(), extra
            )

    def do_extra(self, packages, makefile):
        templates_extra = self.templates.get("control.extra", None)
        if templates_extra is None:
            return

        packages_extra = self.process_packages(templates_extra, self.tpl_vars)
        packages.extend(packages_extra)
        extra_arches = {}
        for package in packages_extra:
            arches = package["Architecture"]
            for arch in arches:
                i = extra_arches.get(arch, [])
                i.append(package)
                extra_arches[arch] = i
        for arch in sorted(extra_arches.keys()):
            cmds = []
            for i in extra_arches[arch]:
                cmds.append(
                    "$(MAKE) -f debian/rules.real install-dummy "
                    "ARCH='{}' DH_OPTIONS='-p{}'".format(arch, i["Package"])
                )
            makefile.add(f"binary-arch_{arch}", [f"binary-arch_{arch}_extra"])
            makefile.add(f"binary-arch_{arch}_extra", cmds=cmds)

    def do_indep_featureset(
        self, packages, makefile, featureset, tpl_vars, makeflags, extra
    ):
        tpl_vars["localversion"] = ""
        if featureset != "none":
            tpl_vars["localversion"] = "-" + featureset

        self.do_indep_featureset_setup(tpl_vars, makeflags, featureset, extra)
        self.do_indep_featureset_makefile(makefile, featureset, makeflags, extra)
        self.do_indep_featureset_packages(
            packages, makefile, featureset, tpl_vars, makeflags, extra
        )

    def do_indep_featureset_setup(self, tpl_vars, makeflags, featureset, extra):
        del tpl_vars, makeflags, featureset, extra

    def do_indep_featureset_makefile(self, makefile, featureset, makeflags, extra):
        del extra
        makeflags["FEATURESET"] = featureset

        for i in self.makefile_targets_indep:
            target1 = i
            target2 = f"{target1}_{featureset}"
            target3 = f"{target2}_real"
            makefile.add(target1, [target2])
            makefile.add(target2, [target3])

    def do_indep_featureset_packages(
        self, packages, makefile, featureset, tpl_vars, makeflags, extra
    ):
        del packages, makefile, featureset, tpl_vars, makeflags, extra

    def do_arch(self, packages, makefile, arch, tpl_vars, makeflags, extra):
        tpl_vars["arch"] = arch

        self.do_arch_setup(tpl_vars, makeflags, arch, extra)
        self.do_arch_makefile(makefile, arch, makeflags, extra)
        self.do_arch_packages(packages, makefile, arch, tpl_vars, makeflags, extra)
        self.do_arch_recurse(packages, makefile, arch, tpl_vars, makeflags, extra)

    def do_arch_setup(self, tpl_vars, makeflags, arch, extra):
        del tpl_vars, makeflags, arch, extra

    def do_arch_makefile(self, makefile, arch, makeflags, extra):
        del extra
        makeflags["ARCH"] = arch

        for i in self.makefile_targets:
            target1 = i
            target2 = f"{target1}_{arch}"
            target3 = f"{target2}_real"
            makefile.add(target1, [target2])
            makefile.add(target2, [target3])

    def do_arch_packages(self, packages, makefile, arch, tpl_vars, makeflags, extra):
        del packages, makefile, arch, tpl_vars, makeflags, extra

    def do_arch_recurse(self, packages, makefile, arch, tpl_vars, makeflags, extra):
        for featureset in iter_arch_featuresets(self.config, arch):
            self.do_featureset(
                packages,
                makefile,
                arch,
                featureset,
                tpl_vars.copy(),
                makeflags.copy(),
                extra,
            )

    def do_featureset(
        self, packages, makefile, arch, featureset, tpl_vars, makeflags, extra
    ):
        tpl_vars["localversion"] = ""
        if featureset != "none":
            tpl_vars["localversion"] = "-" + featureset

        self.do_featureset_setup(tpl_vars, makeflags, arch, featureset, extra)
        self.do_featureset_makefile(makefile, arch, featureset, makeflags, extra)
        self.do_featureset_packages(
            packages, makefile, arch, featureset, tpl_vars, makeflags, extra
        )
        self.do_featureset_recurse(
            packages, makefile, arch, featureset, tpl_vars, makeflags, extra
        )

    def do_featureset_setup(self, tpl_vars, makeflags, arch, featureset, extra):
        del tpl_vars, makeflags, arch, featureset, extra

    def do_featureset_makefile(self, makefile, arch, featureset, makeflags, extra):
        del extra
        makeflags["FEATURESET"] = featureset

        for i in self.makefile_targets:
            target1 = f"{i}_{arch}"
            target2 = f"{target1}_{featureset}"
            target3 = f"{target2}_real"
            makefile.add(target1, [target2])
            makefile.add(target2, [target3])

    def do_featureset_packages(
        self, packages, makefile, arch, featureset, tpl_vars, makeflags, extra
    ):
        del packages, makefile, arch, featureset, tpl_vars, makeflags, extra

    def do_featureset_recurse(
        self, packages, makefile, arch, featureset, tpl_vars, makeflags, extra
    ):
        for flavour in iter_flavours(self.config, arch, featureset):
            self.do_flavour(
                packages,
                makefile,
                arch,
                featureset,
                flavour,
                tpl_vars.copy(),
                makeflags.copy(),
                extra,
            )

    def do_flavour(
        self, packages, makefile, arch, featureset, flavour, tpl_vars, makeflags, extra
    ):
        tpl_vars["localversion"] += "-" + flavour

        self.do_flavour_setup(tpl_vars, makeflags, arch, featureset, flavour, extra)
        self.do_flavour_makefile(makefile, arch, featureset, flavour, makeflags, extra)
        self.do_flavour_packages(
            packages, makefile, arch, featureset, flavour, tpl_vars, makeflags, extra
        )

    def do_flavour_setup(self, tpl_vars, makeflags, arch, featureset, flavour, extra):
        del arch, featureset, flavour, extra
        for i in (
            ("kernel-arch", "KERNEL_ARCH"),
            ("localversion", "LOCALVERSION"),
        ):
            if i[0] in tpl_vars:
                makeflags[i[1]] = tpl_vars[i[0]]

    def do_flavour_makefile(
        self, makefile, arch, featureset, flavour, makeflags, extra
    ):
        del extra
        makeflags["FLAVOUR"] = flavour

        for i in self.makefile_targets:
            target1 = f"{i}_{arch}_{featureset}"
            target2 = f"{target1}_{flavour}"
            target3 = f"{target2}_real"
            makefile.add(target1, [target2])
            makefile.add(target2, [target3])

    def do_flavour_packages(
        self, packages, makefile, arch, featureset, flavour, tpl_vars, makeflags, extra
    ):
        del packages, makefile, arch, featureset, flavour, tpl_vars, makeflags, extra

    def process_relation(self, dep, tpl_vars):
        import copy

        dep = copy.deepcopy(dep)
        for groups in dep:
            for item in groups:
                item.name = self.substitute(item.name, tpl_vars)
                if item.version:
                    item.version = self.substitute(item.version, tpl_vars)
        return dep

    def process_description(self, in_desc, tpl_vars):
        desc = in_desc.__class__()
        desc.short = self.substitute(in_desc.short, tpl_vars)
        for i in in_desc.long:
            desc.append(self.substitute(i, tpl_vars))
        return desc

    def process_package(self, in_entry, tpl_vars=None):
        if tpl_vars is None:
            tpl_vars = {}
        entry = in_entry.__class__()
        for key, value in in_entry.items():
            if isinstance(value, PackageRelation):
                value = self.process_relation(value, tpl_vars)
            elif isinstance(value, PackageDescription):
                value = self.process_description(value, tpl_vars)
            else:
                value = self.substitute(value, tpl_vars)
            entry[key] = value
        return entry

    def process_packages(self, entries, tpl_vars):
        return [self.process_package(i, tpl_vars) for i in entries]

    def substitute(self, s, tpl_vars):
        if isinstance(s, (list, tuple)):
            return [self.substitute(i, tpl_vars) for i in s]

        def subst(match):
            return tpl_vars[match.group(1)]

        return re.sub(r"@([-_a-z0-9]+)@", subst, str(s))

    # Substitute kernel version etc. into maintainer scripts,
    # bug presubj message and lintian overrides
    def substitute_debhelper_config(
        self, prefix, tpl_vars, package_name, output_dir="debian"
    ):
        for config_id in [
            "bug-presubj",
            "lintian-overrides",
            "maintscript",
            "postinst",
            "postrm",
            "preinst",
            "prerm",
        ]:
            name = f"{prefix}.{config_id}"
            try:
                template = self.templates[name]
            except KeyError:
                continue
            else:
                target = f"{output_dir}/{package_name}.{config_id}"
                with open(target, "w", encoding="utf-8") as f:
                    f.write(self.substitute(template, tpl_vars))
                    os.chmod(f.fileno(), self.templates.get_mode(name) & 0o777)

    def merge_build_depends(self, packages):
        # Merge Build-Depends pseudo-fields from binary packages into the
        # source package
        source = packages["source"]
        arch_all = PackageArchitecture("all")
        for name, package in packages.items():
            if name == "source":
                continue
            dep = package.get("Build-Depends")
            if not dep:
                continue
            del package["Build-Depends"]
            for group in dep:
                for item in group:
                    if package["Architecture"] != arch_all and not item.arches:
                        item.arches = sorted(package["Architecture"])
                    if package.get("Build-Profiles") and not item.restrictions:
                        profiles = package["Build-Profiles"]
                        assert profiles[0] == "<" and profiles[-1] == ">"
                        item.restrictions = re.split(r"\s+", profiles[1:-1])
            if package["Architecture"] == arch_all:
                dep_type = "Build-Depends-Indep"
            else:
                dep_type = "Build-Depends-Arch"
            if dep_type not in source:
                source[dep_type] = PackageRelation()
            source[dep_type].extend(dep)

    def write(self, packages, makefile):
        self.write_control(packages.values())
        self.write_makefile(makefile)

    def write_control(self, entries, name="debian/control"):
        with open(name, "w", encoding="utf-8") as f:
            self.write_rfc822(f, entries)

    def write_makefile(self, makefile, name="debian/rules.gen"):
        with open(name, "w", encoding="utf-8") as f:
            makefile.write(f)

    def write_rfc822(self, f, entries):
        for entry in entries:
            for key, value in entry.items():
                f.write(f"{key}: {value}\n")
            f.write("\n")


def merge_packages(packages, new, arch):
    for new_package in new:
        name = new_package["Package"]
        if name in packages:
            package = packages.get(name)
            package["Architecture"].add(arch)

            for field in ("Depends", "Provides", "Suggests", "Recommends", "Conflicts"):
                if field in new_package:
                    if field in package:
                        v = package[field]
                        v.extend(new_package[field])
                    else:
                        package[field] = new_package[field]

        else:
            new_package["Architecture"] = arch
            packages.append(new_package)
