"""User design top wrapper generation module.

This module provides functionality to generate top-level wrappers for user designs that
interface with the FPGA fabric. The wrapper handles signal mapping between user logic
and fabric I/O ports.
"""

from pathlib import Path

from loguru import logger

from FABulous.custom_exception import InvalidFileType
from FABulous.fabric_definition.Fabric import Fabric
from FABulous.fabric_generator.parser.parse_hdl import parseBelFile


def generateUserDesignTopWrapper(
    fabric: Fabric, user_design_path: Path, output: Path
) -> None:
    """Generate a top wrapper for the user design.

    This function creates a Verilog top-level wrapper that instantiates the
    user's design and connects its ports to the fabric's external I/O BELs.
    It automatically discovers external ports from the fabric definition and
    generates the necessary wiring and BEL instantiations.

    Params
    ------
    fabric: Fabric
        Fabric object
    user_design_path: Path
        Path to the user design file
    output: Path
        Path to output the user design top wrapper

    Raises
    ------
    InvalidFileType
        Output file is not a Verilog file or user design path is not a file
    FileNotFoundError
        User design file is not a file or does not exist
    """
    top_wrapper: list[str] = [""]

    if output.suffix != ".v":
        raise InvalidFileType(f"{output} is not a Verilog file")
    if not user_design_path.is_file():
        raise FileNotFoundError(f"{user_design_path} is not a file or does not exist")

    if output.exists():
        logger.warning(f"{output} already exists, overwriting")
        output.unlink()
    else:
        logger.info(f"Creating {output}")
    output.touch()

    user_design = parseBelFile(user_design_path, "")

    top_wrapper.append("// Generated by FABulous")
    top_wrapper.append(f"// Top wrapper for user design {user_design_path}\n")

    top_wrapper.append("module top_wrapper;\n")

    top_wrapper.append("wire clk;")
    top_wrapper.append("(* keep *) Global_Clock clk_i (.CLK(clk));\n")

    bel_count: dict[str, int] = {}  # count how often a bel is instantiated
    bel_inputs: dict[str, list[str]] = {}
    bel_outputs: dict[str, list[str]] = {}

    # generate component instantioations
    for x in range(fabric.numberOfColumns):
        # we walk backwards through the Y list, since there is something mixed up with
        # the coordinate system
        for y in range(fabric.numberOfRows - 1, -1, -1):
            bels = fabric.getBelsByTileXY(x, y)
            if not bels:
                continue
            for i, bel in enumerate(
                reversed(bels)
            ):  # we walk backwards trough the bel list
                belstr = ""
                # we only add bels with external ports to the top wrapper.
                if not bel.externalInput and not bel.externalOutput:
                    logger.info(
                        f"Skipping bel {bel.name} in tile X{x}Y{y} since it has no "
                        f"external ports"
                    )
                    continue
                if len(bel.inputs + bel.outputs) == 0:
                    logger.info(
                        f"{bel.name} in tile X{x}Y{y} has no internal ports, "
                        "only external ports, we just add a dummy to the user design "
                        "top wrapper!"
                    )
                    belstr += "//"

                if bel.name not in bel_count:
                    bel_count[bel.name] = 0
                    bel_inputs[bel.name] = [
                        port.removeprefix(bel.prefix) for port in bel.inputs
                    ]
                    bel_outputs[bel.name] = [
                        port.removeprefix(bel.prefix) for port in bel.outputs
                    ]
                else:
                    # count number of times a BEL type is used
                    bel_count[bel.name] += 1

                # This is done similar in the npnr model gen, to get the bel prefix
                # So we assume to get the same Bel prefix here.
                # convert number of bel i to character A,B,C ...
                # But we need to do this backwards, starting with the highest letter for
                # a tile
                prefix = chr(ord("A") + len(bels) - 1 - i)

                if bel.name in [
                    "InPass4_frame_config",
                    "OutPass4_frame_config",
                    "InPass4_frame_config_mux",
                    "OutPass4_frame_config_mux",
                ]:
                    # This is a special case for the RAM_IO bels, since
                    # for some unknown reasons, the prefix used in the nexpnr backend
                    # is not based on the number of bels, it is based on the actual bel
                    # prefix which is defined in the tile csv.
                    # https://github.com/YosysHQ/nextpnr/blob/master/generic/viaduct/fabulous/fabulous.cc#L355
                    prefix = bel.prefix.removesuffix("_")

                belstr += (
                    f'(* keep, BEL="X{x}Y{y}.{prefix}" *) {bel.name} '
                    f"bel_X{x}Y{y}_{prefix} ("
                )

                first = True
                for port in bel.inputs + bel.outputs:
                    port_name = port.removeprefix(bel.prefix)
                    if first:
                        first = False
                    else:
                        belstr += ", "
                    belstr += (
                        f".{port_name}({bel.name}_{port_name}[{bel_count[bel.name]}])"
                    )

                belstr += ");"
                top_wrapper.append(belstr)

    top_wrapper.append("\n")

    for belname in bel_count:
        count = bel_count[belname]
        if bel_inputs[belname]:
            top_wrapper.append(f"// bel {belname} input wires:")
            for port in bel_inputs[belname]:
                top_wrapper.append(f"wire [{count}:0]{belname}_{port};")
        if bel_outputs[belname]:
            top_wrapper.append(f"// bel {belname} output wires:")
            for port in bel_outputs[belname]:
                top_wrapper.append(f"wire [{count}:0]{belname}_{port};")

    top_wrapper.append("\n")

    top_wrapper.append("// instantiate user_design")

    if user_design.language == "vhdl":
        user_design_inst = f"{user_design.name} user_design_i ("
    else:
        user_design_inst = f"{user_design.module_name} user_design_i ("

    if (
        user_design.name == "sequential_16bit_en"
        and "IO_1_bidirectional_frame_config_pass" in bel_count
    ):
        # hardcoded for now
        logger.info(
            "Using default design, "
            "with sequential_16bit_en counter and IO_1_bidirectional_frame_config_pass"
        )

        user_design_inst += ".clk(clk), "
        user_design_inst += ".io_in(IO_1_bidirectional_frame_config_pass_O), "
        user_design_inst += ".io_out(IO_1_bidirectional_frame_config_pass_I), "
        user_design_inst += ".io_oeb(IO_1_bidirectional_frame_config_pass_T));"
    else:
        # if its not our default design, we are just instantiate it,
        # and the user needs to take connect the ports
        logger.warning(
            f"Custom design detected, "
            f"please connect the ports manually in user design top wrapper "
            f"{user_design_path}!"
        )

        if user_design.language == "vhdl":
            logger.warning(
                f"VHDL design detected, "
                f"please check the generated top wrapper {top_wrapper} and check the "
                "user design module name, as well as the ports!"
            )
            first = True
            for port in user_design.inputs + user_design.outputs:
                if first:
                    first = False
                else:
                    user_design_inst += ", "

                if port in ["clk", "CLK"]:
                    user_design_inst += f".{port}(clk) "
                else:
                    user_design_inst += f".{port}() "
        else:  # verilog
            first = True
            for port in user_design.ports_vectors["internal"]:
                if first:
                    first = False
                else:
                    user_design_inst += ", "

                if port in ["clk", "CLK"]:
                    user_design_inst += f".{port}(clk)"
                else:
                    user_design_inst += f".{port}()"
        user_design_inst += ");"

    top_wrapper.append(user_design_inst)
    top_wrapper.append("\n")

    top_wrapper.append("endmodule //top_wrapper\n")

    # write file
    _ = output.write_text("\n".join(top_wrapper))

    logger.info(
        f"Generated user design top wrapper {output} with {len(top_wrapper)} lines"
    )
