import m5
import os
import configparser

from m5.objects import DRAMsim3, AddrRange, Port, MemCtrl

from ..utils.override import overrides
from ..boards.abstract_board import AbstractBoard
from .abstract_memory_system import AbstractMemorySystem


from typing import Optional, Tuple, Sequence, List


def config_ds3(mem_type: str, num_chnls: int) -> Tuple[str, str]:
    """
    This function creates a config file that will be used to create a memory
    controller of type DRAMSim3. It stores the config file in /tmp/ directory.

    :param mem_type: The name for the type of the memory to be configured.
    :param num_chnls: The number of channels to configure for the memory
    :returns: A tuple containing the output file and the output directory.
    """
    config = configparser.ConfigParser()

    # TODO: We need a better solution to this. This hard-coding is not
    # an acceptable solution.
    dramsim_3_dir = os.path.join(
        os.path.dirname(os.path.abspath(__file__)),
        os.pardir,
        os.pardir,
        os.pardir,
        "ext",
        "DRAMsim3",
    )

    dramsim_3_mem_configs = os.path.join(dramsim_3_dir, "configs")

    input_file = os.path.join(dramsim_3_mem_configs, mem_type + ".ini")

    # Run checks to ensure the `ext/DRAMsim3` directory is present, contains
    # the configs directory, and the configuration file we require.
    if not os.path.isdir(dramsim_3_dir):
        raise Exception(
            "The `ext/DRAMsim3` directory cannot be found.\n"
            "Please navigate to `ext` and run:\n"
            "git clone git@github.com:umd-memsys/DRAMsim3.git"
        )
    elif os.path.isdir(dramsim_3_mem_configs):
        raise Exception(
            "The `ext/DRAMsim3/configs` directory cannot be found."
        )
    elif os.path.isfile(input_file):
        raise Exception(
            "The configuration file '" + input_file + "' cannot " " be found."
        )

    output_file = "/tmp/" + mem_type + "_chnls" + str(num_chnls) + ".ini"
    new_config = open(output_file, "w")
    config.read(input_file)
    config.set("system", "channels", str(num_chnls))
    config.write(new_config)
    new_config.close()
    return output_file, m5.options.outdir


class DRAMSim3MemCtrl(DRAMsim3):
    """
    A DRAMSim3 Memory Controller.

    The class serves as a SimObject object wrapper, utiliszing the DRAMSim3
    configuratons.
    """

    def __init__(self, mem_name: str, num_chnls: int) -> None:
        """
        :param mem_name: The name of the type  of memory to be configured.
        :param num_chnls: The number of channels.
        """
        super(DRAMSim3MemCtrl, self).__init__()
        ini_path, outdir = config_ds3(mem_name, num_chnls)
        self.configFile = ini_path
        self.filePath = outdir


class SingleChannel(AbstractMemorySystem):
    """
    A Single Channel Memory system.
    """

    def __init__(self, mem_type: str, size: Optional[str]):
        """
        :param mem_name: The name of the type  of memory to be configured.
        :param num_chnls: The number of channels.
        """
        super(SingleChannel, self).__init__()
        self.mem_ctrl = DRAMSim3MemCtrl(mem_type, 1)
        if size:
            self.mem_ctrl.range = AddrRange(size)
        else:
            raise NotImplementedError(
                "DRAMSim3 memory controller requires a size parameter."
            )

    @overrides(AbstractMemorySystem)
    def incorporate_memory(self, board: AbstractBoard) -> None:
        pass

    @overrides(AbstractMemorySystem)
    def get_mem_ports(self) -> Tuple[Sequence[AddrRange], Port]:
        return [(self.mem_ctrl.range, self.mem_ctrl.port)]

    @overrides(AbstractMemorySystem)
    def get_memory_controllers(self) -> List[MemCtrl]:
        return [self.mem_ctrl]

    @overrides(AbstractMemorySystem)
    def get_memory_ranges(self):
        return [self.mem_ctrl.range]


def SingleChannelDDR3_1600(
    size: Optional[str] = "2048MB",
) -> SingleChannel:
    """
    A single channel DDR3_1600.

    :param size: The size of the memory system. Default value of 2048MB.
    """
    return SingleChannel("DDR3_8Gb_x8_1600", size)


def SingleChannelDDR4_2400(size: Optional[str] = "1024MB") -> SingleChannel:
    """
    A single channel DDR3_2400.

    :param size: The size of the memory system. Default value of 1024MB.
    """
    return SingleChannel("DDR4_4Gb_x8_2400", size)


def SingleChannelLPDDR3_1600(size: Optional[str] = "256MB") -> SingleChannel:
    """
    A single channel LPDDR3_1600.

    :param size: The size of the memory system. Default value of 256MB.
    """
    return SingleChannel("LPDDR3_8Gb_x32_1600", size)


def SingleChannelHBM(size: Optional[str] = "64MB") -> SingleChannel:
    """
    A single channel HBM.

    :param size: The size of the memory system. Default value of 64MB.
    """
    return SingleChannel("HBM1_4Gb_x128", size)
