# Copyright 2023 Robert Yang
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import os
import re

import pmb.aportgen
import pmb.build
import pmb.chroot
import pmb.helpers
import pmb.helpers.pmaports
import pmb.parse


def match_kbuild_out(word):
    """
    Look for paths in the following formats:
      "<prefix>/<kbuild_out>/arch/<arch>/boot"
      "<prefix>/<kbuild_out>/include/config/kernel.release"

    :param word: space separated string cut out from a line from an APKBUILD
                 function body that might be the kbuild output path
    :returns: kernel build output directory.
              empty string when a separate build output directory isn't used.
              None, when no output directory is found.
    """
    prefix = "^\\\"?\\$({?builddir}?|{?srcdir}?)\\\"?/"
    kbuild_out = "(.*\\/)*"

    postfix = "(arch\\/.*\\/boot.*)\\\"?$"
    match = re.match(prefix + kbuild_out + postfix, word)

    if match is None:
        postfix = "(include\\/config\\/kernel\\.release)\\\"?$"
        match = re.match(prefix + kbuild_out + postfix, word)

    if match is None:
        return None

    groups = match.groups()
    if groups is None or len(groups) != 3:
        return None

    logging.debug("word = " + str(word))
    logging.debug("regex match groups = " + str(groups))
    out_dir = groups[1]
    return "" if out_dir is None else out_dir.strip("/")


def find_kbuild_output_dir(function_body):
    """
    Guess what the kernel build output directory is. Parses each line of the
    function word by word, looking for paths which contain the kbuild output
    directory.

    :param function_body: contents of a function from the kernel APKBUILD
    :returns: kbuild output dir
              None, when output dir is not found
    """

    guesses = []
    for line in function_body:
        for item in line.split():
            kbuild_out = match_kbuild_out(item)
            if kbuild_out is not None:
                guesses.append(kbuild_out)
                break

    # Check if guesses are all the same
    it = iter(guesses)
    first = next(it, None)
    if first is None:
        raise RuntimeError("Couldn't find a kbuild out directory. Is your "
                           "APKBUILD messed up? If not, then consider "
                           "adjusting the patterns in pmb/build/envkernel.py "
                           "to work with your APKBUILD, or submit an issue.")
    if all(first == rest for rest in it):
        return first
    raise RuntimeError("Multiple kbuild out directories found. Can you modify "
                       "your APKBUILD so it only has one output path? If you "
                       "can't resolve it, please open an issue.")


def modify_apkbuild(args, pkgname, aport):
    """
    Modify kernel APKBUILD to package build output from envkernel.sh
    """
    apkbuild_path = aport + "/APKBUILD"
    apkbuild = pmb.parse.apkbuild(apkbuild_path)
    if os.path.exists(args.work + "/aportgen"):
        pmb.helpers.run.user(args, ["rm", "-r", args.work + "/aportgen"])

    pmb.helpers.run.user(args, ["mkdir", args.work + "/aportgen"])
    pmb.helpers.run.user(args, ["cp", "-r", apkbuild_path,
                         args.work + "/aportgen"])

    pkgver = pmb.build._package.get_pkgver(apkbuild["pkgver"],
                                           original_source=False)
    fields = {"pkgver": pkgver,
              "pkgrel": "0",
              "subpackages": "",
              "builddir": "/home/pmos/build/src"}

    pmb.aportgen.core.rewrite(args, pkgname, apkbuild_path, fields=fields)


def host_build_bindmount(args, chroot, flag_file, mount=False):
    """
    Check if the bind mount already exists and unmount it.
    Then bindmount the current directory into the chroot as
    /mnt/linux so it can be used by the envkernel abuild wrapper
    """
    flag_path = f"{chroot}/{flag_file}"
    if os.path.exists(flag_path):
        logging.info("Cleaning up kernel sources bind-mount")
        pmb.helpers.run.root(args, ["umount", chroot + "/mnt/linux"], check=False)
        pmb.helpers.run.root(args, ["rm", flag_path])

    if mount:
        pmb.helpers.mount.bind(args, ".", f"{chroot}/mnt/linux")
        pmb.helpers.run.root(args, ["touch", flag_path])


def run_abuild(args, pkgname, arch, apkbuild_path, kbuild_out):
    """
    Prepare build environment and run abuild.

    :param pkgname: package name of a linux kernel aport
    :param arch: architecture for the kernel
    :param apkbuild_path: path to APKBUILD of the kernel aport
    :param kbuild_out: kernel build system output sub-directory
    """
    chroot = args.work + "/chroot_native"
    build_path = "/home/pmos/build"
    kbuild_out_source = "/mnt/linux/.output"

    # If the kernel was cross-compiled on the host rather than with the envkernel
    # helper, we can still use the envkernel logic to package the artifacts for
    # development, making it easy to quickly sideload a new kernel or pmbootstrap
    # to create a boot image
    # This handles bind mounting the current directory (assumed to be kernel sources)
    # into the chroot so we can run abuild against it for the currently selected
    # devices kernel package.
    flag_file = "envkernel-bind-mounted"
    host_build = False

    if not pmb.helpers.mount.ismount(chroot + "/mnt/linux"):
        logging.info("envkernel.sh hasn't run, assuming the kernel was cross compiled"
                     "on host and using current dir as source")
        host_build = True

    host_build_bindmount(args, chroot, flag_file, mount=host_build)

    if not os.path.exists(chroot + kbuild_out_source):
        raise RuntimeError("No '.output' dir found in your kernel source dir. "
                           "Compile the " + args.device + " kernel first and "
                           "then try again. See https://postmarketos.org/envkernel"
                           "for details. If building on your host and only using "
                           "--envkernel for packaging, make sure you have O=.output "
                           "as an argument to make.")

    # Create working directory for abuild
    pmb.build.copy_to_buildpath(args, pkgname)

    # Create symlink from abuild working directory to envkernel build directory
    build_output = "" if kbuild_out == "" else "/" + kbuild_out
    if build_output != "":
        if os.path.islink(chroot + "/mnt/linux/" + build_output) and \
                os.path.lexists(chroot + "/mnt/linux/" + build_output):
            pmb.chroot.root(args, ["rm", "/mnt/linux/" + build_output])
        pmb.chroot.root(args, ["ln", "-s", "/mnt/linux",
                        build_path + "/src"])
    pmb.chroot.root(args, ["ln", "-s", kbuild_out_source,
                    build_path + "/src" + build_output])

    cmd = ["cp", apkbuild_path, chroot + build_path + "/APKBUILD"]
    pmb.helpers.run.root(args, cmd)

    # Create the apk package
    env = {"CARCH": arch,
           "CHOST": arch,
           "CBUILD": pmb.config.arch_native,
           "SUDO_APK": "abuild-apk --no-progress"}
    cmd = ["abuild", "rootpkg"]
    pmb.chroot.user(args, cmd, working_dir=build_path, env=env)

    # Clean up bindmount if needed
    host_build_bindmount(args, chroot, flag_file)

    # Clean up symlinks
    if build_output != "":
        if os.path.islink(chroot + "/mnt/linux/" + build_output) and \
                os.path.lexists(chroot + "/mnt/linux/" + build_output):
            pmb.chroot.root(args, ["rm", "/mnt/linux/" + build_output])
    pmb.chroot.root(args, ["rm", build_path + "/src"])


def package_kernel(args):
    """
    Frontend for 'pmbootstrap build --envkernel': creates a package from
    envkernel output.
    """
    pkgname = args.packages[0]
    if len(args.packages) > 1 or not pkgname.startswith("linux-"):
        raise RuntimeError("--envkernel needs exactly one linux-* package as "
                           "argument.")

    aport = pmb.helpers.pmaports.find(args, pkgname)

    modify_apkbuild(args, pkgname, aport)
    apkbuild_path = args.work + "/aportgen/APKBUILD"

    arch = args.deviceinfo["arch"]
    apkbuild = pmb.parse.apkbuild(apkbuild_path, check_pkgname=False)
    if apkbuild["_outdir"]:
        kbuild_out = apkbuild["_outdir"]
    else:
        function_body = pmb.parse.function_body(aport + "/APKBUILD", "package")
        kbuild_out = find_kbuild_output_dir(function_body)
    suffix = pmb.build.autodetect.suffix(apkbuild, arch)

    # Install package dependencies
    depends, _ = pmb.build._package.build_depends(
        args, apkbuild, pmb.config.arch_native, strict=False)
    pmb.build.init(args, suffix)
    if pmb.parse.arch.cpu_emulation_required(arch):
        depends.append("binutils-" + arch)
    pmb.chroot.apk.install(args, depends, suffix)

    output = (arch + "/" + apkbuild["pkgname"] + "-" + apkbuild["pkgver"] +
              "-r" + apkbuild["pkgrel"] + ".apk")
    message = "(" + suffix + ") build " + output
    logging.info(message)

    run_abuild(args, pkgname, arch, apkbuild_path, kbuild_out)
    pmb.build.other.index_repo(args, arch)
