"""Primary interface for analyzing the XROMM data using trained network"""

import os
import math
import logging
from pathlib import Path

from itertools import combinations
import cv2
import deeplabcut
import imagehash
import pandas as pd
from PIL import Image
import yaml

from .xma_data_processor import XMADataProcessor

logger = logging.getLogger(__name__)
logging.basicConfig(
    filename="analyzer.log",
    encoding="utf-8",
    level=os.environ.get("DEEPXROMM_LOG_LEVEL", "INFO").upper(),
)


class Analyzer:
    """Analyzes XROMM videos using trained network."""

    def __init__(self, config):
        self.working_dir = Path(config["working_dir"])
        self.project_config = self.working_dir / "project_config.yaml"
        self._trials_path = self.working_dir / "trials"
        self._data_processor = XMADataProcessor(config)
        self._config = config
        self._dlc_config = config["path_config_file"]

    def analyze_videos(self):
        """Analyze videos with a pre-existing network"""
        trials = self._data_processor.list_trials()

        # Establish project vars
        with open(self._dlc_config) as dlc_config:
            dlc = yaml.safe_load(dlc_config)
        iteration = dlc["iteration"]

        mode = self._config["mode"]
        if mode in ["2D", "per_cam"]:
            self._analyze_xromm_videos(iteration)

        elif mode == "rgb":
            self._data_processor.make_rgb_videos("trials")
            for trial_path in trials:
                trial = trial_path.name
                current_files = trial_path.glob("*")
                logger.debug(f"Current files in directory {current_files}")
                video_path = trial_path / f"{trial}_rgb.avi"
                destfolder = trial_path / f"it{iteration}"
                deeplabcut.analyze_videos(
                    self._dlc_config,
                    str(
                        video_path
                    ),  # DLC relies on .endswith to determine suffix, so this needs to be a string
                    destfolder=destfolder,
                    save_as_csv=True,
                )
        else:
            raise ValueError(f"Unsupported mode: {mode}")

    def _analyze_xromm_videos(self, iteration: int) -> None:
        """Analyze all novel videos in the 'trials' folder of a deepxromm project"""
        # assumes you have cam1 and cam2 videos as .avi in their own seperate trial folders
        # assumes all folders w/i new_data_path are trial folders

        # analyze videos
        cameras = [1, 2]
        trials = self._data_processor.list_trials()
        mode = self._config["mode"]

        for trialpath in trials:
            savepath = trialpath / f"it{iteration}"
            if savepath.exists():
                temp = savepath.glob("*Predicted2DPoints.csv")
                if temp:
                    print(
                        f"There are already predicted points in iteration {iteration} subfolders... skipping point prediction"
                    )
                    return
            else:
                savepath.mkdir(parents=True, exist_ok=True)  # make new folder
            # get video file
            for camera in cameras:
                # Error handling handled by find_cam_file helper
                video = self._data_processor.find_cam_file(trialpath, f"cam{camera}")
                # analyze video
                if mode == "2D":
                    deeplabcut.analyze_videos(
                        self._config["path_config_file"],
                        [
                            str(video)
                        ],  # DLC uses endswith filtering for suffixes for some reason
                        destfolder=savepath,
                        save_as_csv=True,
                    )
                else:
                    configs = [
                        self._config["path_config_file"],
                        self._config["path_config_file_2"],
                    ]
                    deeplabcut.analyze_videos(
                        configs[camera - 1],
                        [
                            str(video)
                        ],  # DLC uses endswith filtering for suffixes for some reason
                        destfolder=savepath,
                        save_as_csv=True,
                    )

    def analyze_video_similarity_project(self):
        """Analyze all videos in a project and take their average similar. This is dangerous, as it will assume that all cam1/cam2 pairs match
        or don't match!"""
        similarity_score = {}
        trial_combos = combinations(self._data_processor.list_trials(), 2)
        for trial1, trial2 in trial_combos:
            self._config["trial_1_name"] = trial1.stem
            self._config["trial_2_name"] = trial2.stem
            with self.project_config.open("w") as file:
                yaml.dump(self._config, file, sort_keys=False)
            # TODO: pass the trial names directly instead of writing into config
            similarity_score[(trial1, trial2)] = self.analyze_video_similarity_trial()
        return similarity_score

    def analyze_video_similarity_trial(self):
        """Analyze the average similarity between trials using image hashing"""
        trials = [self._config["trial_1_name"], self._config["trial_2_name"]]
        cameras = ["cam1", "cam2"]
        videos = {
            (trial, cam): cv2.VideoCapture(
                self._data_processor.find_cam_file(self._trials_path / trial, cam)
            )
            for trial in trials
            for cam in cameras
        }

        # Compare hashes based on the camera views configuration
        if self._config["cam1s_are_the_same_view"]:
            cam1_diff, noc1 = self._compare_two_videos(
                videos[(trials[0], "cam1")], videos[(trials[1], "cam1")]
            )
            cam2_diff, noc2 = self._compare_two_videos(
                videos[(trials[0], "cam2")], videos[(trials[1], "cam2")]
            )
        else:
            cam1_diff, noc1 = self._compare_two_videos(
                videos[(trials[0], "cam1")], videos[(trials[1], "cam2")]
            )
            cam2_diff, noc2 = self._compare_two_videos(
                videos[(trials[0], "cam2")], videos[(trials[1], "cam1")]
            )

        # Calculate the average difference for each camera view
        cam1_avg_diff = cam1_diff / noc1 if noc1 > 0 else 0
        cam2_avg_diff = cam2_diff / noc2 if noc2 > 0 else 0

        # Calculate the overall trial average difference
        trial_avg_diff = (cam1_avg_diff + cam2_avg_diff) / 2

        # Note: The number of comparisons (noc) grows with video size, which could potentially affect the similarity measure,
        # making larger videos appear more similar than they actually are. This aspect may need further consideration.
        return trial_avg_diff

    def analyze_marker_similarity_project(self):
        """Analyze all videos in a project and get their average rhythmicity. This assumes that all cam1/2 pairs are either the same or different!"""
        marker_similarity = {}

        trial_perms = combinations(self._data_processor.list_trials(), 2)
        logger.debug(f"Trial permutations for project: {trial_perms}")
        for trial1, trial2 in trial_perms:
            self._config["trial_1_name"] = trial1.stem
            self._config["trial_2_name"] = trial2.stem
            with self.project_config.open("w") as file:
                yaml.dump(self._config, file, sort_keys=False)
            # TODO: pass the trial names directly instead of writing into config
            marker_similarity[(trial1, trial2)] = abs(
                self.analyze_marker_similarity_trial()
            )
        return marker_similarity

    def analyze_marker_similarity_trial(self):
        """Analyze marker similarity for a pair of trials using the distance formula."""
        # Find CSVs for each trial
        trial1 = self._config["trial_1_name"]
        trial2 = self._config["trial_2_name"]
        trial1_path = self._trials_path / trial1
        current_files = trial1_path.glob("*")
        logger.debug(f"Current files in directory {trial1_path}: {current_files}")
        trial2_path = self._trials_path / trial2
        current_files = trial2_path.glob("*")
        logger.debug(f"Current files in directory {trial2_path}: {current_files}")

        # Get a list of markers that each trial have in common
        bodyparts1_csv_path = self._data_processor.find_trial_csv(trial1_path)
        bodyparts2_csv_path = self._data_processor.find_trial_csv(trial2_path)
        bodyparts1 = self._data_processor.get_bodyparts_from_xma(
            bodyparts1_csv_path, mode="rgb"
        )
        bodyparts2 = self._data_processor.get_bodyparts_from_xma(
            bodyparts2_csv_path, mode="rgb"
        )
        markers_in_common = [marker for marker in bodyparts1 if marker in bodyparts2]
        logger.debug(f"Markers in common for similarity analysis: {markers_in_common}")

        # Analyze intermarker distances for each marker in common
        trial1_csv = pd.read_csv(bodyparts1_csv_path)
        trial2_csv = pd.read_csv(bodyparts2_csv_path)
        avg_distances = []
        for marker in markers_in_common:
            # Debug logging
            x1_vals = trial1_csv[f"{marker}_X"]
            logger.debug(f"Trial1 values for {marker}_X: {x1_vals}")
            x2_vals = trial2_csv[f"{marker}_X"]
            logger.debug(f"Trial2 values for {marker}_X: {x2_vals}")

            # Get mean positions for each marker
            avg_x1, avg_y1 = (
                trial1_csv[f"{marker}_X"].mean(),
                trial1_csv[f"{marker}_Y"].mean(),
            )
            logger.debug(
                f"Average trial1 position for marker {marker}: ({avg_x1}, {avg_y1})"
            )
            avg_x2, avg_y2 = (
                trial2_csv[f"{marker}_X"].mean(),
                trial2_csv[f"{marker}_Y"].mean(),
            )
            logger.debug(
                f"Average trial2 position for marker {marker}: ({avg_x2}, {avg_y2})"
            )

            # Calculate the distance between the average positions for this marker in the two trials
            distance = math.sqrt((avg_x2 - avg_x1) ** 2 + (avg_y2 - avg_y1) ** 2)
            avg_distances.append(distance)

        logger.debug(f"Avg distances for trials {trial1} and {trial2}: {avg_distances}")
        # Calculate the mean of the distances to get an overall similarity measure
        marker_similarity = (
            sum(avg_distances) / len(avg_distances) if avg_distances else 0
        )

        return marker_similarity

    def get_max_dissimilarity_for_trial(self, trial_path, window):
        """Calculate the dissimilarity within the trial given the frame sliding window."""
        video1 = cv2.VideoCapture(
            self._data_processor.find_cam_file(trial_path, "cam1")
        )
        video2 = cv2.VideoCapture(
            self._data_processor.find_cam_file(trial_path, "cam2")
        )

        hashes1 = self._hash_trial_video(video1)
        hashes2 = self._hash_trial_video(video2)

        return self._find_dissimilar_regions(hashes1, hashes2, window)

    def _compare_two_videos(self, video1, video2):
        """Compare two videos using image hashing"""
        hashes1 = self._hash_trial_video(video1)
        hashes2 = self._hash_trial_video(video2)

        video1_frames = len(hashes1)
        video2_frames = len(hashes2)
        # Thought: maybe this should be computed alongside the for loop?
        noc = math.perm(
            video1_frames + video2_frames, 2
        )  # Might need revision based on actual comparison logic

        print(f"Video 1 frames: {video1_frames}")
        print(f"Video 2 frames: {video2_frames}")

        print("Comparing hashes between videos")
        hash_dif = sum(hash1 - hash2 for hash1 in hashes1 for hash2 in hashes2)

        return hash_dif, noc

    def _hash_trial_video(self, video):
        """Generate image hashes for a single video"""
        video_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
        print(f"Video frames: {video_frames}")

        hashes = []
        print("Creating hashes for video")
        for i in range(video_frames):
            print(f"Current frame: {i}")
            ret, frame = video.read()
            if not ret:
                # Should this throw an error?
                cv2.destroyAllWindows()
                raise IOError("Error reading video frame")
            hashes.append(imagehash.phash(Image.fromarray(frame)))

        return hashes

    def _find_dissimilar_regions(self, hashes1, hashes2, window):
        """Find the region of maximum dissimilarity given 2 lists of hashes and a sliding window (how many frames)"""
        start_frame_vid1 = 0
        start_frame_vid2 = 0
        max_hash_dif_vid1 = 0
        max_hash_dif_vid2 = 0
        hash_dif_vid1 = 0
        hash_dif_vid2 = 0

        for slider in range(0, len(hashes1) // window):
            print(f"Current start frame {slider * window}")
            hash_dif_vid1, hash_dif_vid2 = self._compare_hash_sets(
                hashes1[slider * window : (slider + 1) * window],
                hashes2[slider * window : (slider + 1) * window],
            )

            print(f"Current hash diff (vid 1): {hash_dif_vid1}")
            print(f"Current hash diff (vid 2): {hash_dif_vid2}")
            if hash_dif_vid1 > max_hash_dif_vid1:
                max_hash_dif_vid1 = hash_dif_vid1
                start_frame_vid1 = slider * window

            if hash_dif_vid2 > max_hash_dif_vid2:
                max_hash_dif_vid2 = hash_dif_vid2
                start_frame_vid2 = slider * window

            print(f"Max hash diff (vid 1): {max_hash_dif_vid1}")
            print(f"Max hash diff (vid 2): {max_hash_dif_vid2}")

            print(f"Start frame (vid 1): {start_frame_vid1}")
            print(f"Start frame (vid 2): {start_frame_vid2}")

        return start_frame_vid1, start_frame_vid2

    def _compare_hash_sets(self, hashes1, hashes2):
        """Compares two sets of image hashes to find dissimilarities"""
        hash1_dif = 0
        hash2_dif = 0

        print(f"Hash set 1 {hashes1[0]}")
        print(f"Hash set 2 {hashes2[0]}")
        # Compares all possible combinations of images
        for combination in combinations(hashes1, 2):
            hash1_dif = hash1_dif + (combination[0] - combination[1])

        for combination in combinations(hashes2, 2):
            hash2_dif = hash2_dif + (combination[0] - combination[1])

        return hash1_dif, hash2_dif
