# coding: utf-8

from time import time, sleep
from typing import Tuple, Any
from .camera import Camera
from .._global import OptionalModule
import numpy as np
from threading import Thread, RLock

try:
  import cv2
except (ModuleNotFoundError, ImportError):
  cv2 = OptionalModule("opencv-python")

try:
  from picamera import PiCamera
  from picamera.array import PiRGBArray
except (ModuleNotFoundError, ImportError, OSError):
  PiCamera = OptionalModule("picamera")

picamera_iso = [0, 100, 200, 320, 400, 500, 640, 800]


class Picamera(Camera):
  """Class for reading images from a PiCamera.

  The Picamera Camera block is meant for reading images from a Picamera.
  It uses the :mod:`picamera` module for capturing images, and :mod:`cv2` for
  converting bgr images to black and white.

  Warning:
    Only works on Raspberry Pi, with the picamera API. On the latest OS release
    "Bullseye", it has to be specifically activated in the configuration menu.
  """

  def __init__(self) -> None:
    """Instantiates the available settings."""

    super().__init__()
    self._cam = PiCamera()

    # Settings definition
    self.add_scale_setting('Width', 1, 3280, self._get_width,
                           self._set_width, 1280)
    self.add_scale_setting('Height', 1, 2464, self._get_height,
                           self._set_height, 720)
    self.add_scale_setting('Iso (0 for auto)', 0, 800, self._get_iso,
                           self._set_iso, 0)
    self.add_scale_setting('Brightness', 0, 100, self._get_brightness,
                           self._set_brightness, 50)
    self.add_scale_setting('Contrast', -100, 100, self._get_contrast,
                           self._set_contrast, 0)
    self.add_scale_setting('Saturation', -100, 100, self._get_saturation,
                           self._set_saturation, 0)
    self.add_scale_setting('Shutter speed (0 for auto)', 0, 30,
                           self._get_shutter_speed, self._set_shutter_speed, 0)
    self.add_bool_setting('Black_and_white', self._get_black_white,
                          self._set_black_white, True)
    self.add_scale_setting('Crop: X offset', 0.0, 1.0, self._get_crop_x_offset,
                           self._set_crop_x_offset, 0.0)
    self.add_scale_setting('Crop: Y offset', 0.0, 1.0, self._get_crop_y_offset,
                           self._set_crop_y_offset, 0.0)
    self.add_scale_setting('Crop: width', 0.0, 1.0, self._get_crop_width,
                           self._set_crop_width, 1.0)
    self.add_scale_setting('Crop: height', 0.0, 1.0, self._get_crop_height,
                           self._set_crop_height, 1.0)

    self._frame_grabber = Thread(target=self._grab_frame)
    self._lock = RLock()
    self._frame = None
    self._stop = False
    self._started = False

  def open(self, **kwargs: Any) -> None:
    """Sets the settings to their default values and starts the thread."""

    self.set_all(**kwargs)

    # Starting the video stream
    self._capture = PiRGBArray(self._cam, (self._get_width(),
                                           self._get_height()))
    self._stream = self._cam.capture_continuous(self._capture, format='bgr',
                                                use_video_port=True)

    self._frame_grabber.start()
    sleep(1)
    self._started = True

  def _stop_stream(self) -> None:
    """Stops the video stream before changing the image size."""

    if not self._started:
      return

    self._stop = True
    self._frame_grabber.join()
    self._capture.close()

  def _restart_stream(self) -> None:
    """Restarts the video stream after a change in the image size."""

    if not self._started:
      return

    self._stop = False
    self._frame_grabber = Thread(target=self._grab_frame)
    self._capture = PiRGBArray(self._cam, (self._get_width(),
                                           self._get_height()))
    self._stream = self._cam.capture_continuous(self._capture, format='bgr',
                                                use_video_port=True)

    self._frame_grabber.start()
    sleep(1)

  def get_image(self) -> Tuple[float, np.ndarray]:
    """Simply returns the last image in the buffer.

    The captured image is in bgr format, and converted into black and white if
    needed.

    Returns:
      The timeframe and the image.
    """

    t = time()
    with self._lock:
      output = self._frame
    if self.Black_and_white:
      output = cv2.cvtColor(output, cv2.COLOR_BGR2GRAY)
    return t, output

  def _grab_frame(self) -> None:
    """Thread for grabbing the last image in the video stream and putting it in
    the buffer."""

    for frame in self._stream:
      with self._lock:
        self._frame = frame.array
      self._capture.truncate(0)
      if self._stop:
        break

  def close(self) -> None:
    """Joins the thread and closes the stream and the
    :class:`picamera.PiCamera` object."""

    self._stop = True
    self._frame_grabber.join()
    self._capture.close()
    self._cam.close()

  def _get_width(self) -> int:
    return self._cam.resolution[0]

  def _get_height(self) -> int:
    return self._cam.resolution[1]

  def _get_iso(self) -> int:
    return self._cam.iso

  def _get_brightness(self) -> int:
    return self._cam.brightness

  def _get_contrast(self) -> int:
    return self._cam.contrast

  def _get_saturation(self) -> int:
    return self._cam.saturation

  def _get_shutter_speed(self) -> float:
    return self._cam.shutter_speed

  def _get_black_white(self) -> bool:
    return self._cam.color_effects == (128, 128)

  def _get_crop_x_offset(self) -> float:
    return self._cam.zoom[0]

  def _get_crop_y_offset(self) -> float:
    return self._cam.zoom[1]

  def _get_crop_width(self) -> float:
    return self._cam.zoom[2]

  def _get_crop_height(self) -> float:
    return self._cam.zoom[3]

  def _set_width(self, width: float) -> None:
    # The Picamera only accepts width that are multiples of 32
    self._stop_stream()
    self._cam.resolution = (32 * (width // 32), self._get_height())
    self._restart_stream()

  def _set_height(self, height: float) -> None:
    # The Picamera only accepts heights that are multiples of 32
    self._stop_stream()
    self._cam.resolution = (self._get_width(), 32 * (height // 32))
    self._restart_stream()

  def _set_iso(self, iso: float) -> None:
    # The Picamera only accepts a limited range of iso values
    self._cam.iso = min(picamera_iso, key=lambda x: abs(x - iso))

  def _set_brightness(self, brightness: float) -> None:
    self._cam.brightness = brightness

  def _set_contrast(self, contrast: float) -> None:
    self._cam.contrast = contrast

  def _set_saturation(self, saturation: float) -> None:
    self._cam.saturation = saturation

  def _set_shutter_speed(self, shutter_speed: float) -> None:
    self._cam.shutter_speed = shutter_speed

  def _set_black_white(self, boolean: bool) -> None:
    if boolean:
      self._cam.color_effects = (128, 128)
    else:
      self._cam.color_effects = None

  def _set_crop_x_offset(self, x_offset: float) -> None:
    self._cam.zoom = (x_offset, self._get_crop_y_offset(),
                      self._get_crop_width(), self._get_crop_height())

  def _set_crop_y_offset(self, y_offset: float) -> None:
    self._cam.zoom = (self._get_crop_x_offset(), y_offset,
                      self._get_crop_width(), self._get_crop_height())

  def _set_crop_width(self, width: float) -> None:
    self._cam.zoom = (self._get_crop_x_offset(), self._get_crop_y_offset(),
                      width, self._get_crop_height())

  def _set_crop_height(self, height: float) -> None:
    self._cam.zoom = (self._get_crop_x_offset(), self._get_crop_y_offset(),
                      self._get_crop_width(), height)
