import logging
import math
from threading import Event
from time import sleep

import cv2
import numpy as np
from PySide6.QtCore import QThread, Signal, Slot
from PySide6.QtGui import QImage, QPixmap
from PySide6.QtWidgets import (
    QGridLayout,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QVBoxLayout,
    QWidget,
)

from caliscope.cameras.synchronizer import Synchronizer
from caliscope.controller import Controller
from caliscope.packets import FramePacket

logger = logging.getLogger(__name__)


class ExtrinsicPlaybackWidget(QWidget):
    def __init__(self, controller: Controller):
        super(ExtrinsicPlaybackWidget, self).__init__()
        self.controller = controller
        self.synchronizer: Synchronizer = self.controller.synchronizer
        self.ports = self.synchronizer.ports

        # need to let synchronizer spin up before able to display frames
        while not hasattr(controller.synchronizer, "current_sync_packet"):
            sleep(0.25)
        # create tools to build and emit the displayed frame
        # self.unpaired_frame_builder = FramePrepper(self.synchronizer)
        self.thumbnail_emitter = FrameDictionaryEmitter(self.synchronizer)
        self.thumbnail_emitter.start()

        # all video output routed to qlabels stored in a dictionariy
        # make it as square as you can get it
        self.playback_displays = {str(port): QLabel() for port in self.ports}
        # self.recording_frame_display = QLabel()

        self.place_widgets()
        self.connect_widgets()

    def place_widgets(self):
        self.setLayout(QVBoxLayout())
        self.settings_group = QGroupBox("Settings")
        self.settings_group.setLayout(QHBoxLayout())
        self.settings_group.layout().addWidget(QLabel("Frame Rate:"))
        self.settings_group.layout().addWidget(self.frame_rate_spin)
        self.layout().addWidget(self.settings_group)

        self.record_controls = QGroupBox()
        self.record_controls.setLayout(QHBoxLayout())
        self.record_controls.layout().addWidget(self.start_stop)
        self.record_controls.layout().addWidget(self.destination_label)
        self.record_controls.layout().addWidget(self.recording_directory)

        self.layout().addWidget(self.record_controls)

        dropped_fps_layout = QHBoxLayout()
        dropped_fps_layout.addStretch(1)
        dropped_fps_layout.addWidget(self.dropped_fps_label)
        dropped_fps_layout.addStretch(1)

        self.layout().addLayout(dropped_fps_layout)

        camera_count = len(self.ports)
        grid_columns = int(math.ceil(camera_count**0.5))
        # grid_rows = int(math.ceil(camera_count / grid_columns))

        frame_grid = QGridLayout()
        row = 0
        column = 0
        for port in sorted(self.ports):
            frame_grid.addWidget(self.playback_displays[str(port)], row, column)

            # update row and column for next iteration
            if column >= grid_columns - 1:
                # start fresh on next row
                column = 0
                row += 1
            else:
                column += 1

        frame_display_layout = QHBoxLayout()
        frame_display_layout.addStretch(1)
        frame_display_layout.addLayout(frame_grid)
        frame_display_layout.addStretch(1)
        self.layout().addLayout(frame_display_layout)

    def connect_widgets(self):
        self.thumbnail_emitter.ThumbnailImagesBroadcast.connect(self.ImageUpdateSlot)
        self.frame_rate_spin.valueChanged.connect(self.controller.set_active_mode_fps)
        self.thumbnail_emitter.dropped_fps.connect(self.update_dropped_fps)
        self.start_stop.clicked.connect(self.toggle_start_stop)
        self.controller.qt_signaler.recording_complete_signal.connect(self.on_recording_complete)

    @Slot(dict)
    def ImageUpdateSlot(self, q_image_dict: dict):
        logger.debug("About to get qpixmap from qimage")
        for port, thumbnail in q_image_dict.items():
            qpixmap = QPixmap.fromImage(thumbnail)
            logger.debug("About to set qpixmap to display")
            self.playback_displays[port].setPixmap(qpixmap)
            logger.debug("successfully set display")


class FrameDictionaryEmitter(QThread):
    ThumbnailImagesBroadcast = Signal(dict)
    dropped_fps = Signal(dict)

    def __init__(self, synchronizer: Synchronizer, single_frame_height=300):
        super(FrameDictionaryEmitter, self).__init__()
        self.single_frame_height = single_frame_height
        self.synchronizer = synchronizer
        logger.info("Initiated recording frame emitter")
        self.keep_collecting = Event()

    def run(self):
        self.keep_collecting.set()

        while self.keep_collecting.is_set():
            logger.debug("About to get next recording frame")
            # recording_frame = self.unpaired_frame_builder.get_recording_frame()

            logger.debug("Referencing current sync packet in synchronizer")
            self.current_sync_packet = self.synchronizer.current_sync_packet

            thumbnail_qimage = {}
            for port in self.synchronizer.ports:
                frame_packet = self.current_sync_packet.frame_packets[port]
                rotation_count = self.synchronizer.streams[port].camera.rotation_count

                text_frame = frame_packet_2_thumbnail(frame_packet, rotation_count, self.single_frame_height, port)
                q_image = cv2_to_qimage(text_frame)
                thumbnail_qimage[str(port)] = q_image

            self.ThumbnailImagesBroadcast.emit(thumbnail_qimage)

            dropped_fps_dict = {str(port): dropped for port, dropped in self.synchronizer.dropped_fps.items()}
            self.dropped_fps.emit(dropped_fps_dict)
        logger.info("Recording thumbnail emitter run thread ended...")


def frame_packet_2_thumbnail(frame_packet: FramePacket, rotation_count: int, edge_length: int, port: int):
    raw_frame = get_frame_or_blank(frame_packet, edge_length)
    # port = frame_packet.port

    # raw_frame = self.get_frame_or_blank(None)
    square_frame = resize_to_square(raw_frame, edge_length)
    rotated_frame = apply_rotation(square_frame, rotation_count)
    flipped_frame = cv2.flip(rotated_frame, 1)

    # put the port number on the top of the frame
    text_frame = cv2.putText(
        flipped_frame,
        str(port),
        (int(flipped_frame.shape[1] / 2), int(edge_length / 4)),
        fontFace=cv2.FONT_HERSHEY_SIMPLEX,
        fontScale=1,
        color=(0, 0, 255),
        thickness=2,
    )

    return text_frame


def get_frame_or_blank(frame_packet: FramePacket, edge_length):
    """Synchronization issues can lead to some frames being None among
    the synched frames, so plug that with a blank frame"""

    if frame_packet is None:
        logger.debug("plugging blank frame data")
        frame = np.zeros((edge_length, edge_length, 3), dtype=np.uint8)
    else:
        frame = frame_packet.frame_with_points

    return frame


def resize_to_square(frame, edge_length):
    """To make sure that frames align well, scale them all to thumbnails
    squares with black borders."""
    logger.debug("resizing square")

    height = frame.shape[0]
    width = frame.shape[1]

    padded_size = max(height, width)

    height_pad = int((padded_size - height) / 2)
    width_pad = int((padded_size - width) / 2)
    pad_color = [0, 0, 0]

    logger.debug("about to pad border")
    frame = cv2.copyMakeBorder(
        frame,
        height_pad,
        height_pad,
        width_pad,
        width_pad,
        cv2.BORDER_CONSTANT,
        value=pad_color,
    )

    frame = resize(frame, new_height=edge_length)
    return frame


def resize(image, new_height):
    (current_height, current_width) = image.shape[:2]
    ratio = new_height / float(current_height)
    dim = (int(current_width * ratio), new_height)
    resized = cv2.resize(image, dim, interpolation=cv2.INTER_AREA)
    return resized


def apply_rotation(frame, rotation_count):
    if rotation_count == 0:
        pass
    elif rotation_count in [1, -3]:
        frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
    elif rotation_count in [2, -2]:
        frame = cv2.rotate(frame, cv2.ROTATE_180)
    elif rotation_count in [-1, 3]:
        frame = cv2.rotate(frame, cv2.ROTATE_90_COUNTERCLOCKWISE)

    return frame


def prep_img_for_qpixmap(image: np.ndarray):
    """
    qpixmap needs dimensions divisible by 4 and without that weird things happen.
    """
    if image.shape[1] % 4 != 0:  # If the width of the row isn't divisible by 4
        padding_width = 4 - (image.shape[1] % 4)  # Calculate how much padding is needed
        padding = np.zeros(
            (image.shape[0], padding_width, image.shape[2]), dtype=image.dtype
        )  # Create a black image of the required size
        image = np.hstack([image, padding])  # Add the padding to the right of the image

    return image


def cv2_to_qimage(frame):
    image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

    qt_frame = QImage(
        image.data,
        image.shape[1],
        image.shape[0],
        QImage.Format.Format_RGB888,
    )

    return qt_frame
