import cv2
import mediapipe as mp
import numpy as np

# Load the correct and incorrect posture images as BGR colors
correct = cv2.imread('right.png')
correct = cv2.cvtColor(correct, cv2.COLOR_BGR2RGB)
incorrect = cv2.imread('wrong.png')
incorrect = cv2.cvtColor(incorrect, cv2.COLOR_BGR2RGB)

def draw_rounded_rect(img, rect_start, rect_end, corner_width, box_color):
        
        #    This function draws a rectangle with rounded corners on an image.
        # Args:
              #img: The image to draw on.
              #rect_start: The top-left corner of the rectangle as a tuple (x1, y1).
              #rect_end: The bottom-right corner of the rectangle as a tuple (x2, y2).
              #corner_width: The width of the rounded corners.
              #box_color: The color of the rectangle in BGR format.
    
    x1, y1 = rect_start
    x2, y2 = rect_end
    w = corner_width

    # Draw filled rectangles for each side of the box
    cv2.rectangle(img, (x1 + w, y1), (x2 - w, y1 + w), box_color, -1)
    cv2.rectangle(img, (x1 + w, y2 - w), (x2 - w, y2), box_color, -1)
    cv2.rectangle(img, (x1, y1 + w), (x1 + w, y2 - w), box_color, -1)
    cv2.rectangle(img, (x2 - w, y1 + w), (x2, y2 - w), box_color, -1)
    cv2.rectangle(img, (x1 + w, y1 + w), (x2 - w, y2 - w), box_color, -1)

    # Draw filled ellipses for the corners
    cv2.ellipse(img, (x1 + w, y1 + w), (w, w),
                angle = 0, startAngle = -90, endAngle = -180, color = box_color, thickness = -1)

    cv2.ellipse(img, (x2 - w, y1 + w), (w, w),
                angle = 0, startAngle = 0, endAngle = -90, color = box_color, thickness = -1)

    cv2.ellipse(img, (x1 + w, y2 - w), (w, w),
                angle = 0, startAngle = 90, endAngle = 180, color = box_color, thickness = -1)

    cv2.ellipse(img, (x2 - w, y2 - w), (w, w),
                angle = 0, startAngle = 0, endAngle = 90, color = box_color, thickness = -1)

    return img

def draw_dotted_line(frame, lm_coord, start, end, line_color):
        
          #This function draws a dotted line on a frame based on landmark coordinates.
    
        #Args:
          #frame: The image to draw on.
          #lm_coord: The landmark coordinates as a NumPy array.
          #start: The index of the starting landmark in the lm_coord array.
          #end: The index of the ending landmark in the lm_coord array.
          #line_color: The color of the line in BGR format.
        
    
    pix_step = 0

    # Draw circles at every 8th element between the start and end landmarks
    for i in range(start, end+1, 8):
        cv2.circle(frame, (lm_coord[0], i+pix_step), 2, line_color, -1, lineType=cv2.LINE_AA)

    return frame

def draw_text(
    img,
    msg,
    width = 7,
    font=cv2.FONT_HERSHEY_SIMPLEX,
    pos=(0, 0),
    font_scale=1,
    font_thickness=2,
    text_color=(0, 255, 0),
    text_color_bg=(0, 0, 0),
    box_offset=(20, 10),
    overlay_image = False,
    overlay_type = None
):

  #This function draws text with a customizable background box on an image.

  #Args:
      #img: The image to draw on.
      #msg: The message to display as a string.
      #width: The thickness of the background box border (default: 7).
      #font: The font style for the text (default: cv2.FONT_HERSHEY_SIMPLEX).
      #pos: The top-left corner coordinates of the text box (default: (0, 0)).
      #font_scale: The scaling factor for the font size (default: 1).
      #font_thickness: The thickness of the text (default: 2).
      #text_color: The color of the text in BGR format (default: green - (0, 255, 0)).
      #text_color_bg: The color of the background box in BGR format (default: black - (0, 0, 0)).
      #box_offset: The offset for the background box relative to the text (default: (20, 10)).
      #overlay_image: Flag to display an overlay image inside the box (default: False).
      #overlay_type: Type of overlay image ("correct" or "incorrect") - used when overlay_image is True.
  #Returns:
      #The size of the drawn text (width, height) as a NumPy array.


    offset = box_offset
    x, y = pos

    # Get the size of the text with the specified font and scale
    text_size, _ = cv2.getTextSize(msg, font, font_scale, font_thickness)
    text_w, text_h = text_size

    # Calculate the top-left and bottom-right corners of the text box with padding
    rec_start = tuple(p - o for p, o in zip(pos, offset))
    rec_end = tuple(m + n - o for m, n, o in zip((x + text_w, y + text_h), offset, (25, 0)))

    resize_height = 0


    # Handle overlay image logic
    if overlay_image:
        resize_height = rec_end[1] - rec_start[1]


        # Draw a rounded rectangle box with the background color
        img = draw_rounded_rect(img, rec_start, (rec_end[0]+resize_height, rec_end[1]), width, text_color_bg)
        
        # Resize the overlay image based on the box height
        if overlay_type == "correct":
            overlay_res = cv2.resize(correct, (resize_height, resize_height), interpolation = cv2.INTER_AREA)		
        elif overlay_type == "incorrect":
            overlay_res = cv2.resize(incorrect, (resize_height, resize_height), interpolation = cv2.INTER_AREA)

        # Overlay the resized image onto the background box
        img[rec_start[1]:rec_start[1]+resize_height, rec_start[0]+width:rec_start[0]+width+resize_height] = overlay_res

    else:
        img = draw_rounded_rect(img, rec_start, rec_end, width, text_color_bg)


    # Draw the text onto the image with specified parameters
    cv2.putText(
        img,
        msg,
        (int(rec_start[0]+resize_height + 8), int(y + text_h + font_scale - 1)), 
        font,
        font_scale,
        text_color,
        font_thickness,
        cv2.LINE_AA,
    )

    return text_size

def find_angle(p1, p2, ref_pt = np.array([0,0])):

  #This function calculates the angle between two points relative to a reference point.

  #Args:
      #p1: The first point coordinates as a NumPy array (x, y).
      #p2: The second point coordinates as a NumPy array (x, y).
      #ref_pt: The reference point coordinates as a NumPy array (default: [0, 0]).

  #Returns:
      #The angle between the two points in degrees (int).

    # Subtract the reference point from both points for normalization
    p1_ref = p1 - ref_pt
    p2_ref = p2 - ref_pt

    # Calculate the cosine of the angle using the dot product
    cos_theta = (np.dot(p1_ref,p2_ref)) / (1.0 * np.linalg.norm(p1_ref) * np.linalg.norm(p2_ref))
    
    # Clip the cosine value to avoid potential errors
    theta = np.arccos(np.clip(cos_theta, -1.0, 1.0))

    # Convert the angle from radians to degrees and cast to integer
    degree = int(180 / np.pi) * theta
    return int(degree)

def get_landmark_array(pose_landmark, key, frame_width, frame_height):

  #This function extracts the normalized image coordinates for a landmark.

  #Args:
      #pose_landmark: A MediaPipe pose landmark object.
      #key: The key name of the landmark to extract (e.g., 'nose', 'shoulder.x').
      #frame_width: The width of the image frame.
      #frame_height: The height of the image frame.

  #Returns:
      #A NumPy array containing the normalized x and y coordinates of the landmark.


    denorm_x = int(pose_landmark[key].x * frame_width)
    denorm_y = int(pose_landmark[key].y * frame_height)

    return np.array([denorm_x, denorm_y])

def get_landmark_features(kp_results, dict_features, feature, frame_width, frame_height):

  #This function extracts landmark coordinates for various body parts based on a feature name.

  #Args:
      #kp_results: The MediaPipe pose landmark results object.
      #dict_features: A dictionary containing landmark key names for different body parts.
      #feature: The name of the body part feature to extract (e.g., 'nose', 'left', 'right').
      #frame_width: The width of the image frame.
      #frame_height: The height of the image frame.

  #Returns:
      #A list containing the landmark coordinates (as NumPy arrays) or raises an error if the feature is invalid.
    
    if feature == 'nose':
        return get_landmark_array(kp_results, dict_features[feature], frame_width, frame_height)

    elif feature == 'left' or 'right':
        shldr_coord = get_landmark_array(kp_results, dict_features[feature]['shoulder'], frame_width, frame_height)
        elbow_coord   = get_landmark_array(kp_results, dict_features[feature]['elbow'], frame_width, frame_height)
        wrist_coord   = get_landmark_array(kp_results, dict_features[feature]['wrist'], frame_width, frame_height)
        hip_coord   = get_landmark_array(kp_results, dict_features[feature]['hip'], frame_width, frame_height)
        knee_coord   = get_landmark_array(kp_results, dict_features[feature]['knee'], frame_width, frame_height)
        ankle_coord   = get_landmark_array(kp_results, dict_features[feature]['ankle'], frame_width, frame_height)
        foot_coord   = get_landmark_array(kp_results, dict_features[feature]['foot'], frame_width, frame_height)

        return shldr_coord, elbow_coord, wrist_coord, hip_coord, knee_coord, ankle_coord, foot_coord
    
    else:
       raise ValueError("feature needs to be either 'nose', 'left' or 'right")

def get_mediapipe_pose(
                        static_image_mode = False, 
                        model_complexity = 1,
                        smooth_landmarks = True,
                        min_detection_confidence = 0.5,
                        min_tracking_confidence = 0.5

                      ):
    pose = mp.solutions.pose.Pose(
                                    static_image_mode = static_image_mode,
                                    model_complexity = model_complexity,
                                    smooth_landmarks = smooth_landmarks,
                                    min_detection_confidence = min_detection_confidence,
                                    min_tracking_confidence = min_tracking_confidence
                                 )
    return pose