# SPDX-License-Identifier: Apache-2.0
# Copyright 2021 EPAM Systems
"""
Yocto builder module
"""

import os.path
import shlex
from typing import List, Tuple, cast
from moulin.utils import create_stamp_name, construct_fetcher_dep_cmd
from moulin import ninja_syntax
from moulin.yaml_wrapper import YamlValue
from moulin.yaml_helpers import YAMLProcessingError


def get_builder(conf: YamlValue, name: str, build_dir: str, src_stamps: List[str],
                generator: ninja_syntax.Writer):
    """
    Return configured YoctoBuilder class
    """
    return YoctoBuilder(conf, name, build_dir, src_stamps, generator)


def gen_build_rules(generator: ninja_syntax.Writer):
    """
    Generate yocto build rules for ninja
    """
    # Create build dir by calling poky/oe-init-build-env script
    cmd = " && ".join([
        "cd $yocto_dir",
        "source poky/oe-init-build-env $work_dir",
    ])
    generator.rule("yocto_init_env",
                   command=f'bash -c "{cmd}"',
                   description="Initialize Yocto build environment",
                   restat=True)
    generator.newline()

    # Add bitbake layers by calling bitbake-layers script
    cmd = " && ".join([
        "cd $yocto_dir",
        "source poky/oe-init-build-env $work_dir",
        "bitbake-layers add-layer $layers",
        "touch $out",
    ])
    generator.rule("yocto_add_layers",
                   command=f'bash -c "{cmd}"',
                   description="Add yocto layers",
                   pool="console",
                   restat=True)
    generator.newline()

    # Write own configuration to moulin.conf. Include it in local.conf
    cmd = " && ".join([
        "cd $yocto_dir",
        "echo '# Code generated by moulin. All manual changes will be lost' > $work_dir/conf/moulin.conf",
        "for x in $conf; do echo $$x >> $work_dir/conf/moulin.conf; done",
        "sed \"/require moulin\\.conf/d\" -i $work_dir/conf/local.conf",
        "echo 'require moulin.conf' >> $work_dir/conf/local.conf",
        "touch -r $work_dir/conf/bblayers.conf $work_dir/conf/local.conf",
    ])
    generator.rule(
        "yocto_update_conf",
        command=cmd,
        description="Update local.conf",
    )
    generator.newline()

    # Invoke bitbake. This rule uses "console" pool so we can see the bitbake output.
    cmd = " && ".join([
        # Generate fetcher dependency file
        construct_fetcher_dep_cmd(),
        "cd $yocto_dir",
        "source poky/oe-init-build-env $work_dir",
        "bitbake $target",
    ])
    generator.rule("yocto_build",
                   command=f'bash -c "{cmd}"',
                   description="Yocto Build: $name",
                   pool="console",
                   deps="gcc",
                   depfile=".moulin_$name.d",
                   restat=True)


def _flatten_yocto_conf(conf: YamlValue) -> List[Tuple[str, str]]:
    """
    Flatten conf entries. While using YAML *entries syntax, we will get list of conf
    entries inside of other list. To overcome this, we need to move inner list 'up'
    """

    # Problem is conf entries that it is list itself
    result: List[Tuple[str, str]] = []
    for entry in conf:
        if not entry.is_list:
            raise YAMLProcessingError("Exptected array on 'conf' node", entry.mark)
        if entry[0].is_list:
            result.extend([(x[0].as_str, x[1].as_str) for x in entry])
        else:
            result.append((entry[0].as_str, entry[1].as_str))
    return result


class YoctoBuilder:
    """
    YoctoBuilder class generates Ninja rules for given build configuration
    """
    def __init__(self, conf: YamlValue, name: str, build_dir: str, src_stamps: List[str],
                 generator: ninja_syntax.Writer):
        self.conf = conf
        self.name = name
        self.generator = generator
        self.src_stamps = src_stamps
        # With yocto builder it is possible to have multiple builds with the same set of
        # layers. Thus, we have two variables - build_dir and work_dir
        # - yocto_dir is the upper directory where layers are stored. Basically, we should
        #   have "poky" in our yocto_dir
        # - work_dir is the build directory where we can find conf/local.conf, tmp and other
        #   directories. It is called "build" by default
        self.yocto_dir = build_dir
        self.work_dir: str = conf.get("work_dir", "build").as_str

    def _get_external_src(self) -> List[Tuple[str, str]]:
        external_src_node = self.conf.get("external_src", None)
        if not external_src_node:
            return []

        ret: List[Tuple[str, str]] = []
        for key, val_node in cast(YamlValue, external_src_node).items():
            if val_node.is_list:
                path = os.path.join(*[x.as_str for x in val_node])
            else:
                path = val_node.as_str
            path = os.path.abspath(path)
            ret.append((f"EXTERNALSRC:pn-{key}", path))

        return ret

    def gen_build(self):
        """Generate ninja rules to build yocto/poky"""
        common_variables = {
            "yocto_dir": self.yocto_dir,
            "work_dir": self.work_dir,
        }

        # First we need to ensure that "conf" dir exists
        env_target = os.path.join(self.yocto_dir, self.work_dir, "conf", "local.conf")
        self.generator.build(env_target,
                             "yocto_init_env",
                             self.src_stamps,
                             variables=common_variables)

        # Then we need to add layers
        layers_node = self.conf.get("layers", None)
        if layers_node:
            layers_stamp = create_stamp_name(self.yocto_dir, self.work_dir, "yocto", "layers")
            layers = " ".join([x.as_str for x in layers_node])
            self.generator.build(layers_stamp,
                                 "yocto_add_layers",
                                 env_target,
                                 variables=dict(common_variables, layers=layers))

        # Next - update local.conf
        local_conf_target = os.path.join(self.yocto_dir, self.work_dir, "conf", "moulin.conf")
        local_conf_node = self.conf.get("conf", None)
        if local_conf_node:
            local_conf = _flatten_yocto_conf(local_conf_node)
        else:
            local_conf = []

        # Handle external sources (like build artifacts from some other build)
        local_conf.extend(self._get_external_src())

        # '$' is a ninja escape character so we need to quote it
        local_conf_lines = [
            shlex.quote(f'{k.replace("$", "$$")} = "{v.replace("$", "$$")}"') for k, v in local_conf
        ]

        self.generator.build(local_conf_target,
                             "yocto_update_conf",
                             layers_stamp if layers_node else env_target,
                             variables=dict(common_variables, conf=" ".join(local_conf_lines)))
        self.generator.newline()

        self.generator.build(f"conf-{self.name}", "phony", local_conf_target)
        self.generator.newline()

        # Next step - invoke bitbake. At last :)
        targets = self.get_targets()
        additional_deps_node = self.conf.get("additional_deps", None)
        if additional_deps_node:
            deps = [os.path.join(self.yocto_dir, d.as_str) for d in additional_deps_node]
        else:
            deps = []
        deps.append(local_conf_target)
        self.generator.build(targets,
                             "yocto_build",
                             deps,
                             variables=dict(common_variables,
                                            target=self.conf["build_target"].as_str,
                                            name=self.name))

        return targets

    def get_targets(self):
        "Return list of targets that are generated by this build"
        return [
            os.path.join(self.yocto_dir, self.work_dir, t.as_str)
            for t in self.conf["target_images"]
        ]

    def capture_state(self):
        """
        Update stored local conf with actual SRCREVs for VCS-based recipes.
        This should ensure that we can reproduce this exact build later
        """
