#! /usr/bin/env python3

import os
import sys
import re

import subprocess
import argparse

from enum import Enum

"""
Configures a testsuite and rewrites the build with custom compilation commands.
"""

# Constant with the dirname of this script
THIS_DIR = os.path.dirname(os.path.abspath(__file__)) + "/"


def _main():
    """
    Main function
    """
    arg_parser = _build_arg_parser()

    if "-h" in sys.argv or "--help" in sys.argv or "-?" in sys.argv or len(sys.argv) == 1:
        arg_parser.print_help()
        exit(0)

    args = arg_parser.parse_args()

    if args.fast_dest is not None:
        destination_folder = os.path.abspath(os.path.join(THIS_DIR, args.fast_dest))
    else:
        destination_folder = os.path.abspath(args.dest)

    # Configure the test suite
    run_cmake(destination_folder, args.clang, args.test_suite, args.z)

    # Rewrite Ninja rules
    if not args.no_rewrite:
        rewrite_ninja(os.path.join(destination_folder, "rules.ninja"), args.script)

def build_path(file):
    """
    Returns the absolute path for the given file, relatively to this script
    """
    return os.path.abspath(os.path.join(THIS_DIR, file))


def run_cmake(folder, clang_path, test_suite_path, o0_cmake):
    """
    Create the output folder and configure the test suite.
    :param folder:          Output folder
    :param clang_path:      Path to the clang binary
    :param test_suite_path: Test suite source folder
    :param o0_cmake:        CMake configuration file with -O0
    """

    if clang_path is None:
        clang_path = build_path("../build/bin/clang") 
    if test_suite_path is None:
        test_suite_path = build_path("../llvm/projects/test-suite")
    if o0_cmake is None:
        o0_cmake = build_path("O0.cmake")

    os.makedirs(folder, exist_ok=True)

    # TODO: handle relative clang paths
    command = ['cmake', '-DCMAKE_C_COMPILER=' + clang_path, '-GNinja', '-D', 'TEST_SUITE_BENCHMARKING_ONLY=ON']
    command.extend(['-C' + o0_cmake, test_suite_path])

    subprocess.call(command, cwd=folder)


def _build_arg_parser():
    """
    Build the arg parser for the test-suite-builder script
    :return: The arg parser
    """

    arg_parser = argparse.ArgumentParser(description="Builds a test-suite with a custom compilation command.", prefix_chars='-',
                                         formatter_class=argparse.ArgumentDefaultsHelpFormatter)

    arg_parser.add_argument('-s', '--script', action="store", default="main.sh",
                            help="Script from /scripts that will build the programs")
    arg_parser.add_argument('-t', '--test_suite', action="store", default=None,
                            help="Path to the test suite source")
    arg_parser.add_argument('-c', '--clang', action="store", default=None,
                            help="Path to the clang binary used to build the test suite")
    arg_parser.add_argument('-n', '--no_rewrite', action="store_true",
                            help="Disable rule rewriting; this is a standard test suite build. You may use -z to"
                                 " specify a different cmake file.")
    arg_parser.add_argument('-z', '-o0path', action="store", default=None,
                            help="Default configuration file; only relevant if -n is used")

    destination_group = arg_parser.add_mutually_exclusive_group(required=True)
    destination_group.add_argument('-d', '--dest', action="store",
                                   help="The folder where the test suite will be built")
    destination_group.add_argument('-f', '--fast_dest', action="store",
                                   help="Same as -d, but relative to /testsuite")

    return arg_parser


def rewrite_ninja(ninja_file, script_name='main.sh'):
    """
    Rewrite the given Ninja file to replace build rules by calls to the
    provided script.

    :param ninja_file:  The ninja file to rewrite
    :param script_name: Name of compilation script in /scripts
    """
    new_ninja_content = []

    class Mode(Enum):
        look_for_compile_rule = 0
        look_for_command_rewriting = 1

    mode = Mode.look_for_compile_rule

    with open(ninja_file) as file:
        for line in file.readlines():
            # Continue = we don't append the line. If we let the instructions go, the line will be append to the
            # new ninja content array
            if mode == Mode.look_for_compile_rule:
                # We rewrite the command of every rules but timeit
                if (line.startswith('rule C_COMPILER__') and not line.startswith('rule C_COMPILER__timeit')) \
                        or line.startswith('rule CXX_COMPILER_'):
                    mode = Mode.look_for_command_rewriting
            elif mode == Mode.look_for_command_rewriting:
                if line.strip().startswith('command = '):
                    new_line = read_and_rewrite_command(line, script_name)

                    if new_line is None:
                        raise Exception("Invalid command line : " + line)

                    new_ninja_content.append(new_line)
                    mode = Mode.look_for_compile_rule
                    continue
            else:
                raise Exception('Unknown mode ' + str(mode))

            new_ninja_content.append(line)

    # Write the new ninja file
    file = open(ninja_file, "w")
    file.write("".join(new_ninja_content))
    file.close()


def read_and_rewrite_command(command, script_name):
    """
    Rewrite a single command.

    :param command:     Original command line
    :param script_name: Name of compilation script
    :return:            Rewritten command if it's a compialtion command, None otherwise
    """

    regex_read = read_command_with_regex(command)
    if regex_read is None:
        return None

    # Return a generic command that calls the provided script
    MD, clang = regex_read['MD'], regex_read['clang']
    root = os.path.dirname(os.path.dirname(os.path.dirname(clang)))

    args = "in out DEFINES INCLUDES FLAGS DEP_FILE".split()
    the_command = f'{root}/scripts/{script_name} --testsuite '
    the_command += ' '.join(map(lambda x: f'"${x}"', args))
    the_command += f' "{MD}" "{clang}"'
    return f'  command = {the_command}\n'


def read_command_with_regex(command):
    """
    Parse a compilation command and return the useful components: path to
    timeit, to clang, and the -MD or -MMD flag for dependency generation.

    :param command: Original command line
    :return:        Dictionary with timeit path, clang path and MD flag if it's
                    compilation command, None otherwise.
    """

    # TODO: use \w for where applicable

    re_cmd1 = 'command = ([-A-Za-z0-9_\\/+]*timeit) --summary \\$out\\.time ([-A-Za-z_0-9\\/\\.+]*) ' \
            '\\$DEFINES \\$INCLUDES \\$FLAGS (\\-M*D)? -MT \\$out -MF \\$DEP_FILE -o \\$out -c \\$in'
    re_cmd1 = re_cmd1.replace(" ", "\\s+")

    result = re.findall(re_cmd1, command)
    if result:
        # Retrieve the MD flag because some Ubuntu distributions use -MD while others use -MMD.
        return {
            'timeit': result[0][0],
            'clang':  result[0][1],
            'MD':     result[0][2],
        }

    re_cmd2 = 'command = ([-A-Za-z_0-9\\/\\.+]*) ' \
            '\\$DEFINES \\$INCLUDES \\$FLAGS (\\-M*D)? -MT \\$out -MF \\$DEP_FILE -o \\$out -c \\$in'
    re_cmd2 = re_cmd2.replace(" ", "\\s+")

    result = re.findall(re_cmd2, command)
    if result:
        return {
            'clang':  result[0][0],
            'MD':     result[0][1],
        }

    return None

if __name__ == '__main__':
    _main()
