from dataclasses import dataclass
from typing import Dict, List

# This file includes data structure used by Pipeline Middleware.


@dataclass
class ValPosition:
    partition_id: int
    offset: int

    def __str__(self) -> str:
        res = f"[partition_id:{self.partition_id},offset:{self.offset}]"
        return res

    def __repr__(self) -> str:
        return self.__str__()


class PartitionInputVal(object):
    def __init__(self, partition_id, offset) -> None:
        # every input from which partition_id and which offset
        val_pos = ValPosition(partition_id, offset)
        self._from_partition_and_offset: ValPosition = val_pos

    def get(self):
        return self._from_partition_and_offset

    def __str__(self) -> str:
        res = ""
        res += f"<-({self._from_partition_and_offset})"
        return res

    def __repr__(self) -> str:
        return self.__str__()


class PartitionOutputVal(object):
    def __init__(self) -> None:
        # every output to which partition_id and which offset
        self._to_partition_and_offset: List[ValPosition] = []

    def add(self, partition_id, offset):
        val_pos = ValPosition(partition_id, offset)
        self._to_partition_and_offset.append(val_pos)

    def get(self):
        return self._to_partition_and_offset

    def __str__(self) -> str:
        res = ""
        res += "->("
        for val_pos in self._to_partition_and_offset:
            res += f"{val_pos},"
        res += ")"
        return res

    def __repr__(self) -> str:
        return self.__str__()


class Partition(object):
    def __init__(self) -> None:
        self._input_vals: List[PartitionInputVal] = []
        self._output_vals: List[PartitionOutputVal] = []

    def add_input_val(self, input_val: PartitionInputVal):
        self._input_vals.append(input_val)

    def add_output_val(self, output_val: PartitionOutputVal):
        self._output_vals.append(output_val)

    def get_input_vals(self):
        return self._input_vals

    def get_output_vals(self):
        return self._output_vals

    # get the output offsets sent to dst_partition_id
    def get_output_offsets(self, dst_partition_id):
        res = []
        for offset, output_val in enumerate(self._output_vals):
            outputs = output_val.get()
            for val_pos in outputs:
                if val_pos.partition_id == dst_partition_id:
                    res.append(offset)

        return res

    # get all input dst partition_ids
    def get_input_partition_ids(self):
        res = []
        for input_val in self._input_vals:
            val_pos = input_val.get()
            if val_pos.partition_id not in res:
                res.append(val_pos.partition_id)
        return res

    # get all output dst partition_ids
    def get_output_partition_ids(self):
        res = []
        for output_val in self._output_vals:
            outputs = output_val.get()
            for val_pos in outputs:
                if val_pos.partition_id not in res:
                    res.append(val_pos.partition_id)
        return res

    def __str__(self) -> str:
        res = ""
        res += f"  input:\n"
        res += f"    length:{len(self._input_vals)}\n"
        for i, input_val in enumerate(self._input_vals):
            res += f"    offset={i}:{input_val}\n"

        res += f"  output:\n"
        res += f"    length:{len(self._output_vals)}\n"
        for i, output_val in enumerate(self._output_vals):
            res += f"    offset={i}:{output_val}\n"

        return res

    def __repr__(self) -> str:
        return self.__str__()


# This class is a middleware between partition splitter
# and Pipeline Scheduler. It records the graph info about
# partition input/output and provides it to scheduler.
# There are three kinds of partition in Pipeline Middleware Design
# which represents the whole process of a model execution: input-fwd-output
# 1. input_partition: records the input of a model.
# 2. mid_partition: record the splitted forwards execution of a model.
# 3. output_partition: records the output of a model.
# attributes:
#   _partitions: include all partitions
#   _input_partition_id: the key represents input_partition
#   _output_partition_id: the key represents output_partition
class Topo(object):
    def __init__(self, input_partition_id=None, output_partition_id=None) -> None:
        self._partitions: Dict[int, Partition] = {}
        self._input_partition_id = input_partition_id
        self._output_partition_id = output_partition_id

    def set_input_partition_id(self, partition_id: int):
        self._input_partition_id = partition_id

    def set_output_partition_id(self, partition_id: int):
        self._output_partition_id = partition_id

    def get_input_partition_id(self):
        return self._input_partition_id

    def get_output_partition_id(self):
        return self._output_partition_id

    def set_partitions(self, partition_id: int, partition: Partition):
        self._partitions[partition_id] = partition

    def get_mid_partitions(self):
        res = {}  # {partition_id: Partition}
        for partition_id, partition in self._partitions.items():
            if self._input_partition_id == partition_id or self._output_partition_id == partition_id:
                continue
            res[partition_id] = partition
        return res

    def get_mid_partition_ids(self):
        return list(self.get_mid_partitions().keys())

    def get_input_partition(self):
        if self._input_partition_id is not None:
            return self._partitions[self._input_partition_id]
        return None

    def get_output_partition(self):
        if self._output_partition_id is not None:
            return self._partitions[self._output_partition_id]
        return None

    def get_partition_by_id(self, partition_id):
        return self._partitions[partition_id]

    def __str__(self) -> str:
        res = ""
        if len(self._partitions) == 0:
            return "Empty Topo Graph."

        input_part = self.get_input_partition()
        if input_part is not None:
            res += "{\n"
            res += f"InputPartition:\n  partition_id={self._input_partition_id}\n{input_part}"
            res += "}\n"

        mid_parts = self.get_mid_partitions()
        for i, (partition_id, part) in enumerate(mid_parts.items()):
            res += "{\n"
            res += f"SubPartition_{i}:\n  partition_id={partition_id}\n  {part}"
            res += "}\n"

        output_part = self.get_output_partition()
        if output_part is not None:
            res += "{\n"
            res += f"OutputPartition:\n  partition_id={self._output_partition_id}\n{output_part}"
            res += "}\n"

        return res

    def __repr__(self) -> str:
        return self.__str__()
