import numpy as np
import cv2
import logging
import time

from paho.mqtt.client import MQTTMessage
from typing import List
from queue import Queue

from Model.Resolutions import ESP32Resolution
from Model.MQTTObserver import MQTTObserver
from Model.MQTTClient import MQTTClient


class ESP32Cam(MQTTObserver):
    """
    This class contains the state and configurations of the ESP32 camera.
    """

    def __init__(self, id: str):
        """
        Initializes the ESP32 camera.
        """
        self._id = id
        self._streaming = False # Flag to indicate whether the camera is streaming
        self._images = Queue(maxsize=5) # Queue to store the last 5 images
        self._resolution = ESP32Resolution.FRAMESIZE_CIF # Default resolution
        self._brightness = 0 # Default brightness
        self._contrast = 0 # Default contrast
        self._saturation = 0 # Default saturation
        self._frame_times = Queue(maxsize=5) # Queue to store the time of last 5 frames    
        self._automatic_exposure = True # Default automatic exposure
        self._automatic_gain = True # Default automatic gain
        self._exposure = 300 # Default exposure value
        self._gain = 15 # Default gain value
        self._vertical_flip = False # Default vertical flip
        self._horizontal_flip = False # Default horizontal flip
        self._flash_led = False # Default LED state

        # Constants for camera settings
        self.MAX_FRAMERATE = 45 # Maximum frame rate of the camera (approximate)
        self.MIN_BRIGHTNESS = -2
        self.MAX_BRIGHTNESS = 2   
        self.MIN_CONTRAST = -2
        self.MAX_CONTRAST = 2
        self.MIN_SATURATION = -2
        self.MAX_SATURATION = 2
        self.MIN_EXPOSURE = 0
        self.MAX_EXPOSURE = 1200
        self.MIN_GAIN = 0
        self.MAX_GAIN = 30


    @property
    def id(self) -> str:
        """
        Returns the ID of the camera.
        
        Returns:
        str: The ID of the camera.
        """
        return self._id


    @property
    def framerate(self) -> int:
        """
        Returns the frame rate of the camera.
        
        Returns:
        int: The frame rate of the camera.
        """
        if len(self._frame_times.queue) == 0:
            return 0
        
        elapsed_time = self._frame_times.queue[-1] - self._frame_times.queue[0]
        if elapsed_time == 0:
            return 0
        
        return (int) (len(self._frame_times.queue) / elapsed_time)


    @property
    def resolution(self) -> ESP32Resolution:
        """
        Returns the resolution of the camera.
        
        Returns:
        ESP32Resolution: The resolution of the camera.
        """
        return self._resolution
    

    @resolution.setter
    def resolution(self, resolution: str) -> None:
        """
        Sets the resolution of the camera.

        Args:
        resolution (ESP32Resolution): The resolution of the camera.

        Raises:
        ValueError: If the resolution is not supported.
        """
        if resolution not in ESP32Resolution:
            raise ValueError(f"Resolution not supported")

        resolution = ESP32Resolution(resolution)
        self._resolution = resolution
        logging.info(f"Resolution updated to {resolution.value}")


    @property
    def brightness(self) -> int:
        """
        Returns the brightness of the camera.
        
        Returns:
        int: The brightness of the camera.
        """
        return self._brightness
    

    @brightness.setter
    def brightness(self, brightness: int) -> None:
        """
        Sets the brightness of the camera.
        
        Args:
        brightness (int): The brightness of the camera.
        
        Raises:
        ValueError: If the brightness is not in the valid range.
        """
        if brightness < self.MIN_BRIGHTNESS or brightness > self.MAX_BRIGHTNESS:
            raise ValueError(f"Brightness must be between {self.MIN_BRIGHTNESS} and {self.MAX_BRIGHTNESS}")
        self._brightness = brightness
        logging.info(f"Brightness updated to {brightness}")


    @property
    def contrast(self) -> int:
        """
        Returns the contrast of the camera.
        
        Returns:
        int: The contrast of the camera.
        """
        return self._contrast
    

    @contrast.setter
    def contrast(self, contrast: int) -> None:
        """
        Sets the contrast of the camera.
        
        Args:
        contrast (int): The contrast of the camera.
        
        Raises:
        ValueError: If the contrast is not in the valid range.
        """
        if contrast < self.MIN_CONTRAST or contrast > self.MAX_CONTRAST:
            raise ValueError(f"Contrast must be between {self.MIN_CONTRAST} and {self.MAX_CONTRAST}")
        self._contrast = contrast
        logging.info(f"Contrast updated to {contrast}")


    @property
    def saturation(self) -> int:
        """
        Returns the saturation of the camera.
        
        Returns:
        int: The saturation of the camera.
        """
        return self._saturation
    

    @saturation.setter
    def saturation(self, saturation: int) -> None:
        """
        Sets the saturation of the camera.
        
        Args:
        saturation (int): The saturation of the camera.
        """
        if saturation < self.MIN_SATURATION or saturation > self.MAX_SATURATION:
            raise ValueError(f"Saturation must be between {self.MIN_SATURATION} and {self.MAX_SATURATION}")
        self._saturation = saturation
        logging.info(f"Saturation updated to {saturation}")



    @property
    def automatic_exposure(self) -> bool:
        """
        Returns a boolean indicating whether the camera is using automatic exposure.
        
        Returns:
        bool: A boolean indicating whether the camera is using automatic exposure.
        """
        return self._automatic_exposure
    

    @automatic_exposure.setter
    def automatic_exposure(self, automatic_exposure: bool) -> None:
        """
        Sets whether the camera should use automatic exposure.
        
        Args:
        automatic_exposure (bool): A boolean indicating whether the camera should use automatic exposure.
        """
        self._automatic_exposure = automatic_exposure
        logging.info(f"Automatic exposure updated to {automatic_exposure}")

    
    @property
    def automatic_gain(self) -> bool:
        """
        Returns a boolean indicating whether the camera is using automatic gain.
        
        Returns:
        bool: A boolean indicating whether the camera is using automatic gain.
        """
        return self._automatic_gain
    

    @automatic_gain.setter
    def automatic_gain(self, automatic_gain: bool) -> None:
        """
        Sets whether the camera should use automatic gain.
        
        Args:
        automatic_gain (bool): A boolean indicating whether the camera should use automatic gain.
        """
        self._automatic_gain = automatic_gain
        logging.info(f"Automatic gain updated to {automatic_gain}")

    
    @property
    def exposure(self) -> int:
        """
        Returns the exposure value of the camera.
        
        Returns:
        int: The exposure value of the camera.
        """
        return self._exposure
    

    @exposure.setter
    def exposure(self, exposure_value: int) -> None:
        """
        Sets the exposure value of the camera.
        
        Args:
        exposure_value (int): The exposure value of the camera.
        """
        if exposure_value < self.MIN_EXPOSURE or exposure_value > self.MAX_EXPOSURE:
            raise ValueError(f"Exposure value must be between {self.MIN_EXPOSURE} and {self.MAX_EXPOSURE}")

        self._exposure = exposure_value
        logging.info(f"Exposure value updated to {exposure_value}")


    @property
    def gain(self) -> int:
        """
        Returns the gain value of the camera.
        
        Returns:
        int: The gain value of the camera.
        """
        return self._gain
    

    @gain.setter
    def gain(self, gain: int) -> None:
        """
        Sets the gain value of the camera.
        
        Args:
        gain_value (int): The gain value of the camera.
        """
        if gain < self.MIN_GAIN or gain > self.MAX_GAIN:
            raise ValueError(f"Gain value must be between {self.MIN_GAIN} and {self.MAX_GAIN}")

        self._gain = gain
        logging.info(f"Gain value updated to {gain}")


    @property
    def vertical_flip(self) -> bool:
        """
        Returns a boolean indicating whether the camera is vertically flipped.
        
        Returns:
        bool: A boolean indicating whether the camera is vertically flipped.
        """
        return self._vertical_flip
    

    @vertical_flip.setter
    def vertical_flip(self, vertical_flip: bool) -> None:
        """
        Sets whether the camera should be vertically flipped.
        
        Args:
        vertical_flip (bool): A boolean indicating whether the camera should be vertically flipped.
        """
        self._vertical_flip = vertical_flip
        logging.info(f"Vertical flip updated to {vertical_flip}")

    
    @property
    def horizontal_flip(self) -> bool:
        """
        Returns a boolean indicating whether the camera is horizontally flipped.
        
        Returns:
        bool: A boolean indicating whether the camera is horizontally flipped.
        """
        return self._horizontal_flip
    

    @horizontal_flip.setter
    def horizontal_flip(self, horizontal_flip: bool) -> None:
        """
        Sets whether the camera should be horizontally flipped.
        
        Args:
        horizontal_flip (bool): A boolean indicating whether the camera should be horizontally flipped.
        """
        self._horizontal_flip = horizontal_flip
        logging.info(f"Horizontal flip updated to {horizontal_flip}")

    @property
    def flash_led(self) -> bool:
        """
        Returns a boolean indicating whether the camera LED is on.
        
        Returns:
        bool: A boolean indicating whether the camera LED is on.
        """
        return self._flash_led
    
    @flash_led.setter
    def flash_led(self, flash_led: bool) -> None:
        """
        Sets whether the camera LED should be on.
        
        Args:
        led_on (bool): A boolean indicating whether the camera LED should be on.
        """
        self._flash_led = flash_led
        logging.info(f"LED state updated to {flash_led}")
        

    # MQTTObserver method implementation
    def update(self, message: MQTTMessage) -> None:
        """
        Updates the camera with the latest image from the ESP32.

        Args:
        message (MQTTMessage): The message containing the latest image from the ESP32.
        """
        if self._streaming:
            self._add_frame(message.payload)

    
    def _add_frame(self, raw_picture: bytes) -> None:

        if self._frame_times.full(): # If the queue is full, discard the oldest frame time
            self._framerate = len(self._frame_times.queue) / (self._frame_times.queue[-1] - self._frame_times.queue[0])
            self._frame_times.get()
        
        self._frame_times.put(time.time()) # Add the current time to the queue of frame
        
        try:
            np_image = np.frombuffer(raw_picture, np.uint8)
            image = cv2.imdecode(np_image, cv2.IMREAD_COLOR)
            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
            
            if self._images.full(): # If the queue is full, discard the oldest image
                self._images.get()
            self._images.put(image)
        
        except cv2.error:
            logging.error("Failed converting image")

    
    def get_frame(self) -> np.ndarray | None:
        """
        Returns the next frame from the queue of images that are being streamed.
        
        Returns:
        np.ndarray | None: The next frame from the queue of images that are being streamed.
        """
        if self._images.empty():
            return None
        image =  self._images.get()
        if len(self._images.queue) == 0:
            self._images.put(image) 
        return image

    
    def stop_stream(self) -> None:
        """
        Stops the streaming of images.
        """
        self._streaming = False
        self._images.queue.clear() # Clear the queue for the next stream
        self._frame_times.queue.clear() # Clear the queue for the next stream

    
    def start_stream(self) -> None:
        """
        Starts the streaming of images.
        """
        self._streaming = True


    def get_resolutions(self) -> List[str]:
        """
        Returns a list of supported resolutions.

        Returns:
        List[ESP32Resolution]: A list of supported resolutions.
        """
        return [resolution for resolution in ESP32Resolution]
    

    def apply_configuration(self) -> None:
        """
        Configures the camera with the current configuration.
        """
        client = MQTTClient()
        data = {"frame_size": str(self._resolution)[1:-1], # Remove the parentheses
                "brightness": self._brightness, 
                "contrast": self._contrast,
                "saturation": self._saturation,
                "auto_exposure": (int)(self._automatic_exposure),
                "auto_gain": (int)(self._automatic_gain),
                "exposure_value": self._exposure,
                "gain_value": self._gain,
                "vertical_flip": (int)(self._vertical_flip),
                "horizontal_flip": (int)(self._horizontal_flip),
                "led_on": (int)(self._flash_led)}
    
        client.publish(f"esp32/{self._id}/config", str(data))

        
