"""
The controller for altering arena lighting provided by a DirectionalLight and a Background
Currently doesn't support:
- Timed pre-match lighting changes
"""
import sys
from typing import Dict, List, Tuple, Optional, NamedTuple
from pathlib import Path
from dataclasses import dataclass

from controller import Node, Supervisor

REPO_ROOT = Path(__file__).resolve().parent.parent.parent

sys.path.insert(1, str(REPO_ROOT / 'modules'))

import controller_utils  # isort:skip
import webots_utils  # isort:skip


class ArenaLighting(NamedTuple):
    light_def: str
    intensity: float = 3.5
    colour: Tuple[float, float, float] = (1, 1, 1)


class LightingEffect(NamedTuple):
    start_time: float  # Negative start times are relative to the end of the match
    # Negative values 0 - (-0.08) don't get included in the produced recordings
    fade_time: Optional[float] = None
    lighting: List[ArenaLighting] = [ArenaLighting('SUN')]
    luminosity: float = 0.35
    name: str = ""

    def __repr__(self) -> str:
        lights_info = [
            f"({light.light_def}, int={light.intensity}, col={light.colour})"
            for light in self.lighting
        ]
        return (
            f"<LightingEffect: {self.name!r}, "
            f"start={self.start_time}, fade={self.fade_time}, "
            f"lum={self.luminosity}, "
            f"{', '.join(lights_info)}"
            ">"
        )


@dataclass
class LightFade:
    light: Node
    remaining_steps: int
    intensity_step: float
    colour_step: Tuple[float, float, float]
    current_intensity: float
    current_colour: Tuple[float, float, float]
    effect: ArenaLighting


@dataclass
class LuminosityFade:
    remaining_steps: int
    luminosity_step: float
    current_luminosity: float
    final_luminosity: float


CUE_STACK = [
    LightingEffect(
        0,
        lighting=[ArenaLighting('SUN', intensity=1)],
        luminosity=0.05,
        name="Pre-set",
    ),
    LightingEffect(0, fade_time=1.5, name="Fade-up"),
    LightingEffect(
        -0.1,
        lighting=[ArenaLighting('SUN', intensity=1, colour=(1, 0, 0))],
        luminosity=0.05,
        name="End of match",
    ),
]


class LightingController:
    def __init__(self, duration: float, cue_stack: List[LightingEffect]) -> None:
        self._robot = Supervisor()
        self.timestep = self._robot.getBasicTimeStep()
        self.start_offset: float = 0

        self.duration = duration
        self.cue_stack = cue_stack

        self.ambient_node = webots_utils.node_from_def(self._robot, 'AMBIENT')

        self.luminosity_fade = LuminosityFade(0, 0, 0.35, 0.35)
        self.lighting_fades: List[LightFade] = []

        # fetch all nodes used in effects, any missing nodes will be flagged here
        self.light_nodes: Dict[str, Node] = {}
        for cue in cue_stack:
            for light in cue.lighting:
                if self.light_nodes.get(light.light_def) is None:
                    self.light_nodes[light.light_def] = webots_utils.node_from_def(
                        self._robot,
                        light.light_def,
                    )

    def set_node_luminosity(self, node: Node, luminosity: float) -> None:
        node.getField('luminosity').setSFFloat(luminosity)

    def set_node_intensity(self, node: Node, intensity: float) -> None:
        node.getField('intensity').setSFFloat(intensity)

    def set_node_colour(self, node: Node, colour: Tuple[float, float, float]) -> None:
        node.getField('color').setSFColor(list(colour))

    def get_current_luminosity(self) -> float:
        return self.ambient_node.getField('luminosity').getSFFloat()

    def get_current_lighting_values(self, light_def: str) -> ArenaLighting:
        light = self.light_nodes[light_def]
        return ArenaLighting(
            light_def,
            light.getField('intensity').getSFFloat(),
            light.getField('color').getSFColor(),  # type: ignore
        )

    def increment_colour(
        self,
        colour: Tuple[float, float, float],
        step: Tuple[float, float, float],
    ) -> Tuple[float, float, float]:
        return tuple(colour[i] + step[i] for i in range(3))  # type: ignore

    def current_match_time(self) -> float:
        return self._robot.getTime() - self.start_offset

    def remaining_match_time(self) -> float:
        return self.duration - self.current_match_time()

    def start_lighting_effect(self, effect: LightingEffect) -> None:
        print(  # noqa: T001
            f"Running lighting effect: {effect.name} @ {self.current_match_time()}",
        )

        if effect.fade_time is None:
            self.set_node_luminosity(self.ambient_node, effect.luminosity)
            for light in effect.lighting:
                self.set_node_intensity(self.light_nodes[light.light_def], light.intensity)
                self.set_node_colour(self.light_nodes[light.light_def], light.colour)

            print(f"Lighting effect '{effect.name}' complete")  # noqa: T001

        else:
            steps = int((effect.fade_time * 1000) / self.timestep)

            # get starting values
            current_luminosity = self.get_current_luminosity()
            luminosity_step = (effect.luminosity - current_luminosity) / steps
            self.luminosity_fade = LuminosityFade(
                steps,
                luminosity_step,
                current_luminosity,
                effect.luminosity,
            )

            for light in effect.lighting:
                # get starting values
                (
                    _,
                    current_intensity,
                    current_colour,
                ) = self.get_current_lighting_values(light.light_def)

                # figure out steps of each value
                intensity_step = (light.intensity - current_intensity) / steps
                colour_step: Tuple[float, float, float] = tuple(  # type: ignore
                    light.colour[i] - current_colour[i]
                    for i in range(3)
                )

                # add fade to queue to have steps processed
                self.lighting_fades.append(LightFade(
                    self.light_nodes[light.light_def],
                    steps,
                    intensity_step,
                    colour_step,
                    current_intensity,
                    current_colour,
                    light,
                ))

    def do_lighting_step(self) -> None:
        if self.luminosity_fade.remaining_steps != 0:
            self.luminosity_fade.current_luminosity += self.luminosity_fade.luminosity_step
            self.luminosity_fade.remaining_steps -= 1

            if self.luminosity_fade.remaining_steps == 0:  # final step
                self.luminosity_fade.current_luminosity = self.luminosity_fade.final_luminosity

            self.set_node_luminosity(
                self.ambient_node,
                self.luminosity_fade.current_luminosity,
            )

        for fade in self.lighting_fades:
            if fade.remaining_steps > 1:
                fade.current_intensity += fade.intensity_step
                fade.current_colour = self.increment_colour(
                    fade.current_colour,
                    fade.colour_step,
                )
                fade.remaining_steps -= 1

                self.set_node_intensity(fade.light, fade.current_intensity)
                self.set_node_colour(fade.light, fade.current_colour)
            else:
                self.set_node_intensity(fade.light, fade.effect.intensity)
                self.set_node_colour(fade.light, fade.effect.colour)

                print(f"Lighting effect for '{fade.effect.light_def}' complete")  # noqa: T001

                self.lighting_fades.remove(fade)  # remove completed fade

    def schedule_lighting(self) -> None:
        if controller_utils.get_robot_mode() != 'comp':
            return

        print('Scheduled cues:')  # noqa: T001
        for cue in self.cue_stack:
            print(cue)  # noqa: T001

        # run pre-start snap changes
        for cue in self.cue_stack.copy():
            if cue.start_time == 0 and cue.fade_time is None:
                self.start_lighting_effect(cue)
                self.cue_stack.remove(cue)

        # Interact with the supervisor "robot" to wait for the start of the match.
        while self._robot.getCustomData() != 'start':
            self._robot.step(int(self.timestep))

        self.start_offset = self._robot.getTime()

        while self.cue_stack:
            for cue in self.cue_stack.copy():
                if (
                    cue.start_time >= 0 and
                    self.current_match_time() >= cue.start_time
                ):  # cue relative to start
                    self.start_lighting_effect(cue)
                    self.cue_stack.remove(cue)
                elif (
                    cue.start_time < 0 and
                    self.remaining_match_time() <= -(cue.start_time)
                ):  # cue relative to end
                    self.start_lighting_effect(cue)
                    self.cue_stack.remove(cue)

            self.do_lighting_step()
            self._robot.step(int(self.timestep))


if __name__ == "__main__":
    lighting_controller = LightingController(
        controller_utils.get_match_duration_seconds(),
        CUE_STACK,
    )
    lighting_controller.schedule_lighting()
