#!/usr/bin/env python3
"""VeriSnip (VS) is a project designed to bring the power of Verilog scripting to the open-source hardware community. This tool simplifies the generation of Verilog modules or snippets by seamlessly integrating with other programs. The generated files can be easily included in any Verilog project."""

import os
import re
import shutil
import subprocess
import sys

from .vs_colours import INFO, OK, WARNING, ERROR, DEBUG, print_coloured


def help_build():
    text = """
VeriSnip (VS) version 0.0.3
To show this help page: 
    Usage: vs_build --help

Create a build directory containing all the compiled hardware:
    Usage: vs_build <main_module> --TestBench <testbench_name> --Boards <board_modules> --quiet --debug
    <main_module> -> This is the name of the main RTL design.
    --TestBench=<testbench_name> (optional) -> by default vs_build looks for a TestBench file with the name <main_module>_tb.
    --Boards=<board_modules> (optional) -> by default vs_build looks for NO board RTL design top module. Multiple boards can be passed in a single argument (example, "Board1 Board2 Board3").
    --quiet (optional) -> suppresses INFO prints.
    --debug (optional) -> enables DEBUG prints.
    <PARAMETER_NAME>=<verilog_value>  (optional) -> user defined parameters to use in the Verilog HDL code generation.

Clean the contents generated by vs_build:
    Usage: vs_build --clean
    "--clean" removes the "build" and "generated" directories.

"""
    print_coloured(INFO, text)


def clean_build(current_directory):
    """
    Cleans the build directory by removing it and its contents.

    Args:
        current_directory (str): The current directory of the build.
    """
    remove_directory(f"{current_directory}/build")
    remove_directory(f"{current_directory}/generated")


def remove_directory(directory_to_remove):
    """
    Removes a directory and its contents.

    Args:
        directory_to_remove (str): The directory to remove.
    """
    try:
        shutil.rmtree(directory_to_remove)
        print_coloured(
            OK, f"Removed directory '{directory_to_remove}' and its contents."
        )
    except OSError as e:
        print_coloured(WARNING, f"Could not remove directory. {e}")


def find_files(current_directory):
    """
    Finds all Verilog snippets, modules, and scripts under the given directory.

    Args:
        current_directory (str): The directory to search.

    Returns:
        tuple: A tuple containing lists of all Verilog snippets, modules, and scripts found in the directory.
    """
    script_files = []
    verilog_files = []
    search_directories = []
    excluded_files = ["LICENSE", ".gitignore", ".gitmodules"]
    verilog_extensions = [".v", ".vh", ".sv", ".svh", ".vs"]
    script_extensions = [".py", ".sh", ".lua", ".scala", ".rb", ".pl", ".tcl"]

    search_directories.append(current_directory)
    search_directories += get_included_directories()
    for directory in search_directories:
        for root, directories, files in os.walk(directory, topdown=True):
            filtered_dirs = []
            for directory in directories:
                if directory not in [".git", "build", "generated", "__pycache__"]:
                    filtered_dirs.append(directory)
            directories[:] = filtered_dirs
            for file in files:
                filename, extension = os.path.splitext(file)
                file_path = os.path.join(root, file)
                if filename not in excluded_files:
                    if extension in script_extensions and file_path not in script_files:
                        script_files.append(file_path)
                    elif (
                        extension in verilog_extensions
                        and file_path not in verilog_files
                    ):
                        verilog_files.append(file_path)

    print_coloured(DEBUG, f"Found verilog files:")
    for file_path in verilog_files:
        print_coloured(DEBUG, f"\t{relative_path(file_path)}")
    print_coloured(DEBUG, f"Found script files:")
    for file_path in script_files:
        print_coloured(DEBUG, f"\t{relative_path(file_path)}")

    return script_files, verilog_files


def get_included_directories():
    """
    Get the directories the user wants to include from the command line arguments.

    Returns:
        list: A list of the directories to include when searching for scripts and Verilog.
    """
    included_directories = []

    if "--inc_dir" in sys.argv or "-I" in sys.argv:
        include_index = 3  # need to get id.
        included_directories.append(sys.argv[include_index])

    return included_directories


def fetch_sources(current_directory, verilog_files, script_files, top_module):
    """
    Fetch Verilog files, generating them if necessary.

    Args:
        verilog_files (list): List of existing Verilog file paths.
        script_files (list): List of script file paths.
        top_module (str): The top module name.

    Returns:
        list: A list of all Verilog files used by the core.
    """
    sources_list = []
    sources_list, verilog_files = find_or_generate(
        current_directory,
        "",
        tuple([top_module]),
        script_files,
        verilog_files,
        sources_list,
    )

    i = 0
    while i < len(sources_list):
        verilog_file = sources_list[i]
        sources_list, verilog_files = analyse_file(
            current_directory, verilog_file, script_files, verilog_files, sources_list
        )
        i = i + 1

    return sources_list


def analyse_file(
    current_directory, file_path, script_files, verilog_files, sources_list
):
    """
    Analyze a Verilog file for module instantiations or includes.

    Args:
        file_path (str): Path to the Verilog file.
        script_files (list): List of script file paths.
        verilog_files (list): List of Verilog file paths.
        sources_list (list): List to store additional source file paths.

    Returns:
        tuple: A tuple containing the updated sources_list and verilog_files.

    This function reads the content of a Verilog file specified by file_path and searches for module instantiations
    or includes. It uses regular expressions to extract relevant information from the file content and then calls the
    find_or_generate function to process the extracted information.

    The moduleInstantiationPattern, includePattern, and the multiCommentIncludePattern are the regular
    expressions used to identify module instantiations and includes in the Verilog file.
    """
    with open(file_path, "r") as file:
        content = file.read()
    filename = os.path.basename(file_path)

    moduleInstantiationPattern = r"\n\s*?(\w+?)\s+?(?:#\([\s\S]*?\))?\s*?(\w+?)\s*?\(\s*?(\.\w+?\s*?\([\s\S]*?)\);"
    includePattern = r'\n\s*?`include\s+?"(.*?)"(?!\s*?/\*)(.*)'
    multiCommentIncludePattern = r'\n\s*?`include\s+?"(.*?)"\s*?/\*([\s\S]*?)\*/'
    for pattern in [
        moduleInstantiationPattern,
        includePattern,
        multiCommentIncludePattern,
    ]:
        matches = re.findall(pattern, content)
        for item in matches:
            sources_list, verilog_files = find_or_generate(
                current_directory,
                filename,
                item,
                script_files,
                verilog_files,
                sources_list,
            )

    return sources_list, verilog_files


def find_or_generate(
    current_directory,
    callee_filename,
    match_strings,
    script_files,
    verilog_files,
    sources_list,
):
    """
    Find or generate a file based on given conditions.

    Args:
        current_directory (str): The current working directory.
        callee_filename (str): Name of the file where the "`include" is present.
        match_strings (list): List of strings extracted from the include directive.
                              Index 0: File name, Index 1: Comment argument.
        script_files (list): List of script file paths.
        verilog_files (list): List of Verilog file paths.
        sources_list (list): List to store the generated or found file paths.

    Returns:
        tuple: (sources_list, verilog_files) - Updated lists containing the 
               generated or found file paths.
    """
    # Extract file information from match strings
    file_name = match_strings[0].split()[0]
    _, extension = os.path.splitext(file_name)
    comment_arg = match_strings[1].strip() if len(match_strings) > 1 else ""

    # Try to locate the file in the verilog_files list
    file_path = _find_verilog_file(file_name, extension, verilog_files)

    # Handle VS_NO_GENERATE directive
    if "VS_NO_GENERATE" in comment_arg:
        print_coloured(
            INFO,
            f"File {file_name} is ignored due to 'VS_NO_GENERATE' comment in {callee_filename}.",
        )
        return sources_list, verilog_files

    # Process the file: generate if not found, add to sources if found
    if file_path is None:
        sources_list, verilog_files = _generate_file_from_script(
            file_name, script_files, comment_arg, callee_filename,
            current_directory, sources_list, verilog_files
        )
    elif file_path not in sources_list:
        sources_list.append(file_path)

    return sources_list, verilog_files


def _find_verilog_file(file_name, extension, verilog_files):
    """
    Find a Verilog file by name, trying multiple extensions if none specified.

    Args:
        file_name (str): The base file name.
        extension (str): The file extension (may be empty).
        verilog_files (list): List of available Verilog file paths.

    Returns:
        str or None: The path to the found file, or None if not found.
    """
    if extension == "":
        # Try .v first, then .sv
        file_path = find_filename_in_list(f"{file_name}.v", verilog_files)
        if file_path is None:
            file_path = find_filename_in_list(f"{file_name}.sv", verilog_files)
        return file_path
    else:
        return find_filename_in_list(file_name, verilog_files)


def _generate_file_from_script(
    file_name, script_files, comment_arg, callee_filename,
    current_directory, sources_list, verilog_files
):
    """
    Generate a file using a matching script.

    Args:
        file_name (str): The name of the file to generate.
        script_files (list): List of available script file paths.
        comment_arg (str): Comment arguments from the include directive.
        callee_filename (str): Name of the file requesting generation.
        current_directory (str): The current working directory.
        sources_list (list): Current list of source files.
        verilog_files (list): Current list of Verilog files.

    Returns:
        tuple: Updated (sources_list, verilog_files).
    """
    script_path, file_suffix = find_most_common_prefix(file_name, script_files)
    
    if script_path != "":
        script_arguments = [
            script_path,
            file_suffix,
            comment_arg,
            callee_filename,
        ] + sys.argv[1:]
        subprocess.run(script_arguments)
        
        return move_to_generated_dir(
            script_path, current_directory, sources_list, verilog_files
        )
    
    return sources_list, verilog_files


def move_to_generated_dir(script_path, current_directory, sources_list, verilog_files):
    """
    Moves Verilog files generated by a script under the current directory to the generated directory.

    Args:
        script_path (str): A string equivalent to the script path executed.
        current_directory (str): A string equivalent to the current directory.
        sources_list (list): List of source file paths.
        verilog_files (list): List of Verilog file paths.

    Returns:
        tuple: A tuple containing the updated sources_list and verilog_files.

    This function iterates through files in the current directory, identifies Verilog files based on their extensions,
    and moves them to the "generated/RTL" directory. It updates the sources_list and verilog_files accordingly.
    """
    verilog_extensions = [".v", ".vh", ".sv", ".svh", ".vs"]
    verilog_files_found = []
    generated_dir = os.path.join(current_directory, "generated")

    for filename in os.listdir(current_directory):
        _, extension = os.path.splitext(filename)
        file_dst_path = os.path.join(generated_dir, filename)
        file_src_path = os.path.join(current_directory, filename)
        if extension in verilog_extensions:
            shutil.move(file_src_path, file_dst_path)
            verilog_files_found.append(file_dst_path)
            if file_dst_path not in verilog_files:
                verilog_files.append(file_dst_path)
            if file_dst_path not in sources_list:
                sources_list.append(file_dst_path)

    if verilog_files_found == []:
        print_coloured(WARNING, f"{script_path} generated no Verilog files.")
    else:
        print_coloured(
            INFO, f"{script_path} generated {', '.join(verilog_files_found)}."
        )

    return sources_list, verilog_files


def find_most_common_prefix(input_name, file_list):
    """
    Searches for the file with the most common prefix in a file list.

    Args:
        input_name (str): A string equivalent to the file name to search for.
        file_list (list): A list of script file names.

    Returns:
        tuple: A tuple containing the file path and the remaining string words.
    """
    input_words = input_name.split("_")
    similar_word_counter = 0
    most_similar_file = ""
    file_suffix = ""
    for file_path in file_list:
        file_name = os.path.splitext(os.path.basename(file_path))[0]
        tmp_counter = 0
        tmp_string = ""
        for word in input_words:
            tmp_string = tmp_string + word
            tmp_counter = tmp_counter + 1
            if file_name == tmp_string:
                if tmp_counter > similar_word_counter:
                    similar_word_counter = tmp_counter
                    most_similar_file = file_path
                    file_suffix = "_".join(input_words[tmp_counter:])
            tmp_string = tmp_string + "_"
    if most_similar_file == "" and file_suffix == "":
        print_coloured(
            WARNING, f'Could not locate any matching files for "{input_name}".'
        )
    elif file_suffix == "":
        print_coloured(
            DEBUG,
            f'The most similar file to "{input_name}" is "{relative_path(most_similar_file)}" and "{input_name}" does not have a suffix at the end of its name.',
        )
    else:
        print_coloured(
            DEBUG,
            f'The most similar file to "{input_name}" is "{relative_path(most_similar_file)}" and the suffix is {file_suffix}.',
        )
    return most_similar_file, file_suffix


def rtl_build(current_directory, module, parameters, verilog_files, script_files):
    """
    Builds Verilog files and creates a build directory for RTL sources.

    Args:
        module (str): The module name for Verilog files.
        verilog_files (list): List of Verilog source file paths.
        script_files (list): List of script file paths.

    Returns:
        list: The list of RTL Verilog source files.
    """
    create_directory(f"{current_directory}/generated")
    rtl_dir = f"{current_directory}/build/RTL"
    rtl_sources = fetch_sources(current_directory, verilog_files, script_files, module)
    verilog_copy(rtl_sources, [], rtl_dir)
    print_coloured(OK, "Built all RTL sources.")
    return rtl_sources


def testbench_build(
    current_directory, TestBench, verilog_files, script_files, rtl_sources
):
    """
    Builds TestBench Verilog files and creates a build directory.

    Args:
        TestBench (str): The TestBench name.
        verilog_files (list): List of Verilog source file paths.
        script_files (list): List of script file paths.
        rtl_sources (list): List of RTL Verilog source files.
    """
    testbench_dir = f"{current_directory}/build/TestBench"
    testbench_sources = fetch_sources(
        current_directory, verilog_files, script_files, TestBench
    )
    verilog_copy(testbench_sources, rtl_sources, testbench_dir)
    copy_testbench_cpp(TestBench, testbench_dir)
    print_coloured(OK, "Built all TestBench sources.")


def copy_testbench_cpp(TestBench, testbench_dir):
    """
    Copies TestBench C++ file to the TestBench build directory.

    Args:
        TestBench (str): The TestBench name.
        testbench_dir (str): The directory for TestBench files.
    """
    for root, directories, files in os.walk(os.path.abspath("."), topdown=True):
        filtered_dirs = []
        for directory in directories:
            if directory not in [".git", "build", "generated", "__pycache__"]:
                filtered_dirs.append(directory)
        directories[:] = filtered_dirs
        if f"{TestBench}.cpp" in files:
            # Form the source and destination paths
            source_path = os.path.join(root, f"{TestBench}.cpp")
            destination_path = os.path.join(testbench_dir, f"{TestBench}.cpp")

            # Copy the file to the testbench_dir
            shutil.copy(source_path, destination_path)
            print_coloured(
                INFO, f"Testbench '{source_path}' copied to '{destination_path}'"
            )
            return

    print_coloured(
        INFO,
        f"File '{TestBench}.cpp' not found in the current directory or its subdirectories.",
    )


def board_build(
    current_directory, Boards, main_module, verilog_files, script_files, rtl_sources
):
    """
    Builds Verilog files for specified boards and creates a build directory.

    Args:
        Boards (list): List of board names.
        main_module (str): The main module name.
        verilog_files (list): List of Verilog source file paths.
        script_files (list): List of script file paths.
        rtl_sources (list): List of RTL Verilog source files.
    """
    for Board in Boards:
        Board_name = Board
        if Board.startswith(main_module):
            Board_name = Board[len(main_module) :].lstrip("_")
        Board_dir = f"{current_directory}/build/RTL/{Board_name}"
        Board_sources = fetch_sources(
            current_directory, verilog_files, script_files, Board
        )
        verilog_copy(Board_sources, rtl_sources, Board_dir)
        print_coloured(OK, f"Built all {Board_name} sources.")


def verilog_copy(new_sources, existing_sources, build_dir):
    """
    Copy Verilog files to build directories and substitute ".vs" on said files.

    Args:
        new_sources (list): List of new Verilog source file paths.
        existing_sources (list): List of existing Verilog source file paths.
        build_dir (str): Path to the build directory.
    """
    sources_list = filter_list(new_sources, existing_sources)
    create_directory(build_dir)
    for verilog_file in sources_list:
        if not verilog_file.endswith(".vs"):
            verilog_content = ""
            verilog_content = substitute_vs_file(verilog_file, sources_list)
            file_name = os.path.basename(verilog_file)
            with open(f"{build_dir}/{file_name}", "w") as file:
                file.write(verilog_content)


def filter_list(target_list, source_list):
    """
    Filter common files between the target list and the source list.

    Args:
        target_list (list): List to be filtered.
        source_list (list): Secondary source files list.

    Returns:
        list: `target_list` filtered, remains with all ".vs" files and files not present in `source_list`.
    """
    # Create an empty list to store elements from target_list that are not in source_list
    filtered_list = []

    # Iterate through target_list and only add elements not present in source_list to filtered_list
    for element in target_list:
        if element not in source_list:
            filtered_list.append(element)

    return filtered_list


def substitute_vs_file(source_file, sources_list):
    """
    Recursively substitutes included .vs files in the source file content.

    Args:
        source_file (str): The source file containing potential `include directives.
        sources_list (list): List of source file paths.

    Returns:
        str: The new content with included .vs files substituted.
    """
    new_content = ""
    on_comment = False

    with open(source_file, "r") as file:
        for line in file:
            if not on_comment:
                filename_match = re.findall(r'^\s*?`include\s+?"(.+?)\.vs"', line)
                if filename_match:
                    vs_file = filename_match[0] + ".vs"
                    vs_file_path = find_filename_in_list(vs_file, sources_list)

                    if vs_file_path:
                        new_content += substitute_vs_file(vs_file_path, sources_list)
                    else:
                        warning_text = f"File {vs_file} does not exist to substitute."
                        print_coloured(WARNING, warning_text)
                        new_content += f"  // {warning_text}\n"
                    if "/*" in line:
                        on_comment = True
                else:
                    new_content += line
            else:
                if "*/" in line:
                    on_comment = False

    return new_content


def find_filename_in_list(filename, files_list):
    """
    Finds the filename in the list of files.

    Args:
        filename (str): The filename to find.
        files_list (list): The list of files to search.

    Returns:
        str or None: The first matching file, or None if the file is not found.
    """
    found_files = None
    for file in files_list:
        if os.path.basename(file) == filename:
            if found_files != None:
                print_coloured(
                    WARNING,
                    f"Found more than one directory with file {filename}.\n  {file}",
                )
            found_files = file

    if found_files:
        return found_files
    return None


def relative_path(path):
    """
    Convert an absolute path to a relative path based on the current working directory.

    Args:
        path (str): The absolute path to be converted.

    Returns:
        str: The relative path derived from the given absolute path.
    """
    path = os.path.relpath(path, os.getcwd())
    return path


def create_directory(path):
    """
    Creates a directory at the specified path.

    Args:
        path (str): The path of the directory to be created.
    """
    try:
        os.makedirs(path)
        print_coloured(INFO, f"Created directory '{path}'.")
    except OSError as e:
        print_coloured(DEBUG, f"Did not create directory: {e}")


def parse_arguments():
    """
    Parses arguments with which vs_build is called.

    Returns:
        tuple: A tuple containing the module_name (string), testbench_name (string), board_modules (list) and parameters (dict).

    This function parses command-line arguments provided when calling vs_build. It extracts information such as the
    module name, testbench name, supported board modules, and any parameters passed on the command line.
    """
    module_name = None
    testbench_name = None
    board_modules = []
    parameters = {}

    for i in range(1, len(sys.argv)):
        parameter = re.match(r'^(\w+)="?([^"]+)"?$', sys.argv[i])
        testbench = re.match(r'^--TestBench="?([^"]+)"?$', sys.argv[i])
        boards = re.match(r'^--Boards="?(.+?)"?$', sys.argv[i])
        if testbench:
            testbench_name = testbench.group(1)
            testbench_name_pattern = r"^\s+$"
            if re.match(testbench_name_pattern, testbench_name):
                if testbench_name.startswith("_"):
                    testbench_name = f"{module_name}{testbench_name}"
            else:
                print_coloured(ERROR, "Invalid argument after --TestBench=")
                help_build()
                exit(1)
        elif boards:
            Boards = boards.group(1).split()
            Board_pattern = r"^[\w]+$"
            for Board in Boards:
                if re.match(Board_pattern, Board):
                    if Board.startswith("_"):
                        board_modules.append(f"{module_name}{Board}")
                    else:
                        board_modules.append(Board)
                else:
                    print_coloured(ERROR, f"Invalid Board name {Board}")
                    help_build()
                    exit(1)
        elif parameter:
            name = parameter.group(1)
            value = parameter.group(2)
            
            # Validate if it's a valid Verilog number format or integer
            verilog_pattern = r"^\d+('[bBdDhH][0-9a-fA-F_]+)$"
            integer_pattern = r"^\d+$"
            
            if re.match(verilog_pattern, value) or re.match(integer_pattern, value):
                parameters[name] = value
                print_coloured(DEBUG, f"Parsed parameter {name} = {value}")
            else:
                print_coloured(WARNING, f"Invalid parameter value format: {sys.argv[i]}")
            continue
        elif not sys.argv[i].startswith("--"):
            module_name = sys.argv[i]
            testbench_name = f"{sys.argv[i]}_tb"
          
    return module_name, testbench_name, board_modules, parameters


def main():
    """
    Main function to handle the vs_build script execution.
    It processes command-line arguments, cleans the build directory if requested,
    and builds the RTL, TestBench, and board modules as specified.
    """
    current_directory = os.getcwd()
    if len(sys.argv) < 2 or sys.argv[1] == "--help":
        help_build()        
    else:
        if "--clean" in sys.argv:
            clean_build(current_directory)
        main_module, testbench, board_modules, parameters = parse_arguments()
        if main_module != None:
            script_files, verilog_files = find_files(current_directory)
            rtl_sources = rtl_build(
                current_directory, main_module, parameters, verilog_files, script_files
            )
            testbench_build(
                current_directory, testbench, verilog_files, script_files, rtl_sources
            )
            board_build(
                current_directory,
                board_modules,
                main_module,
                verilog_files,
                script_files,
                rtl_sources,
            )
            print_coloured(OK, f"Created {main_module} project build directory.")
        else:
            print_coloured(ERROR, f"Undefined main module!")
            exit(1)


# Check if this script is called directly
if __name__ == "__main__":
    main()
