"""Scenario class for managing VLC simulation environments."""

import numpy as np
import math as m
import warnings
from typing import List, Optional, Any

from .vled import VLed
from .rf import RF
from .receiver import Receiver


class Scenario:
    """Represents the simulation scenario's physical environment and configuration.

    The Scenario class manages the 3D room dimensions, lighting infrastructure
    (VLEDs and RF femtocells), receivers, and performs channel gain and capacity
    calculations for both line-of-sight and wall-reflected signals.

    Attributes:
        numberOfAPs: Class variable tracking the total number of access points
            (VLEDs + RF) created across all scenarios.

    Properties:
        - Dimensions: width, length, height, start_x/y, end_x/y
        - Infrastructure: vleds, rfs, vledsPositions, rfsPositions
        - Counts: numberOfVLeds, numberOfRFs

    Example:
        >>> scenario = Scenario(width=5.0, length=5.0, height=3.0, nGrids=5, rho=0.8)
        >>> vled = VLed(x=0, y=0, z=3.0, ml=1.0, pt=10.0)
        >>> scenario.addVLed(vled)
        >>> scenario.numberOfVLeds
        1
    """

    numberOfAPs = 0
    """Class variable tracking the total number of access points created."""

    warnings.filterwarnings(
        "ignore", message="invalid value encountered in double_scalars"
    )

    def __init__(
        self, width: float, length: float, height: float, nGrids: int, rho: float
    ) -> None:
        """Initialize a simulation scenario with room dimensions and parameters.

        Args:
            width: Room width in meters (Y-axis dimension)
            length: Room length in meters (X-axis dimension)
            height: Room height in meters (Z-axis dimension)
            nGrids: Number of grid divisions per meter for discretization
            rho: Wall reflection coefficient (0.0 to 1.0)

        Note:
            The room coordinate system is centered at (0, 0, 0) in the XY plane,
            with Z extending from 0 (floor) to height (ceiling).
        """

        self.__length: float = length  # x
        self.__width: float = width  # y
        self.__height: float = height  # z

        self.__rho: float = rho

        self.__start_x: float = -self.__width / 2
        self.__start_y: float = -self.__length / 2
        self.__end_x: float = self.__width / 2
        self.__end_y: float = self.__length / 2

        self.__mobile_terminals: List[Receiver] = []
        self.__vleds: List[VLed] = []
        self.__femtocells: List[RF] = []

        self.__vledsPositions: List[int] = []
        self.__rfsPositions: List[int] = []

        self.__nx = round(self.__length * nGrids)
        self.__ny = round(self.__width * nGrids)
        self.__nz = round(self.__height * nGrids)

        self.__g_x = np.linspace(self.__start_x, self.__end_x, self.__nx)
        self.__g_y = np.linspace(self.__start_y, self.__end_y, self.__ny)
        self.__g_z = np.linspace(0, self.__height, self.__nz)

        self.__g_xyz = np.array([self.__g_x, self.__g_y, self.__g_z], dtype=object)

    def addVLed(self, vled: "VLed") -> None:
        """Add a VLED access point to the scenario.

        Args:
            vled: The VLED device to add
        """
        self.__vleds.append(vled)
        self.__vledsPositions.append(Scenario.numberOfAPs)
        Scenario.numberOfAPs += 1

    def addRF(self, rf: "RF") -> None:
        """Add an RF femtocell to the scenario.

        Args:
            rf: The RF femtocell device to add
        """
        self.__femtocells.append(rf)
        self.__rfsPositions.append(Scenario.numberOfAPs)
        Scenario.numberOfAPs += 1

    def getPowerInPointFromWalls(
        self, receiver: "Receiver", vledID: Optional[int]
    ) -> float:
        """Calculate the received optical power from wall reflections.

        Args:
            receiver: The receiver device at the measurement point
            vledID: ID of the transmitting VLED, or None

        Returns:
            float: Received power from wall reflections in watts

        Note:
            Returns 0.0 if vledID is None or out of bounds.
        """
        if vledID is None or vledID < 0 or vledID >= len(self.__vleds):
            return 0.0
        vled = self.__vleds[vledID]
        h1 = self.__channelGainWall(receiver, vled, 1, 0)
        h2 = self.__channelGainWall(receiver, vled, 0, 1)
        h3 = self.__channelGainWall(receiver, vled, 1, 2)
        h4 = self.__channelGainWall(receiver, vled, 0, 3)
        power = (h1 + h2 + h3 + h4) * vled.totalPower * receiver.ts * receiver.gCon
        return float(power)

    def getPowerInPointFromVled(
        self, receiver: "Receiver", vledID: Optional[int]
    ) -> float:
        """Calculate the received optical power via line-of-sight from a VLED.

        Args:
            receiver: The receiver device at the measurement point
            vledID: ID of the transmitting VLED, or None

        Returns:
            float: Received LOS power in watts

        Note:
            Returns 0.0 if vledID is None, out of bounds, or receiver is
            outside the FOV of the VLED.
        """
        if vledID is None or vledID < 0 or vledID >= len(self.__vleds):
            return 0.0
        vled = self.__vleds[vledID]
        D_los = m.sqrt(
            (receiver.x - vled.x) ** 2
            + (receiver.y - vled.y) ** 2
            + (receiver.z - vled.z) ** 2
        )
        cosphi = (vled.z - receiver.z) / D_los
        # print(vled.x, vled.y, vled.z)
        # print(receiver.x, receiver.y, receiver.z)
        # print(cosphi)
        r_angle = m.degrees(m.acos(cosphi))
        H = (
            (vled.ml + 1)
            * receiver.aDet
            * cosphi ** (vled.ml + 1)
            / (2 * m.pi * D_los**2)
        )
        power = (
            vled.totalPower * H * receiver.ts * receiver.gCon
            if abs(r_angle) <= receiver.fov
            else 0
        )
        return power

    def __channelGainWall(
        self, receiver: Receiver, vled: VLed, posVar: int, posFixed: int
    ) -> float:
        """Calculate the channel gain contribution from a specific wall.

        Args:
            receiver: The receiver device
            vled: The transmitting VLED
            posVar: Index of the varying position coordinate (0=X, 1=Y, 2=Z)
            posFixed: Index of the fixed wall (0=left X, 1=front Y, 2=right X, 3=back Y)

        Returns:
            float: Channel gain from the specified wall (dimensionless)

        Note:
            This is a private method used internally by getPowerInPointFromWalls().
        """
        wall = None
        dA = self.__height
        if posFixed == 0:
            wall = (self.__start_x, 0)
            dA *= self.__length / (self.__nx * self.__nz)
        elif posFixed == 1:
            wall = (self.__start_y, 1)
            dA *= self.__width / (self.__ny * self.__nz)
        elif posFixed == 2:
            wall = (self.__end_x, 0)
            dA *= self.__length / (self.__nx * self.__nz)
        elif posFixed == 3:
            wall = (self.__end_y, 1)
            dA *= self.__width / (self.__ny * self.__nz)

        if wall is None:
            return 0.0

        h = 0
        wp = np.full(3, wall[0])
        g = self.__g_xyz[posVar]

        # Minimum distance threshold to avoid division by zero
        # epsilon = 1e-10 meters (0.1 nanometers) - physically impossible to have
        # a device exactly at a wall point, but mathematically can occur with grid points
        epsilon = 1e-10

        for i in g:
            wp[posVar] = i
            for j in self.__g_z:
                wp[2] = j
                # D1: Distance from VLed to wall reflection point
                D1 = m.sqrt(np.dot(vled.position - wp, vled.position - wp))

                # Skip if VLed coincides with wall point (D1 ≈ 0 causes division by zero)
                # Physically: VLed cannot be on the wall, so this point has no contribution
                if D1 < epsilon:
                    continue

                cosphi = abs(wp[2] - vled.position[2]) / D1
                cosalpha = abs(vled.position[wall[1]] - wp[wall[1]]) / D1

                # D2: Distance from wall reflection point to receiver
                D2 = m.sqrt(np.dot(wp - receiver.position, wp - receiver.position))

                # Skip if receiver coincides with wall point (D2 ≈ 0 causes division by zero)
                # Physically: receiver cannot be on the wall, so this point has no contribution
                if D2 < epsilon:
                    continue

                cosbeta = abs(wp[wall[1]] - receiver.position[wall[1]]) / D2
                cospsi = abs(wp[2] - receiver.position[2]) / D2

                if abs(m.degrees(m.acos(cospsi))) <= receiver.fov:
                    h = h + (vled.ml + 1) * receiver.aDet * self.__rho * dA * (
                        cosphi**vled.ml
                    ) * cosalpha * cosbeta * cospsi / (
                        2 * (m.pi**2) * (D1**2) * (D2**2)
                    )
        return h

    @property
    def numberOfVLeds(self) -> int:
        """Get the number of VLEDs in this scenario.

        Returns:
            int: Number of VLED access points
        """
        return len(self.__vleds)

    @property
    def numberOfRFs(self) -> int:
        """Get the number of RF femtocells in this scenario.

        Returns:
            int: Number of RF access points
        """
        return len(self.__femtocells)

    @property
    def vleds(self) -> List[VLed]:
        """Get the list of VLEDs in this scenario.

        Returns:
            List[VLed]: List of all VLED access points
        """
        return self.__vleds

    @property
    def rfs(self) -> List[RF]:
        """Get the list of RF femtocells in this scenario.

        Returns:
            List[RF]: List of all RF access points
        """
        return self.__femtocells

    @property
    def start_x(self) -> float:
        """Get the starting X coordinate of the room.

        Returns:
            float: Starting X coordinate in meters (left edge)
        """
        return self.__start_x

    @start_x.setter
    def start_x(self, value: float) -> None:
        """Set the starting X coordinate of the room.

        Args:
            value: Starting X coordinate in meters
        """
        self.__start_x = value

    @property
    def start_y(self) -> float:
        """Get the starting Y coordinate of the room.

        Returns:
            float: Starting Y coordinate in meters (front edge)
        """
        return self.__start_y

    @start_y.setter
    def start_y(self, value: float) -> None:
        """Set the starting Y coordinate of the room.

        Args:
            value: Starting Y coordinate in meters
        """
        self.__start_y = value

    @property
    def end_x(self) -> float:
        """Get the ending X coordinate of the room.

        Returns:
            float: Ending X coordinate in meters (right edge)
        """
        return self.__end_x

    @end_x.setter
    def end_x(self, value: float) -> None:
        """Set the ending X coordinate of the room.

        Args:
            value: Ending X coordinate in meters
        """
        self.__end_x = value

    @property
    def end_y(self) -> float:
        """Get the ending Y coordinate of the room.

        Returns:
            float: Ending Y coordinate in meters (back edge)
        """
        return self.__end_y

    @end_y.setter
    def end_y(self, value: float) -> None:
        """Set the ending Y coordinate of the room.

        Args:
            value: Ending Y coordinate in meters
        """
        self.__end_y = value

    @property
    def height(self) -> float:
        """Get the scenario room height.

        Returns:
            float: Room height in meters (Z-axis dimension)
        """
        return self.__height

    @height.setter
    def height(self, value: float) -> None:
        """Set the scenario room height.

        Args:
            value: Room height in meters
        """
        self.__height = value

    @property
    def length(self) -> float:
        """Get the scenario room length.

        Returns:
            float: Room length in meters (X-axis dimension)
        """
        return self.__length

    @length.setter
    def length(self, value: float) -> None:
        """Set the scenario room length.

        Args:
            value: Room length in meters
        """
        self.__length = value

    @property
    def width(self) -> float:
        """Get the scenario room width.

        Returns:
            float: Room width in meters (Y-axis dimension)
        """
        return self.__width

    @width.setter
    def width(self, value: float) -> None:
        """Set the scenario room width.

        Args:
            value: Room width in meters
        """
        self.__width = value

    @property
    def vledsPositions(self) -> List[int]:
        """Get the access point indices for VLEDs.

        Returns:
            List[int]: List of AP indices corresponding to each VLED
        """
        return self.__vledsPositions

    @property
    def rfsPositions(self) -> List[int]:
        """Get the access point indices for RF femtocells.

        Returns:
            List[int]: List of AP indices corresponding to each RF
        """
        return self.__rfsPositions

    def snrVled(self, receiver: Receiver, vled: VLed) -> float:
        """Calculate the Signal-to-Noise Ratio for a VLC channel.

        Computes the total SNR at a receiver from a VLED, considering both
        line-of-sight and wall-reflected signals, including shot noise and
        thermal noise components.

        Args:
            receiver: Receiver device
            vled: Transmitting VLED

        Returns:
            float: SNR (dimensionless), or 0.0 if noise is zero
        """
        vled_id = vled.ID if vled.ID is not None else -1
        powerReceived = self.getPowerInPointFromVled(
            receiver, vled_id
        ) + self.getPowerInPointFromWalls(receiver, vled_id)
        rd = (2 * receiver.q * receiver.s * powerReceived * receiver.b) + (
            2 * receiver.q * receiver.ibg * receiver.i1 * receiver.b
        )
        rt = (
            8
            * m.pi
            * receiver.cb
            * receiver.tk
            * receiver.n
            * receiver.a
            * receiver.b**2
            * (
                (receiver.i1 / receiver.gv)
                + (2 * m.pi)
                * receiver.fr
                / receiver.gm
                * receiver.n
                * receiver.a
                * receiver.i2
                * receiver.b
            )
        )
        rg = rd + rt
        return (receiver.s * powerReceived) ** 2 / rg if rg != 0 else 0.0

    def snrRf(self, receiver: Receiver, rf: RF) -> float:
        """Calculate the Signal-to-Noise Ratio for an RF channel.

        Computes the SNR at a receiver from an RF femtocell using the
        pathloss model.

        Args:
            receiver: Receiver device
            rf: Transmitting RF femtocell

        Returns:
            float: SNR (dimensionless)
        """
        df = m.sqrt(
            (receiver.x - rf.x) ** 2
            + (receiver.y - rf.y) ** 2
            + (receiver.z - rf.z) ** 2
        )

        return rf.pf * rf.Af * df ** (-rf.Ef)

    def capacityVled(self, receiver: Receiver, vled: VLed) -> float:
        """Calculate the channel capacity for a VLC link.

        Uses Shannon's capacity formula with the SNR from the VLED channel.

        Args:
            receiver: Receiver device
            vled: Transmitting VLED

        Returns:
            float: Channel capacity in bits/s
        """
        return vled.B * m.log2(1 + self.snrVled(receiver, vled))

    def capacityRf(self, receiver: Receiver, rf: RF) -> float:
        """Calculate the channel capacity for an RF link.

        Uses Shannon's capacity formula with the SNR from the RF channel.

        Args:
            receiver: Receiver device
            rf: Transmitting RF femtocell

        Returns:
            float: Channel capacity in bits/s
        """
        return rf.B * m.log2(1 + self.snrRf(receiver, rf))
