#!python
# Use the package from the current directory by default
import sys
import os
SCRIPT_ROOT = os.path.dirname(__file__)
SBUILDR_ROOT = os.path.abspath(os.path.join(SCRIPT_ROOT, os.path.pardir))
sys.path.insert(0, SBUILDR_ROOT)

from sbuildr.project.target import ProjectTarget
from sbuildr.project.project import Project
from sbuildr.logger import G_LOGGER, SBuildrException
from sbuildr.misc import paths, utils
import sbuildr.logger as logger
import sbuildr

from contextlib import redirect_stderr, redirect_stdout
from typing import List, Tuple
import subprocess
import argparse
import shutil
import sys
import io


# Given target names, returns the corresponding targets.
def select_targets(project, args, search_dicts=["libraries", "executables", "tests"]) -> List[ProjectTarget]:
    targets = []
    dicts = {attr: getattr(project, attr) for attr in search_dicts}
    for tgt_name in args.targets:
        target_found = False

        for attr, map in dicts.items():
            if tgt_name in map:
                G_LOGGER.verbose(f"Found target: {tgt_name} in project.{attr}")
                targets.append(map[tgt_name])
                if target_found:
                    G_LOGGER.warning(f"Target name: {tgt_name} refers to multiple targets, selecting all.")
                target_found = True

        if not target_found:
            msg = f"Could not find target: {tgt_name} in project. Available targets:"
            for attr, map in dicts.items():
                msg += f"\n\t{attr}: {map.keys()}"
            G_LOGGER.critical(msg)
    return targets

# Sets up the the command-line interface for the given project/generator combination.
# When no profile(s) are specified, default_profile will be used.
def add_project_specific_subcommands(project: Project, parser: argparse.ArgumentParser, subparsers) -> int:
    """
    Adds the SBuildr command-line interface to the Python script invoking this function. For detailed usage information, you can run the Python code invoking this function with ``--help``.

    :param project: The project that the CLI will interface with.
    """
    # Given argparse's args struct, parses out profile flags, and returns a list of profile names included.
    def select_profile_names(args) -> List[str]:
        return [prof_name for prof_name in project.profiles.keys() if getattr(args, prof_name)]


    def help(args):
        targets = select_targets(project, args) or project.all_targets()
        G_LOGGER.info(f"\n{utils.wrap_str(' Targets ')}")
        for target in targets:
            G_LOGGER.info(f"Target: {target}{'(internal)' if target.internal else ''}. Available Profiles:")
            for prof, node in target.items():
                G_LOGGER.info(f"\tProfile: {prof}. Path: {node.path}.")
        G_LOGGER.info(f"\n{utils.wrap_str(' Public Interface ')}")
        G_LOGGER.info(f"Headers: {project.public_headers}")


    def try_build(targets: List[ProjectTarget], profile_names: List[str]):
        try:
            project.build(targets, profile_names)
        except SBuildrException:
            G_LOGGER.critical(f"Could not build project. Has this project been configured?")


    def build(args) -> Tuple[List[ProjectTarget], List[str]]:
        targets = select_targets(project, args) or project.all_targets()
        profile_names = select_profile_names(args) or project.all_profile_names()
        try_build(targets, profile_names)


    def run(args):
        args.targets = [args.target]
        targets = select_targets(project, args, search_dicts=["executables"])
        profile_names = select_profile_names(args) or project.all_profile_names()
        try_build(targets, profile_names)
        project.run(targets, profile_names)


    def tests(args):
        tests = select_targets(project, args, search_dicts=["tests"]) or project.test_targets()
        profile_names = select_profile_names(args) or project.all_profile_names()
        try_build(tests, profile_names)
        project.run_tests(tests, profile_names)


    def get_install_targets(args):
        headers = [tgt for tgt in args.targets if tgt not in project] or list(project.public_headers)
        args.targets = [tgt for tgt in args.targets if tgt in project]
        targets = [tgt for tgt in (select_targets(project, args) or project.all_targets()) if not tgt.internal]
        G_LOGGER.verbose(f"Selected public targets: {targets}")
        profile_names = select_profile_names(args) or ["release"]
        G_LOGGER.verbose(f"Targets: {targets} for profiles: {profile_names}")
        G_LOGGER.verbose(f"Headers: {headers}")
        return targets, profile_names, headers


    def install(args):
        targets, profile_names, headers = get_install_targets(args)
        try_build(targets, profile_names)
        project.install(targets, profile_names, headers, args.headers, args.libraries, args.executables, dry_run=not args.force)


    def uninstall(args):
        targets, profile_names, headers = get_install_targets(args)
        project.uninstall(targets, profile_names, headers, args.headers, args.libraries, args.executables, dry_run=not args.force)


    def clean(args):
        project.clean(nuke=args.nuke, dry_run=not args.force)
        if args.force and args.nuke:
            shutil.rmtree(args.project_file, ignore_errors=True)
            G_LOGGER.info(f"Removed exported project file: {args.project_file}")


    def add_profile_args(parser_like, verb: str):
        for prof_name in project.profiles.keys():
            parser_like.add_argument(f"--{prof_name}", help=f"{verb} targets for the {prof_name} profile", action="store_true")


    # Help
    help_parser = subparsers.add_parser("help", help="Display information about available targets and public headers", description="Display information about the targets and public headers in this project")
    help_parser.add_argument("targets", nargs='*', help="Targets to display. By default, displays help information for all targets.", default=[])
    help_parser.set_defaults(func=help)


    # Build
    build_parser = subparsers.add_parser("build", help="Build project targets", description="Build one or more project targets")
    build_parser.add_argument("targets", nargs='*', help="Targets to build. By default, builds all targets for the default profiles.", default=[])
    add_profile_args(build_parser, "Build")
    build_parser.set_defaults(func=build)


    # Run
    run_parser = subparsers.add_parser("run", help="Run a project executable", description="Run a project executable. To run tests, 'sbuildr tests' should be used instead.")
    run_parser.add_argument("target", help="Target corresponding to an executable")
    add_profile_args(run_parser, "Run")
    run_parser.set_defaults(func=run)


    # Test
    tests_parser = subparsers.add_parser("test", help="Run project tests", description="Run one or more project tests")
    tests_parser.add_argument("targets", nargs='*', help="Targets to test. By default, tests all targets for all profiles.", default=[])
    add_profile_args(tests_parser, "Test")
    tests_parser.set_defaults(func=tests)


    def add_installation_dir_args(parser_like):
        parser_like.add_argument("-I", "--headers", help="Installation directory for headers", default=paths.default_header_install_path())
        parser_like.add_argument("-L", "--libraries", help="Installation directory for libraries", default=paths.default_library_install_path())
        parser_like.add_argument("-X", "--executables", help="Installation directory for executables", default=paths.default_executable_install_path())


    # Install
    install_parser = subparsers.add_parser("install", help="Install project targets", description="Install one or more project targets. Uses only the release profile by default.")
    install_parser.add_argument("targets", nargs='*', help="Targets to install. By default, installs all targets and headers specified.", default=[])
    install_parser.add_argument("-f", "--force", help="Copies targets. Without this flag, install will only do a dry-run", action="store_true")
    add_installation_dir_args(install_parser)
    add_profile_args(install_parser, "Install")
    install_parser.set_defaults(func=install)


    # Uninstall
    uninstall_parser = subparsers.add_parser("uninstall", help="Uninstall project targets", description="Uninstall one or more project targets. Uses only the release profile by default.")
    uninstall_parser.add_argument("-f", "--force", help="Remove targets. Without this flag, uninstall will only do a dry-run", action="store_true")
    uninstall_parser.add_argument("targets", nargs='*', help="Targets to uninstall. By default, uninstalls all targets and headers specified.", default=[])
    add_installation_dir_args(uninstall_parser)
    add_profile_args(uninstall_parser, "Uninstall")
    uninstall_parser.set_defaults(func=uninstall)


    # Clean
    clean_parser = subparsers.add_parser("clean", help="Clean project targets", description="Clean one or more project targets. By default, cleans all targets for the default profiles.")
    clean_parser.add_argument("--nuke", help="The nuclear option. Removes the entire build directory, including all targets for all profiles, meaning that the project must be reconfigured before subsequent builds.", action="store_true")
    clean_parser.add_argument("-f", "--force", help="Removes targets. Without this flag, clean will only do a dry-run.", action="store_true")
    clean_parser.set_defaults(func=clean)


    args, unknown = parser.parse_known_args()
    # Dispatch
    if hasattr(args, "func"):
        args.func(args)
    return 0


def add_project_generic_subcommands(args, parser: argparse.ArgumentParser) -> int:
    def exit_help():
        parser.print_help()
        sys.exit(0)

    def load_project(args) -> Project:
        if not os.path.exists(args.project_file):
            G_LOGGER.error(f"Saved project: {args.project_file} does not exist. Has the project been configured? Please provide a path to the saved project using the -p/--project-file option. ")
            exit_help()

        return Project.load(args.project_file)

    def configure(args) -> Project:
        args.build_script = os.path.abspath(args.build_script)
        if not os.path.exists(args.build_script):
            G_LOGGER.error(f"Specified build script: {args.build_script} does not exist")
            exit_help()

        status = subprocess.run([sys.executable, args.build_script], capture_output=True)
        if status.returncode:
            G_LOGGER.critical(f"Failed to run build script with:\n{utils.subprocess_output(status)}")
        else:
            G_LOGGER.info(f"Successfully ran build script:\n{utils.subprocess_output(status)}")

        project = load_project(args)

        targets = select_targets(project, args) or project.all_targets()
        profile_names = project.all_profile_names()

        project.configure(targets, profile_names)
        # Save the configured project
        project.export(args.project_file)
        return project

    subparsers = parser.add_subparsers()

    # Configure
    configure_parser = subparsers.add_parser("configure", help="Configure the project for build", description="Configures the project for building. This includes fetching dependencies, building the project graph, and configuring a backend. When invoked with no arguments, automatically performed these three actions for all project targets.")
    configure_parser.add_argument("-b", "--build-script", help="Path to the build script that exports the project. If the script exports the project to a non-default path, the path should be specified to sbuildr with the -p/--project-file option.", default="build.py")
    configure_parser.add_argument("targets", nargs='*', help="Targets for which to configure. By default, configures for all targets in the project.")
    configure_parser.set_defaults(configure_called=True)

    def configure_called(args):
        return hasattr(args, "configure_called") or "configure" in sys.argv

    try:
        with redirect_stderr(io.StringIO()), redirect_stdout(io.StringIO()):
            args, _ = parser.parse_known_args()
    except SystemExit:
        if configure_called(args) and args.help:
            configure_parser.print_help()
            sys.exit(0)
        # This means a subcommand other than configure was called, so proceed as normal.
        pass

    project = configure(args) if configure_called(args) else load_project(args)
    if project.PROJECT_API_VERSION != Project.PROJECT_API_VERSION:
        G_LOGGER.critical(f"This project has an older API version. System Project API version: {Project.PROJECT_API_VERSION}, Project version: {project.PROJECT_API_VERSION}. Please reconfigure the project.")

    status = add_project_specific_subcommands(project, parser, subparsers)
    return status


def main():
    # The project needs to be loaded before the main parser can take over. However, with add_help=True, the help
    # message for the CLI parser will not display, so use this workaround to get both help messages to show.
    parser = argparse.ArgumentParser(add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument("-h", "--help", help="Show this help message and exit.", action="store_true")
    parser.add_argument("-p", "--project-file", help="A path to a saved project file.", default=os.path.abspath(os.path.join("build", Project.DEFAULT_SAVED_PROJECT_NAME)))
    parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose logging.")
    parser.add_argument("-vv", "--very-verbose", action="store_true", help="Enable very verbose logging.")

    # Parse without any subparsers to get global options (subparses will block all parsing if an unrecognized subcommand is used)
    args, _ = parser.parse_known_args()
    if args.very_verbose:
        G_LOGGER.verbosity = logger.Verbosity.VERBOSE
    elif args.verbose:
        G_LOGGER.verbosity = logger.Verbosity.DEBUG
    args.project_file = os.path.abspath(args.project_file)

    status = add_project_generic_subcommands(args, parser)
    if args.help:
        parser.print_help()
        return 0
    return status


if __name__ == '__main__':
    sys.exit(main())
