"""
The :mod:`vlcsim.controller` module is in charge of managing the connections and the physical sustrate.
"""

from __future__ import annotations
from typing import Optional, Union, Callable, Any
from .scene import *
from enum import Enum
import math
import sys


class Connection:
    """
    The :class:`vlcsim.controller.Connection` contains the connection information. Each :class:`vlcsim.controller.Connection` object contains an ID connection, the Access Point (AP) that uses the :class:`vlcsim.controller.Connection` object, the receiver connected to the AP, and finally if the connection was successfully allocated.
    """

    def __init__(self, id: int, receiver: "Receiver", time: float) -> None:
        """
        Connection constructor.

        :param id: Connection ID (non-negative integer).
        :param receiver: Associated receiver (Receiver instance).
        :param time: Creation time (float >= 0).
        :raises ValueError: If any argument is invalid.
        """
        if id < 0:
            raise ValueError("id must be a non-negative integer.")
        if time < 0:
            raise ValueError("time must be a non-negative float.")
        self.__id: int = id
        self.__ap: Optional[AccessPoint] = None
        self.__receiver: Receiver = receiver
        self.__allocated: bool = True
        self.__frameSlice: list[list[int]] = []
        self.__timesAssigned: list[float] = []
        self.__goalTime: Optional[float] = None
        self.__time: float = float(time)
        self.__capacityRequired: Optional[float] = None
        self.__snr: float = sys.float_info.min

    @property
    def snr(self) -> float:
        """Signal to Noise Ratio

        :return: Signal to Noise Ratio
        :rtype: float
        """
        return self.__snr

    @snr.setter
    def snr(self, value: float):
        self.__snr = float(value)

    @property
    def capacityRequired(self) -> Optional[float]:
        """
        Capacity required in bps (can be None).

        :return: Capacity required in bps or None.
        :rtype: Optional[float]
        """
        return self.__capacityRequired

    @capacityRequired.setter
    def capacityRequired(self, value: Optional[float]):
        if value is not None and value < 0:
            raise ValueError("capacityRequired must be a non-negative float or None.")
        self.__capacityRequired = float(value) if value is not None else None

    @property
    def time(self) -> float:
        """The time when this connection arrived to simulation

        :return: time when this connection arrived to simulation
        :rtype: float
        """
        return self.__time

    @time.setter
    def _time(self, value: float):
        if value < 0:
            raise ValueError("time must be a non-negative float.")
        self.__time = float(value)

    @property
    def goalTime(self) -> Optional[float]:
        """
        Total effective time that the connection will use the channel.
        Depends on the required and available capacity.

        :return: Effective time or None.
        :rtype: Optional[float]
        """
        return self.__goalTime

    @goalTime.setter
    def goalTime(self, value: Optional[float]):
        if value is not None and value < 0:
            raise ValueError("goalTime must be a non-negative float or None.")
        self.__goalTime = float(value) if value is not None else None

    @property
    def frameSlice(self) -> list[list[int]]:
        """
        List of lists containing the tuples [frame, slice] that this connection will use

        :return: Tuples frame,slice that the connection will use
        :rtype: list[list[int]]
        """
        return self.__frameSlice

    @frameSlice.setter
    def frameSlice(self, value: list[list[int]]):
        self.__frameSlice = value

    def getNextTime(self) -> float:
        """
        Gets the next time when this connection will start to transmit again.

        :return: the next time
        :rtype: float
        :raises IndexError: If there are no assigned times left.
        """
        if not self.__timesAssigned:
            raise IndexError("No assigned times left for this connection.")
        return self.__timesAssigned.pop(0)

    def insertTime(self, time: float) -> None:
        """function in charge of defininfg the times when this connection will be used. It works with the simulation FEL

        :param time: List of times that this connection will start the transmission
        :type time: float
        """

        for i in range(len(self.__timesAssigned)):
            if self.__timesAssigned[i] > time:
                self.__timesAssigned.insert(i, time)
                break
        self.__timesAssigned.append(time)

    def assignFrameSlice(self, frame: int, slice: int):
        """Assigns a frame and an slice to this connection.

        :param frame: frame number
        :type frame: int
        :param slice: slice number
        :type slice: int
        """
        self.__frameSlice.append([frame, slice])

    def numberOfSlicesNeeded(
        self, capacityRequired: float, capacityFromAP: float
    ) -> int:
        """
        Determines the number of slices needed by this connection depending on the required and available capacity.

        :param capacityRequired: Capacity required
        :type capacityRequired: float
        :param capacityFromAP: capacity given
        :type capacityFromAP: float
        :return: The number of slices needed by this connection
        :rtype: int
        :raises ValueError: If capacityFromAP is zero or negative.
        """
        if capacityFromAP <= 0:
            raise ValueError("capacityFromAP must be a positive number.")
        return math.ceil(capacityRequired / capacityFromAP)

    def nextSliceInAPWhenArriving(self, ap: AccessPoint) -> int:
        """
        When a new connection arrives, this function gives the position of the next slot depending on the arriving time for an AP.

        :param ap: Access Point
        :type ap: :class:`vlcsim.scene.AccessPoint`
        :return: the slot position
        :rtype: int
        :raises ValueError: If ap.sliceTime or ap.slicesInFrame is None or invalid.
        """
        if ap.sliceTime is None or ap.slicesInFrame is None:
            raise ValueError("AccessPoint must have valid sliceTime and slicesInFrame.")
        if ap.sliceTime <= 0 or ap.slicesInFrame <= 0:
            raise ValueError("sliceTime and slicesInFrame must be positive numbers.")
        return math.ceil(self.__time / ap.sliceTime) % ap.slicesInFrame

    @property
    def receiver(self) -> Receiver:
        """
        Receiver of this connection

        :return: The receiver of this connection.
        :rtype: :class:`vlcsim.scene.Receiver`
        """
        return self.__receiver

    @receiver.setter
    def receiver(self, value: Receiver):
        self.__receiver = value

    @property
    def id(self) -> int:
        """
        ID of this connection

        :return: The ID of this connection.
        :rtype: int
        """
        return self.__id

    @property
    def AP(self) -> Optional[AccessPoint]:
        """
        Access Point of this connection

        :return: The Access Point used by this connection.
        :rtype: Optional[:class:`vlcsim.scene.AccessPoint`]
        """
        return self.__ap

    @AP.setter
    def AP(self, ap: Optional[AccessPoint]):
        self.__ap = ap

    @property
    def allocated(self) -> bool:
        """
        Is this connection allocated?

        :return: True if the connection was allocated. False otherwise.
        :rtype: bool
        """
        return self.__allocated

    @allocated.setter
    def allocated(self, value: bool):
        self.__allocated = value


class Controller:
    """
    Class in charge of controlling which connections are assigned and which are not. Contains the routines necessary to assign, unassign, pause and resume a connection.
    """

    status = Enum("status", "ALLOCATED NOT_ALLOCATED WAIT")
    """
    Corresponds to the status that the allocation algorithm could return. The status **ALLOCATED** means that the connection was succesfully allocated. **NOT_ALLOCATED** means that the allocation algorithm could not allocate the connection, and this was refused. And finally, the **WAIT** means that the connection will wait to be allocated in the future, using the same allocation algorithm.
    """

    nextStatus = Enum("nextStatus", "PAUSE FINISH RESUME IDLE RND_WAIT")

    """
    Corresponds to the status used inside every allocation routine in the controller module. The status **PAUSE** means that the connection was PAUSED, and it will be resumed in the future. **FINISH** means that the connection completes its work, and finished. The **RESUME** means the connection will start again the transmission after a **PAUSE**. **IDLE** means that the AP is not connected to any receiver. Finally, **RND_WAIT** means that the connection could not be connected, and the controller will wait a random time to try again. 
    """

    def __init__(self, x: float, y: float, z: float, nGrids: int, rho: float) -> None:
        """
        Controller constructor.

        :param x: Length of the scenario.
        :type x: float
        :param y: Width of the scenario.
        :type y: float
        :param z: Height of the scenario.
        :type z: float
        :param nGrids: Number of segments on each dimension. Useful for precision.
        :type nGrids: int
        :param rho: Reflection coefficient. A floating number between 0 and 1.
        :type rho: float
        """

        self.__scenario: Scenario = Scenario(x, y, z, nGrids, rho)
        self.__allocator: Optional[
            Callable[
                [Receiver, Connection, Scenario, "Controller"], tuple[int, Connection]
            ]
        ] = None
        self.__allocationStatus: Optional[int] = None
        self.__activeConnections: list[list[list[Union[bool, Connection]]]] = []
        self.__numberActiveConnections: list[int] = []
        # self.__activeConnections = [[]] * len(self.__scenario.vleds)

    @property
    def scenario(self) -> Scenario:
        """
        Scenario controlled by this controller.

        :return: The scenario of this controller.
        :rtype: :class:`vlcsim.scene.Scenario`
        """
        return self.__scenario

    @property
    def allocationStatus(self):
        """
        The last status of the allocation process

        :return: The last status of the allocation process
        :rtype: :class:`vlcsim.controller.status`
        """
        return self.__allocationStatus

    @property
    def allocator(self):
        """
        The method used to allocate connections. This method could be replaced with any other method using always this signature:

        .. code-block:: python
            :linenos:

            def alloc_function(receiver: Receiver, connection: Connection, scenario: Scenario, controller: Controller):
                # Some allocation algorithm using the parameters.

                # It have to return an status and the connection in a tuple


        An example of an allocation method:

        .. code-block:: python
            :linenos:
            :emphasize-lines: 1,11

            def alloc(receiver, connection, scenario: Scenario, controller: Controller):
                vleds = scenario.vleds
                snrFromVleds = []
                for vled in vleds:
                    snrFromVleds.append(scenario.snrVled(receiver, vled))
                posBestSNR = snrFromVleds.index(max(snrFromVleds))
                if controller.numberOfActiveConnections(posBestSNR) > 2:
                    return Controller.status.WAIT, connection
                else:
                    connection.AP = vleds[posBestSNR]
                return Controller.status.ALLOCATED, connection
        """
        return self.__allocator

    @allocator.setter
    def allocator(
        self,
        allocator: Callable[
            [Receiver, Connection, Scenario, "Controller"], tuple[int, Connection]
        ],
    ):
        """Set the allocation algorithm.

        :param allocator: The allocator function to use
        :type allocator: Callable
        """
        self.__allocator = allocator

    def assignConnection(self, connection: Connection, time: float):
        """
        Routine that assigns a connection to a specific AP. This routine uses the allocator method to make the assignment.

        :param connection: Connection object that will try to connect.
        :type connection: :class:`vlcsim.controller.Connection`
        :param time: Instant in which this connection tried to connect.
        :type time: float
        """
        if self.__allocator is None:
            raise ValueError(
                "Allocator function must be set before assigning connections."
            )

        self.__allocationStatus, connection = self.__allocator(
            connection.receiver, connection, self.__scenario, self
        )
        connection.receiver.timeFirstConnected = time

        if self.__allocationStatus == Controller.status.ALLOCATED:
            if connection.AP is None:
                raise ValueError(
                    "Connection must have a valid AccessPoint (AP) after allocation."
                )
            index = self.APPosition(connection.AP)
            self.__numberActiveConnections[index] += 1
            actualSlice = connection.nextSliceInAPWhenArriving(connection.AP) - 1
            actualTime = (
                (time // (connection.AP.slicesInFrame * connection.AP.sliceTime))
                * connection.AP.slicesInFrame
                * connection.AP.sliceTime
            )
            auxPreviousTime = 0
            if actualSlice == -1:
                auxPreviousTime = 1
            if (
                connection.receiver.capacityFromAP is None
                or connection.AP.sliceTime is None
            ):
                raise ValueError(
                    "Receiver must have capacityFromAP and AP must have sliceTime set."
                )
            if connection.capacityRequired is None:
                raise ValueError("Connection must have capacityRequired set.")
            connection.receiver.goalTime = (
                connection.capacityRequired
                / connection.receiver.capacityFromAP
                * connection.AP.sliceTime
            )
            connection.goalTime = connection.receiver.goalTime
            for fs in connection.frameSlice:
                if fs[0] <= 0 and fs[1] <= actualSlice:
                    raise Exception("You are trying to assign a slice in the past...")
                self.assignSlice(index, fs[0], fs[1], connection)
                connection.insertTime(
                    actualTime
                    + connection.AP.sliceTime
                    * connection.AP.slicesInFrame
                    * (fs[0] + auxPreviousTime)
                    + fs[1] * connection.AP.sliceTime
                )
            time = connection.getNextTime()
            return Controller.nextStatus.RESUME, time, connection
        elif self.__allocationStatus == Controller.status.NOT_ALLOCATED:
            return Controller.nextStatus.IDLE, time, connection
        elif self.__allocationStatus == Controller.status.WAIT:
            return Controller.nextStatus.RND_WAIT, time, connection
        else:
            raise Exception("Return status of allocation algorithm not supported")

    def pauseConnection(self, connection: Connection, time: float):
        """
        Routine that pauses a connection to a specific AP.

        :param connection: Connection object that will be paused.
        :type connection: :class:`vlcsim.controller.Connection`
        :param time: Instant in which this connection was paused.
        :type time: float
        """
        if connection.AP is None:
            raise ValueError("Connection must have a valid AccessPoint (AP).")

        receiver = connection.receiver
        index = self.APPosition(connection.AP)
        receiver.timeActive += connection.AP.sliceTime
        timeNext = connection.getNextTime()
        nextSlice = connection.nextSliceInAPWhenArriving(connection.AP)
        flag = False
        for i in range(nextSlice, connection.AP.slicesInFrame):
            if len(self.__activeConnections[index]) == 0:
                flag = True
                break
            if self.__activeConnections[index][0][i] != False:
                flag = True
                break
        if not flag:
            self.__activeConnections[index].pop(0)
        return Controller.nextStatus.RESUME, timeNext, connection

    def resumeConnection(self, connection: Connection, time: float):
        """
        Routine that resumes a connection to a specific AP.

        :param connection: Connection object that will be resumed.
        :type connection: :class:`vlcsim.controller.Connection`
        :param time: Instant in which this connection was resumed.
        :type time: float
        """
        if connection.AP is None:
            raise ValueError("Connection must have a valid AccessPoint (AP).")

        receiver = connection.receiver
        if receiver.goalTime is None:
            raise ValueError(
                "Receiver must have goalTime set before resuming connection."
            )
        if receiver.goalTime < receiver.timeActive + connection.AP.sliceTime:
            return (
                Controller.nextStatus.FINISH,
                time + receiver.goalTime - receiver.timeActive,
                connection,
            )
        else:
            return (
                Controller.nextStatus.PAUSE,
                time + connection.AP.sliceTime,
                connection,
            )

    def unassignConnection(self, connection: Connection, time: float):
        """
        Routine that unassigns a connection from a specific AP.

        :param connection: Connection object that will be unassigned.
        :type connection: :class:`vlcsim.controller.Connection`
        :param time: Instant in which this connection was unassigned.
        :type time: float
        """
        if connection.AP is None:
            raise ValueError("Connection must have a valid AccessPoint (AP).")

        index = self.APPosition(connection.AP)
        receiver = connection.receiver
        if receiver.goalTime is not None:
            receiver.timeActive = receiver.goalTime
        receiver.timeFinished = time

        nextSlice = connection.nextSliceInAPWhenArriving(connection.AP)
        flag = False
        for i in range(nextSlice, connection.AP.slicesInFrame):
            if len(self.__activeConnections[index]) == 0:
                break
            if self.__activeConnections[index][0][i] != False:
                flag = True
        if flag:
            self.__activeConnections[index].pop(self.__activeConnection[index])
        self.__numberActiveConnections[index] -= 1
        return Controller.nextStatus.IDLE, time, None

    def init(self):
        """Initialize the controller's internal structures for simulation.

        Must be invoked before starting the simulation.
        Initializes the active connections lists and the count of connections per AP.
        Performs robustness checks on the scenario and APs configuration.

        :raises RuntimeError: If the scenario or APs are not correctly configured.
        """
        self.__activeConnections = []
        self.__numberActiveConnections = []

        if not hasattr(self, "_Controller__scenario") or self.__scenario is None:
            raise RuntimeError("The scenario is not configured in the controller.")
        vleds = getattr(self.__scenario, "vleds", None)
        rfs = getattr(self.__scenario, "rfs", None)
        if vleds is None or rfs is None:
            raise RuntimeError("The scenario must have 'vleds' and 'rfs' lists.")

        nvleds = 0
        nrfs = 0
        actual = None
        total_aps = len(vleds) + len(rfs)
        for i in range(total_aps):
            self.__numberActiveConnections.append(0)
            self.__activeConnections.append([])
            if nvleds < len(vleds) and self.APPosition(vleds[nvleds]) == i:
                actual = vleds[nvleds]
                nvleds += 1
            elif nrfs < len(rfs):
                actual = rfs[nrfs]
                nrfs += 1
            else:
                raise RuntimeError(f"Could not determine the AP for index {i}.")

            if not hasattr(actual, "slicesInFrame") or actual.slicesInFrame is None:
                raise RuntimeError(
                    f"The AP at position {i} does not have 'slicesInFrame' defined."
                )
            if not isinstance(actual.slicesInFrame, int) or actual.slicesInFrame <= 0:
                raise RuntimeError(
                    f"'slicesInFrame' must be a positive integer for the AP at position {i}."
                )

            self.__activeConnections[-1].append([])
            for _ in range(actual.slicesInFrame):
                self.__activeConnections[-1][-1].append(False)
        self.__activeConnection = [0] * len(self.__activeConnections)

    def APPosition(self, ap: Union[VLed, RF, AccessPoint]) -> int:
        """
        Returns the AP position. The AP could be a VLed or a Femtocell, so this method gets the position of this AP in the controller.

        :param ap: The AP.
        :type ap: :class:`vlcsim.scene.VLed` or :class:`vlcsim.scene.RF`
        :return: The position of the AP.
        :rtype: int
        """
        if isinstance(ap, VLed):
            return self.__scenario.vledsPositions[ap.ID]
        elif isinstance(ap, RF):
            return self.__scenario.rfsPositions[ap.ID]
        return -1  # Should not reach here

    @property
    def activeConnections(self):
        """
        This attribute is a list of a list of Connections. Every element on the first list corresponds to a list of active connections in every Access Point. In that way, the cardinality of the outer list is equal to the number of Access Points in the scenario, while the number of connections in every access point is the cardinality of each item of the outer list.

        :return: A list that contains *n* lists, where *n* is the number of APs in the scenario. Each item on the *n* lists corresponds to the connections associated to each AP.
        :rtype: list of lists
        """

        return self.__activeConnections

    def numberOfActiveConnections(self, ap: Union[VLed, RF, AccessPoint]) -> int:
        """
        The number of active connections in this AP.

        :param ap: The access point.
        :type ap: :class:`vlcsim.scene.VLed` or :class:`vlcsim.scene.RF`
        :return: The number of active connections in this AP.
        :rtype: int
        """
        return self.__numberActiveConnections[self.APPosition(ap)]

    def assignSlice(self, apIndex: int, frame: int, slice: int, connection: Connection):
        """Assigns the connection to the AP, in the frame and slice given.

        :param apIndex: Index of the AP.
        :type apIndex: int
        :param frame: Frame to be used.
        :type frame: int
        :param slice: Slice to be used.
        :type slice: int
        :param connection: Connection to be assigned.
        :type connection: Connection
        """
        numberOfFrames = len(self.__activeConnections[apIndex])
        if numberOfFrames < frame + 1:
            for _ in range(numberOfFrames, frame + 1):
                self.__activeConnections[apIndex].append([])
                for __ in range(connection.AP.slicesInFrame):
                    self.__activeConnections[apIndex][-1].append(False)

        if self.__activeConnections[apIndex][frame][slice] != False:
            raise ValueError(
                f"The connection in frame: {frame}, slice: {slice} is already used."
            )
        else:
            self.__activeConnections[apIndex][frame][slice] = connection

    def framesState(self, ap: AccessPoint):
        """Return the set of frame/slices of the ap given.

        :param ap: Access Point
        :type ap: AccessPoint
        :return: A list of lists containing the frames and slices of this AP
        """
        return self.__activeConnections[self.APPosition(ap)]

    @staticmethod
    def default_alloc(
        receiver: "Receiver",
        connection: Connection,
        scenario: Scenario,
        controller: "Controller",
    ) -> tuple[Any, Connection]:
        vleds = scenario.vleds
        rfs = scenario.rfs
        vled_snr: list[float] = []
        rf_snr: list[float] = []
        for vled in vleds:
            vled_snr.append(scenario.snrVled(receiver, vled))
        for rf in rfs:
            rf_snr.append(scenario.snrRf(receiver, rf))

        numberOfSlices = 0
        number_better_rf = 0
        for vled_pos in range(len(vleds)):
            number_better_rf = 0
            for rf_pos in range(len(rfs)):
                if vled_snr[vled_pos] > rf_snr[rf_pos]:
                    if controller.numberOfActiveConnections(vleds[vled_pos]) < 5:
                        connection.AP = vleds[vled_pos]
                        connection.receiver.capacityFromAP = scenario.capacityVled(
                            receiver, connection.AP
                        )
                        numberOfSlices = connection.numberOfSlicesNeeded(
                            connection.capacityRequired,
                            connection.receiver.capacityFromAP,
                        )
                        connection.snr = vled_snr[vled_pos]
                        if numberOfSlices < 5:
                            number_better_rf += 1
                            break
                    else:
                        number_better_rf += 1
                        break
                else:
                    number_better_rf += 1
                    break
            if number_better_rf == 0:
                break
        if number_better_rf != 0:
            best_rf_SNR = sys.float_info.min
            best_rf = None
            for rf_pos in range(len(rfs)):
                if rf_snr[rf_pos] > best_rf_SNR:
                    best_rf_SNR = rf_snr[rf_pos]
                    best_rf = rf_pos
            if best_rf is not None:
                connection.AP = rfs[best_rf]
                connection.receiver.capacityFromAP = scenario.capacityRf(
                    receiver, connection.AP
                )
                numberOfSlices = connection.numberOfSlicesNeeded(
                    connection.capacityRequired, connection.receiver.capacityFromAP
                )
                connection.snr = rf_snr[best_rf]
        else:
            for vled_pos in range(len(vleds)):
                if vled_snr[vled_pos] > connection.snr:
                    connection.AP = vleds[vled_pos]
                    connection.receiver.capacityFromAP = scenario.capacityVled(
                        receiver, connection.AP
                    )
                    numberOfSlices = connection.numberOfSlicesNeeded(
                        connection.capacityRequired, connection.receiver.capacityFromAP
                    )
                    connection.snr = vled_snr[vled_pos]

        if connection.AP is None:
            raise ValueError("Failed to assign an AccessPoint to the connection.")

        if (
            connection.capacityRequired is None
            or connection.receiver.capacityFromAP is None
        ):
            raise ValueError(
                "Connection must have valid capacityRequired and capacityFromAP."
            )

        actualSlice = connection.nextSliceInAPWhenArriving(connection.AP)
        aux = 0
        auxFrame = 0

        # Actual frame
        for slice in range(actualSlice, connection.AP.slicesInFrame):
            if (
                len(controller.framesState(connection.AP)) == 0
                or controller.framesState(connection.AP)[0][slice] == False
            ):
                connection.assignFrameSlice(0, slice)
                aux += 1
                break

        # next frames
        for frameIndex in range(1, len(controller.framesState(connection.AP))):
            for slice in range(connection.AP.slicesInFrame):
                if controller.framesState(connection.AP)[frameIndex][slice] == False:
                    connection.assignFrameSlice(frameIndex, slice)
                    aux += 1
                    auxFrame = frameIndex
                    break

            if aux == numberOfSlices:
                break

        frameIndex = auxFrame + 1
        while aux < numberOfSlices:
            connection.assignFrameSlice(frameIndex, 0)
            frameIndex += 1
            aux += 1
        return Controller.status.ALLOCATED, connection
