# Copyright 2022 Attila Szollosi
# SPDX-License-Identifier: GPL-3.0-or-later
import glob
import logging
import re
import os

import pmb.build
import pmb.config
import pmb.parse
import pmb.helpers.pmaports


def is_set(config, option):
    """
    Check, whether a boolean or tristate option is enabled
    either as builtin or module.
    """
    return re.search("^CONFIG_" + option + "=[ym]$", config, re.M) is not None


def is_set_str(config, option, string):
    """
    Check, whether a config option contains a string as value.
    """
    match = re.search("^CONFIG_" + option + "=\"(.*)\"$", config, re.M)
    if match:
        return string == match.group(1)
    else:
        return False


def is_in_array(config, option, string):
    """
    Check, whether a config option contains string as an array element
    """
    match = re.search("^CONFIG_" + option + "=\"(.*)\"$", config, re.M)
    if match:
        values = match.group(1).split(",")
        return string in values
    else:
        return False


def check_option(component, details, config, config_path_pretty, option,
                 option_value):
    link = (f"https://wiki.postmarketos.org/wiki/kconfig#CONFIG_{option}")
    warning_no_details = (f"WARNING: {config_path_pretty} isn't"
                          f" configured properly for {component}, run"
                          f" 'pmbootstrap kconfig check' for details!")
    if isinstance(option_value, list):
        for string in option_value:
            if not is_in_array(config, option, string):
                if details:
                    logging.info(f"WARNING: {config_path_pretty}:"
                                 f' CONFIG_{option} should contain "{string}".'
                                 f" See <{link}> for details.")
                else:
                    logging.warning(warning_no_details)
                return False
    elif isinstance(option_value, str):
        if not is_set_str(config, option, option_value):
            if details:
                logging.info(f"WARNING: {config_path_pretty}: CONFIG_{option}"
                             f' should be set to "{option_value}".'
                             f" See <{link}> for details.")
            else:
                logging.warning(warning_no_details)
            return False
    elif option_value in [True, False]:
        if option_value != is_set(config, option):
            if details:
                should = "should" if option_value else "should *not*"
                logging.info(f"WARNING: {config_path_pretty}: CONFIG_{option}"
                             f" {should} be set. See <{link}> for details.")
            else:
                logging.warning(warning_no_details)
            return False
    else:
        raise RuntimeError("kconfig check code can only handle booleans,"
                           f" strings and arrays. Given value {option_value}"
                           " is not supported. If you need this, please patch"
                           " pmbootstrap or open an issue.")
    return True


def check_config(config_path, config_path_pretty, config_arch, pkgver,
                 waydroid=False,
                 iwd=False,
                 nftables=False,
                 containers=False,
                 zram=False,
                 netboot=False,
                 community=False,
                 uefi=False,
                 details=False):
    logging.debug(f"Check kconfig: {config_path}")
    with open(config_path) as handle:
        config = handle.read()

    components = {"postmarketOS": pmb.config.necessary_kconfig_options}
    if waydroid:
        components["waydroid"] = pmb.config.necessary_kconfig_options_waydroid
    if iwd:
        components["iwd"] = pmb.config.necessary_kconfig_options_iwd
    if nftables:
        components["nftables"] = pmb.config.necessary_kconfig_options_nftables
    if containers:
        components["containers"] = \
            pmb.config.necessary_kconfig_options_containers
    if zram:
        components["zram"] = pmb.config.necessary_kconfig_options_zram
    if netboot:
        components["netboot"] = pmb.config.necessary_kconfig_options_netboot
    if community:
        components["waydroid"] = pmb.config.necessary_kconfig_options_waydroid
        components["iwd"] = pmb.config.necessary_kconfig_options_iwd
        components["nftables"] = pmb.config.necessary_kconfig_options_nftables
        components["containers"] = \
            pmb.config.necessary_kconfig_options_containers
        components["zram"] = pmb.config.necessary_kconfig_options_zram
        components["netboot"] = pmb.config.necessary_kconfig_options_netboot
    if uefi:
        components["uefi"] = pmb.config.necessary_kconfig_options_uefi

    results = [check_config_options_set(config, config_path_pretty,
                                        config_arch, options, component,
                                        pkgver, details)
               for component, options in components.items()]
    return all(results)


def check_config_options_set(config, config_path_pretty, config_arch, options,
                             component, pkgver, details=False):
    # Loop through necessary config options, and print a warning,
    # if any is missing
    ret = True
    for rules, archs_options in options.items():
        # Skip options irrelevant for the current kernel's version
        # Example rules: ">=4.0 <5.0"
        skip = False
        for rule in rules.split(" "):
            if not pmb.parse.version.check_string(pkgver, rule):
                skip = True
                break
        if skip:
            continue

        for archs, options in archs_options.items():
            if archs != "all":
                # Split and check if the device's architecture architecture has
                # special config options. If option does not contain the
                # architecture of the device kernel, then just skip the option.
                architectures = archs.split(" ")
                if config_arch not in architectures:
                    continue

            for option, option_value in options.items():
                if not check_option(component, details, config,
                                    config_path_pretty, option, option_value):
                    ret = False
                    if not details:
                        break  # do not give too much error messages
    return ret


def check(args, pkgname,
          force_waydroid_check=False,
          force_iwd_check=False,
          force_nftables_check=False,
          force_containers_check=False,
          force_zram_check=False,
          force_netboot_check=False,
          force_community_check=False,
          force_uefi_check=False,
          details=False,
          must_exist=True):
    """
    Check for necessary kernel config options in a package.

    :returns: True when the check was successful, False otherwise
              None if the aport cannot be found (only if must_exist=False)
    """
    # Pkgname: allow omitting "linux-" prefix
    if pkgname.startswith("linux-"):
        flavor = pkgname.split("linux-")[1]
    else:
        flavor = pkgname

    # Read all kernel configs in the aport
    ret = True
    aport = pmb.helpers.pmaports.find(args, "linux-" + flavor, must_exist=must_exist)
    if aport is None:
        return None
    apkbuild = pmb.parse.apkbuild(f"{aport}/APKBUILD")
    pkgver = apkbuild["pkgver"]
    check_waydroid = force_waydroid_check or (
        "pmb:kconfigcheck-waydroid" in apkbuild["options"])
    check_iwd = force_iwd_check or (
        "pmb:kconfigcheck-iwd" in apkbuild["options"])
    check_nftables = force_nftables_check or (
        "pmb:kconfigcheck-nftables" in apkbuild["options"])
    check_containers = force_containers_check or (
        "pmb:kconfigcheck-containers" in apkbuild["options"])
    check_zram = force_zram_check or (
        "pmb:kconfigcheck-zram" in apkbuild["options"])
    check_netboot = force_netboot_check or (
        "pmb:kconfigcheck-netboot" in apkbuild["options"])
    check_community = force_community_check or (
        "pmb:kconfigcheck-community" in apkbuild["options"])
    check_uefi = force_uefi_check or (
        "pmb:kconfigcheck-uefi" in apkbuild["options"])
    for config_path in glob.glob(aport + "/config-*"):
        # The architecture of the config is in the name, so it just needs to be
        # extracted
        config_name = os.path.basename(config_path)
        config_name_split = config_name.split(".")

        if len(config_name_split) != 2:
            raise RuntimeError(f"{config_name} is not a valid kernel config "
                               "name. Ensure that the _config property in your "
                               "kernel APKBUILD has a . before the "
                               "architecture name, e.g. .aarch64 or .armv7, "
                               "and that there is no excess punctuation "
                               "elsewhere in the name.")

        config_arch = config_name_split[1]
        config_path_pretty = f"linux-{flavor}/{os.path.basename(config_path)}"
        ret &= check_config(config_path, config_path_pretty, config_arch,
                            pkgver,
                            waydroid=check_waydroid,
                            iwd=check_iwd,
                            nftables=check_nftables,
                            containers=check_containers,
                            zram=check_zram,
                            netboot=check_netboot,
                            community=check_community,
                            uefi=check_uefi,
                            details=details)
    return ret


def extract_arch(config_file):
    # Extract the architecture out of the config
    with open(config_file) as f:
        config = f.read()
    if is_set(config, "ARM"):
        return "armv7"
    elif is_set(config, "ARM64"):
        return "aarch64"
    elif is_set(config, "X86_32"):
        return "x86"
    elif is_set(config, "X86_64"):
        return "x86_64"

    # No match
    logging.info("WARNING: failed to extract arch from kernel config")
    return "unknown"


def extract_version(config_file):
    # Try to extract the version string out of the comment header
    with open(config_file) as f:
        # Read the first 3 lines of the file and get the third line only
        text = [next(f) for x in range(3)][2]
    ver_match = re.match(r"# Linux/\S+ (\S+) Kernel Configuration", text)
    if ver_match:
        return ver_match.group(1)

    # No match
    logging.info("WARNING: failed to extract version from kernel config")
    return "unknown"


def check_file(config_file, waydroid=False, nftables=False,
               containers=False, zram=False, netboot=False,
               community=False, uefi=False, details=False):
    """
    Check for necessary kernel config options in a kconfig file.

    :returns: True when the check was successful, False otherwise
    """
    arch = extract_arch(config_file)
    version = extract_version(config_file)
    logging.debug(f"Check kconfig: parsed arch={arch}, version={version} from "
                  f"file: {config_file}")
    return check_config(config_file, config_file, arch, version,
                        waydroid=waydroid,
                        nftables=nftables,
                        containers=containers,
                        zram=zram,
                        netboot=netboot,
                        community=community,
                        uefi=uefi,
                        details=details)
