#!/usr/bin/env python3

"""explore-kconfig - Obtain build attributes of configuration variants

explore-kconfig obtains build attributes such as ROM or RAM usage of
configuration variants for a given software project. It works on random
configurations (--random) or in the neighbourhood of existing configurations
(--neighbourhood).

Supported projects must be configurable via kconfig and provide a command which
outputs a JSON dict of build attributes on stdout. Use
--{clean,build,attribute}-command to configure explore-kconfig for a project.

explore-kconfig places the experiment results (containing configurations, build
logs, and correspondnig attributes) in the current working directory. Use
analyze-kconfig to build a model once data acquisition is complete.
"""

import argparse
import logging
import os
import sys
import time

from dfatool import kconfig


def main():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__
    )
    parser.add_argument(
        "--neighbourhood",
        type=str,
        help="Explore neighbourhood of provided .config file(s)",
    )
    parser.add_argument(
        "--log-level",
        default=logging.INFO,
        type=lambda level: getattr(logging, level.upper()),
        help="Set log level",
    )
    parser.add_argument(
        "--enumerate",
        action="store_true",
        help="Enumerate all valid configurations (DEPRECATED and UNMAINTAINED)",
    )
    parser.add_argument(
        "--random",
        type=int,
        help="Explore a number of random configurations (make randconfig)",
    )
    parser.add_argument(
        "--random-int",
        action="store_true",
        help="Randomize integers after running make randconfig",
    )
    parser.add_argument(
        "--with-neighbourhood",
        action="store_true",
        help="Explore neighbourhood of successful random configurations",
    )
    parser.add_argument(
        "--repeatable",
        action="store_true",
        help="Allow repeated measurements of already benchmarked configurations",
    )
    parser.add_argument(
        "--repeat",
        type=int,
        metavar="N",
        help="Run each benchmark N times. Implies --repeatable",
    )
    parser.add_argument(
        "--clean-command", type=str, help="Clean command", default="make clean"
    )
    parser.add_argument(
        "--build-command", type=str, help="Build command", default="make"
    )
    parser.add_argument(
        "--attribute-command",
        type=str,
        help="Attribute extraction command",
        default="make nfpvalues",
    )
    parser.add_argument(
        "--randconfig-command",
        type=str,
        help="Randconfig command for --random",
        default="make randconfig",
    )
    parser.add_argument(
        "--kconfig-file", type=str, help="Kconfig file", default="Kconfig"
    )
    parser.add_argument("project_root", type=str, help="Project root directory")

    args = parser.parse_args()

    if isinstance(args.log_level, int):
        logging.basicConfig(level=args.log_level)
    else:
        print(f"Invalid log level. Setting log level to INFO.", file=sys.stderr)

    kconf = kconfig.KConfig(args.project_root)

    if args.clean_command:
        kconf.clean_command = args.clean_command
    if args.build_command:
        kconf.build_command = args.build_command
    if args.attribute_command:
        kconf.attribute_command = args.attribute_command
    if args.randconfig_command:
        kconf.randconfig_command = args.randconfig_command
    if args.kconfig_file:
        kconf.kconfig = args.kconfig_file
    if args.repeatable:
        kconf.repeatable = args.repeatable
    if args.repeat:
        kconf.repeat = args.repeat - 1
        kconf.repeatable = True

    kconf.run_nfpkeys()

    if args.enumerate:
        kconf.enumerate()

    if args.random:
        num_successful = 0
        total_randconfig_seconds = 0
        # Assumption: At least 1% of builds are successful
        for i in range(args.random * 100):
            logging.info(f"Running randconfig {num_successful+1} of {args.random}")
            if total_randconfig_seconds and num_successful:
                seconds_per_randconfig = total_randconfig_seconds / num_successful
                remaining_minutes = (
                    int(seconds_per_randconfig * (args.random - num_successful)) // 60
                )
                logging.info(
                    f"Estimated remaining exploration time: {remaining_minutes // (24*60):2d} days {(remaining_minutes % (24*60)) // 60:2d} hours {remaining_minutes % 60:2d} minutes"
                )
            randconfig_start_time = time.time()
            status = kconf.run_randconfig(with_random_int=args.random_int)
            if status["success"]:
                num_successful += 1
            if args.with_neighbourhood and status["success"]:
                config_filename = status["config_path"]
                logging.debug(f"Exploring neighbourhood of {config_filename}")
                kconf.run_exploration_from_file(
                    config_filename, with_initial_config=False
                )
            total_randconfig_seconds += time.time() - randconfig_start_time
            if num_successful == args.random:
                break

    if args.neighbourhood:
        if os.path.isfile(args.neighbourhood):
            kconf.run_exploration_from_file(args.neighbourhood)
        elif os.path.isdir(args.neighbourhood):
            for filename in os.listdir(args.neighbourhood):
                config_filename = f"{args.neighbourhood}/{filename}"
                logging.debug(f"Exploring neighbourhood of {config_filename}")
                kconf.run_exploration_from_file(config_filename)
        else:
            print(
                f"--neighbourhod: Error: {args.neighbourhood} must be a file or directory, but is neither",
                file=sys.stderr,
            )


if __name__ == "__main__":
    main()