import logging
import os
import time
from typing import Dict, List, Tuple

import cv2
import mediapipe as mp
import numpy as np
import pandas as pd


class KeypointExtractor:
    def __init__(self, video_folder: str, cache_folder: str = "cache"):
        self.mp_drawing = mp.solutions.drawing_utils
        self.mp_holistic = mp.solutions.holistic
        self.video_folder = video_folder
        self.cache_folder = cache_folder

        # we will store the keypoints of each frame as a row in the dataframe. The columns are the keypoints: Pose (33), Left Hand (21), Right Hand (21). Each keypoint has 3 values: x, y
        self.columns = [f"{i}_{j}"  for i in range(33+21*2) for j in ["x", "y"]]

        # holistic extractor
        self.holistic = mp.solutions.holistic.Holistic(
            min_detection_confidence=0.5,
            min_tracking_confidence=0.5,
        )

    def extract_keypoints_from_video(self,
                                 video: str,
                                 normalize: bool = False,
                                 draw: bool = False,
                                ) -> pd.DataFrame:
        """extract_keypoints_from_video this function extracts keypoints from a video and stores them in a dataframe

        :param video: the video to extract keypoints from
        :type video: str
        :return: dataframe with keypoints
        :rtype: pd.DataFrame
        """

        if not draw:
            # check if video exists
            if not os.path.exists(self.video_folder + video):
                logging.error("Video does not exist at path: " + self.video_folder + video)
                return None

            # check if cache exists
            if not os.path.exists(self.cache_folder):
                os.makedirs(self.cache_folder)

            # check if cache file exists and return
            if os.path.exists(self.cache_folder + "/" + video + ".npy"):
                # create dataframe from cache
                df = pd.DataFrame(np.load(self.cache_folder + "/" + video + ".npy", allow_pickle=True), columns=self.columns)
                if normalize:
                    df = self.normalize_hands(df)
                return df

        # open video
        cap = cv2.VideoCapture(self.video_folder + video)

        keypoints_df = pd.DataFrame(columns=self.columns)

        # extract frames from video so we extract 5 frames per second
        frame_rate = int(cap.get(cv2.CAP_PROP_FPS))
        frame_skip = frame_rate // 10

        output_frames = []

        while cap.isOpened():

            # skip frames
            for _ in range(frame_skip):
                success, image = cap.read()
                if not success:
                    break

            success, image = cap.read()
            if not success:
                break
            # extract keypoints of frame
            if draw:
                results, draw_image = self.extract_keypoints_from_frame(image, draw=True)
                output_frames.append(draw_image)
            else:
                results = self.extract_keypoints_from_frame(image)

            def extract_keypoints(landmarks):
                if landmarks:
                    return [i for landmark in landmarks.landmark for i in [landmark.x, landmark.y]]

            # store keypoints in dataframe
            k1 = extract_keypoints(results.pose_landmarks)
            k2 = extract_keypoints(results.left_hand_landmarks)
            k3 = extract_keypoints(results.right_hand_landmarks)
            if k1 and (k2 or k3):
                data = [k1 + (k2 or [0] * 42) + (k3 or [0] * 42)]
                new_df = pd.DataFrame(data, columns=self.columns)
                keypoints_df = pd.concat([keypoints_df, new_df], ignore_index=True)
                
        # close video
        cap.release()

        # save keypoints to cache
        np.save(self.cache_folder + "/" + video + ".npy", keypoints_df.to_numpy())

        if normalize:
            keypoints_df = self.normalize_hands(keypoints_df)

        if draw:
            return keypoints_df, output_frames
    
        return keypoints_df


    def extract_keypoints_from_frame(self, image: np.ndarray, draw: bool = False):
        """extract_keypoints_from_frame this function extracts keypoints from a frame and draws them on the frame if draw is set to True

        :param image: the frame to extract keypoints from
        :type image: np.ndarray
        :param draw: indicates if frame with keypoints on must be returned, defaults to False
        :type draw: bool, optional
        :return: the keypoints and the frame with keypoints on if draw is set to True
        :rtype: np.ndarray
        """
        # Convert the BGR image to RGB and process it with MediaPipe Pose.
        results = self.holistic.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))

        if draw:
            # Draw the pose annotations on the image
            draw_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
            # self.mp_drawing.draw_landmarks(draw_image, results.face_landmarks, self.mp_holistic.FACEMESH_CONTOURS)
            self.mp_drawing.draw_landmarks(draw_image, results.left_hand_landmarks, self.mp_holistic.HAND_CONNECTIONS)
            self.mp_drawing.draw_landmarks(draw_image, results.right_hand_landmarks, self.mp_holistic.HAND_CONNECTIONS)
            
            # create bounding box around hands
            if results.left_hand_landmarks:
                x = [landmark.x for landmark in results.left_hand_landmarks.landmark]
                y = [landmark.y for landmark in results.left_hand_landmarks.landmark]
                draw_image = cv2.rectangle(draw_image, (int(min(x) * 640), int(min(y) * 480)), (int(max(x) * 640), int(max(y) * 480)), (255, 0, 0), 2)

            if results.right_hand_landmarks:
                x = [landmark.x for landmark in results.right_hand_landmarks.landmark]
                y = [landmark.y for landmark in results.right_hand_landmarks.landmark]
                draw_image = cv2.rectangle(draw_image, (int(min(x) * 640), int(min(y) * 480)), (int(max(x) * 640), int(max(y) * 480)), (255, 0, 0), 2)
            
            self.mp_drawing.draw_landmarks(draw_image, results.pose_landmarks, self.mp_holistic.POSE_CONNECTIONS)

            return results, draw_image
        
        return results


    def normalize_hands(self, dataframe: pd.DataFrame) -> pd.DataFrame:
        """normalize_hand this function normalizes the hand keypoints of a dataframe

        :param dataframe: the dataframe to normalize
        :type dataframe: pd.DataFrame
        :return: the normalized dataframe
        :rtype: pd.DataFrame
        """
        
        # normalize left hand
        dataframe = self.normalize_hand_helper(dataframe, "left_hand")

        # normalize right hand
        dataframe = self.normalize_hand_helper(dataframe, "right_hand")

        return dataframe
    
    def normalize_hand_helper(self, dataframe: pd.DataFrame, hand: str) -> pd.DataFrame:
        """normalize_hand_helper this function normalizes the hand keypoints of a dataframe

        :param dataframe: the dataframe to normalize
        :type dataframe: pd.DataFrame
        :param hand: the hand to normalize
        :type hand: str
        :return: the normalized dataframe
        :rtype: pd.DataFrame
        """
        # get all columns that belong to the hand (left hand column 66 - 107, right hand column 108 - 149)
        hand_columns = np.array([i for i in range(66 + (42 if hand == "right_hand" else 0), 108 + (42 if hand == "right_hand" else 0))])
        
        # get the x, y coordinates of the hand keypoints
        hand_coords = dataframe.iloc[:, hand_columns].values.reshape(-1, 21, 2)
        
        # get the min and max x, y coordinates of the hand keypoints
        min_x, min_y = np.min(hand_coords[:, :, 0], axis=1), np.min(hand_coords[:, :, 1], axis=1)
        max_x, max_y = np.max(hand_coords[:, :, 0], axis=1), np.max(hand_coords[:, :, 1], axis=1)
        
        # calculate the center of the hand keypoints
        center_x, center_y = (min_x + max_x) / 2, (min_y + max_y) / 2
        
        # calculate the width and height of the bounding box around the hand keypoints
        bbox_width, bbox_height = max_x - min_x, max_y - min_y
        
        # repeat the center coordinates and bounding box dimensions to match the shape of hand_coords
        center_coords = np.tile(np.array([center_x, center_y]), (21, 1)).reshape(-1, 21, 2)
        bbox_dims = np.tile(np.array([bbox_width, bbox_height]), (21, 1)).reshape(-1, 21, 2)
        
        if np.any(bbox_dims == 0):
            return dataframe
        # normalize the hand keypoints based on the bounding box around the hand
        norm_hand_coords = (hand_coords - center_coords) / bbox_dims
        
        # flatten the normalized hand keypoints array and replace the original hand keypoints with the normalized hand keypoints in the dataframe
        dataframe.iloc[:, hand_columns] = norm_hand_coords.reshape(-1, 42)
        
        return dataframe