from jinja2 import Environment, FileSystemLoader
import yaml
import os
import sys
import stringcase
import shutil
import uncrustify
import clang_format
from pathlib import Path


def get_generation_dict(
    real_imports_folder_path: str,
    mock_imports_folder_path: str,
    feature_by_name: dict,
    application_name: str,
) -> dict:
    generation_dict = {}
    generation_dict["controllers"] = {}
    for feature_name, feature_data in feature_by_name.items():
        feature_snake_name = stringcase.snakecase(feature_name)
        feature_pascal_name = stringcase.pascalcase(feature_name)
        feature_camel_name = stringcase.camelcase(feature_name)
        generation_dict["controllers"][feature_pascal_name] = {
            "mock_presenter_file": os.path.join(
                mock_imports_folder_path,
                "Presenter",
                f"{feature_pascal_name}Controller.qml",
            ),
            "mock_template_path": "QML/mock_imports/presenter/",
            "mock_template": "controller.qml.jinja2",
            "real_presenter_file": os.path.join(
                real_imports_folder_path,
                "presenter",
                f"foreign_{feature_snake_name}_controller.h",
            ),
            "real_template_path": "QML/real_imports/presenter/",
            "real_template": "foreign_controller.h.jinja2",
            "feature_pascal_name": feature_pascal_name,
            "feature_camel_name": feature_camel_name,
            "feature_snake_name": feature_snake_name,
            "CRUD": feature_data.get("CRUD", {}),
        }

    # add mock_custom_functions to the generation_dict["controllers"][feature_pascal_name] dict
    for feature_name, feature_data in feature_by_name.items():
        feature_pascal_name = stringcase.pascalcase(feature_name)
        generation_dict["controllers"][feature_pascal_name][
            "mock_custom_functions"
        ] = []
        commands = feature_data.get("commands", [])
        if commands:
            for command in commands:
                generation_dict["controllers"][feature_pascal_name][
                    "mock_custom_functions"
                ] += [stringcase.camelcase(command["name"])]

        queries = feature_data.get("queries", [])
        if queries:
            for query in queries:
                generation_dict["controllers"][feature_pascal_name][
                    "mock_custom_functions"
                ] += [stringcase.camelcase(query["name"])]

    # add qmldir:
    qmldir_file = os.path.join(mock_imports_folder_path, "Presenter", "qmldir")

    generation_dict["qmldir_file"] = qmldir_file

    # add CMakelists.txt:
    common_cmake_file = os.path.join(
        real_imports_folder_path, "presenter", "CMakeLists.txt"
    )
    generation_dict["common_cmake_file"] = common_cmake_file

    # add "mock_presenter_file" and "real_presenter_file" to the generation_dict["real_presenter_files"] list
    generation_dict["real_presenter_files"] = []
    generation_dict["mock_presenter_files"] = []
    for _, controller in generation_dict["controllers"].items():
        generation_dict["real_presenter_files"].append(
            controller["real_presenter_file"]
        )
        generation_dict["mock_presenter_files"].append(
            controller["mock_presenter_file"]
        )

    # add application_name
    generation_dict["application_name"] = application_name

    return generation_dict


def generate_mock_controller_file(
    root_path: str,
    controller: dict,
    generation_dict: dict,
    files_to_be_generated: dict[str, bool],
):
    # generate the mock controller file if in the files_to_be_generated dict the value is True
    if not files_to_be_generated.get(controller["mock_presenter_file"], False):
        return

    # Create the jinja2 environment
    template_path = os.path.join("templates", controller["mock_template_path"])
    env = Environment(loader=FileSystemLoader(template_path))
    # Load the template
    template = env.get_template(controller["mock_template"])

    crud = controller["CRUD"]
    crud_enabled = crud.get("enabled", False)
    get_enabled = crud.get("get", {}).get("enabled", False)
    get_all_enabled = crud.get("get_all", {}).get("enabled", False)
    create_enabled = crud.get("create", {}).get("enabled", False)
    update_enabled = crud.get("update", {}).get("enabled", False)
    remove_enabled = crud.get("remove", {}).get("enabled", False)

    # Render the template
    output = template.render(
        controller=controller,
        crud=crud,
        crud_enabled=crud_enabled,
        get_enabled=get_enabled,
        get_all_enabled=get_all_enabled,
        create_enabled=create_enabled,
        update_enabled=update_enabled,
        remove_enabled=remove_enabled,
    )

    output_file = os.path.join(root_path, controller["mock_presenter_file"])

    # Create the directory if it does not exist
    os.makedirs(os.path.dirname(output_file), exist_ok=True)

    # Write the output to the file
    with open(output_file, "w") as fh:
        fh.write(output)


def generate_mock_qmldir_file(
    root_path: str, generation_dict: dict, files_to_be_generated: dict[str, bool]
):
    # generate the mock qmldir file if in the files_to_be_generated dict the value is True
    if not files_to_be_generated.get(generation_dict["qmldir_file"], False):
        return

    # Create the jinja2 environment
    env = Environment(loader=FileSystemLoader("templates/QML/mock_imports/presenter/"))
    # Load the template
    template = env.get_template("qmldir_template.jinja2")

    singleton_list = []
    for _, controller in generation_dict["controllers"].items():
        name = controller["feature_pascal_name"]
        singleton_list.append(f"singleton {name}Controller 1.0 {name}Controller.qml")

    # Render the template
    output = template.render(singleton_list=singleton_list)

    output_file = os.path.join(root_path, generation_dict["qmldir_file"])

    # Create the directory if it does not exist
    os.makedirs(os.path.dirname(output_file), exist_ok=True)

    # Write the output to the file
    with open(output_file, "w") as fh:
        fh.write(output)


def generate_real_controller_file(
    root_path: str,
    controller: dict,
    generation_dict: dict,
    files_to_be_generated: dict[str, bool],
    uncrustify_config_file: str,
):
    # generate the real controller file if in the files_to_be_generated dict the value is True
    real_presenter_file = controller["real_presenter_file"]

    if not files_to_be_generated.get(real_presenter_file, False):
        return

    # Create the jinja2 environment
    template_path = os.path.join("templates", controller["real_template_path"])
    env = Environment(loader=FileSystemLoader(template_path))
    # Load the template
    template = env.get_template(controller["real_template"])

    # Render the template
    output = template.render(controller=controller)

    output_file = os.path.join(root_path, real_presenter_file)

    # Create the directory if it does not exist
    os.makedirs(os.path.dirname(output_file), exist_ok=True)

    # Write the output to the file
    with open(output_file, "w") as fh:
        fh.write(output)

    # if uncrustify_config_file:
    #     uncrustify.run_uncrustify(real_presenter_file, uncrustify_config_file)
    clang_format.run_clang_format(output_file)


def generate_real_cmakelists_file(
    root_path: str, generation_dict: dict, files_to_be_generated: dict[str, bool]
):
    common_cmake_file = generation_dict["common_cmake_file"]

    # generate the real cmakelists file if in the files_to_be_generated dict the value is True
    if not files_to_be_generated.get(common_cmake_file, False):
        return

    output_file = os.path.join(root_path, common_cmake_file)

    # Create the jinja2 environment
    env = Environment(loader=FileSystemLoader("templates/QML/real_imports/presenter/"))
    # Load the template
    template = env.get_template("cmakelists_template.jinja2")

    files = generation_dict["real_presenter_files"]
    relative_files = []
    for file in files:
        relative_files.append(
            os.path.relpath(
                os.path.join(root_path, file), os.path.dirname(output_file)
            ).replace("\\", "/")
        )

    # Render the template
    output = template.render(
        files=relative_files,
        application_name=generation_dict["application_name"],
    )

    # Create the directory if it does not exist
    os.makedirs(os.path.dirname(output_file), exist_ok=True)

    # Write the output to the file
    with open(output_file, "w") as fh:
        fh.write(output)


def generate_qml_files(
    root_path: str,
    manifest_file: str,
    files_to_be_generated: dict[str, bool] = {},
    uncrustify_config_file: str = None,
):
    with open(manifest_file, "r") as stream:
        try:
            manifest_data = yaml.safe_load(stream)
        except yaml.YAMLError as exc:
            print(exc)
            return

    application_name = manifest_data.get("global", {}).get(
        "application_name", "example"
    )
    application_name = stringcase.spinalcase(application_name)

    application_data = manifest_data.get("application", [])
    feature_list = application_data.get("features", [])

    # Organize feature_list by name for easier lookup
    feature_by_name = {feature["name"]: feature for feature in feature_list}

    qml_data = manifest_data.get("qml", [])

    generation_dict = get_generation_dict(
        qml_data["real_imports_folder_path"],
        qml_data["mock_imports_folder_path"],
        feature_by_name,
        application_name,
    )

    # generate mock files
    for _, controller in generation_dict["controllers"].items():
        generate_mock_controller_file(
            root_path, controller, generation_dict, files_to_be_generated
        )

    # generate mock qmldir file
    generate_mock_qmldir_file(root_path, generation_dict, files_to_be_generated)

    # generate real files
    for _, controller in generation_dict["controllers"].items():
        generate_real_controller_file(
            root_path,
            controller,
            generation_dict,
            files_to_be_generated,
            uncrustify_config_file,
        )

    # generate real CMakeLists.txt file
    generate_real_cmakelists_file(root_path, generation_dict, files_to_be_generated)


def get_files_to_be_generated(
    manifest_file: str, files_to_be_generated: dict[str, bool] = {}
) -> list[str]:
    """
    Get the list of files that need to be generated based on the manifest file
    """
    # Read the manifest file
    with open(manifest_file, "r") as stream:
        try:
            manifest_data = yaml.safe_load(stream)
        except yaml.YAMLError as exc:
            print(exc)
            return

    application_name = manifest_data.get("global", {}).get(
        "application_name", "example"
    )
    application_name = stringcase.spinalcase(application_name)

    application_data = manifest_data.get("application", [])
    feature_list = application_data.get("features", [])

    # Organize feature_list by name for easier lookup
    feature_by_name = {feature["name"]: feature for feature in feature_list}

    qml_data = manifest_data.get("qml", [])

    files = []
    generation_dict = get_generation_dict(
        qml_data["real_imports_folder_path"],
        qml_data["mock_imports_folder_path"],
        feature_by_name,
        application_name,
    )
    files += generation_dict["real_presenter_files"]
    files += generation_dict["mock_presenter_files"]

    # for _, controller in generation_dict["controllers"].items():
    #     files += feature["real_presenter_files"]

    # # add CMakelists.txt:
    common_cmake_file = generation_dict["common_cmake_file"]
    files.append(common_cmake_file)

    # # add qmldir:
    qmldir_file = generation_dict["qmldir_file"]
    files.append(qmldir_file)

    # strip from files if the value in files_to_be_generated is False
    if files_to_be_generated:
        for path, generate in files_to_be_generated.items():
            if not generate:
                files.remove(path)

    return files


# generate the files into the preview folder
def preview_qml_files(
    root_path: str,
    manifest_file: str,
    files_to_be_generated: dict[str, bool] = {},
    uncrustify_config_file: str = None,
):
    manifest_preview_file = "temp/manifest_preview.yaml"

    # make a copy of the manifest file into temp/manifest_preview.yaml
    shutil.copy(manifest_file, manifest_preview_file)

    # modify the manifest file to generate the files into the preview folder
    with open(manifest_preview_file, "r") as fh:
        manifest = yaml.safe_load(fh)

    # remove .. from the path
    manifest["qml"]["mock_imports_folder_path"] = manifest["qml"][
        "mock_imports_folder_path"
    ].replace("..", "")
    manifest["qml"]["real_imports_folder_path"] = manifest["qml"][
        "real_imports_folder_path"
    ].replace("..", "")

    # write the modified manifest file
    with open(manifest_preview_file, "w") as fh:
        yaml.dump(manifest, fh)

    root_path = os.path.join(root_path, "qleany_preview")

    # remove .. from the path
    if files_to_be_generated:
        preview_files_to_be_generated = {}
        for path, value in files_to_be_generated.items():
            preview_files_to_be_generated[path.replace("..", "")] = value

        generate_qml_files(
            root_path,
            manifest_preview_file,
            preview_files_to_be_generated,
            uncrustify_config_file,
        )

    else:
        generate_qml_files(root_path, manifest_preview_file, {}, uncrustify_config_file)


# Main execution
if __name__ == "__main__":
    full_path = Path(__file__).resolve().parent

    # Add the current directory to the path so that we can import the generated files
    sys.path.append(full_path)

    # Set the current directory to the generator directory
    os.chdir(full_path)

    if len(sys.argv) > 1:
        manifest_arg = sys.argv[1]
        if manifest_arg.endswith(".yaml") or manifest_arg.endswith(".yml"):
            manifest_file = manifest_arg
            root_path = Path(manifest_file).parent

            if len(sys.argv) > 2 and sys.argv[2] == "--preview":
                preview_qml_files(root_path, manifest_file)
            else:
                generate_qml_files(root_path, manifest_file)
        else:
            print("Error: Manifest file must be named 'qleany.yaml' or 'qleany.yml'")
    else:
        print("Error: Please provide the manifest file as an argument")
