import logging
from collections import deque

from plugin import Plugin
import file_methods as fm
import fixation_detector
from fixation_detector import (
    FixationDetectionMethod,
    can_use_3d_gaze_mapping,
    Fixation_Result_Factory,
    gaze_dispersion,
)


logger = logging.getLogger(__name__)

class FixationPatcherKieran(Plugin):
    @classmethod
    def parse_pretty_class_name(cls) -> str:
        return "Fixation Detector Patcher"

    def __init__(self, g_pool):
        super().__init__(g_pool)
        self.original_func = fixation_detector.detect_fixations
        fixation_detector.detect_fixations = detect_fixations_safer
        logger.info("Patched fixation detector")

    def cleanup(self):
        fixation_detector.detect_fixations = self.original_func
        logger.info("Unpatched fixation detector")




def detect_fixations_safer(
    capture, gaze_data, max_dispersion, min_duration, max_duration, min_data_confidence
):
    yield "Detecting fixations...", ()
    filtered_gaze_data = []
    for i,serialized in enumerate(gaze_data):
        try:
            datum = fm.Serialized_Dict(msgpack_bytes=serialized)
            if datum["confidence"] > min_data_confidence:
                filtered_gaze_data.append(datum)
        except Exception as exc:
            print('Failure on', i, exc)

    gaze_data = filtered_gaze_data
    if not gaze_data:
        logger.warning("No data available to find fixations")
        return "Fixation detection failed", ()

    method = (
        FixationDetectionMethod.GAZE_3D
        if can_use_3d_gaze_mapping(gaze_data)
        else FixationDetectionMethod.GAZE_2D
    )
    logger.info(f"Starting fixation detection using {method.value} data...")
    fixation_result = Fixation_Result_Factory()

    working_queue = deque()
    remaining_gaze = deque(gaze_data)

    while remaining_gaze:
        # check if working_queue contains enough data
        if (
            len(working_queue) < 2
            or (working_queue[-1]["timestamp"] - working_queue[0]["timestamp"])
            < min_duration
        ):
            datum = remaining_gaze.popleft()
            working_queue.append(datum)
            continue

        # min duration reached, check for fixation
        dispersion = gaze_dispersion(capture, working_queue, method)
        if dispersion > max_dispersion:
            # not a fixation, move forward
            working_queue.popleft()
            continue

        left_idx = len(working_queue)

        # minimal fixation found. collect maximal data
        # to perform binary search for fixation end
        while remaining_gaze:
            datum = remaining_gaze[0]
            if datum["timestamp"] > working_queue[0]["timestamp"] + max_duration:
                break  # maximum data found
            working_queue.append(remaining_gaze.popleft())

        # check for fixation with maximum duration
        dispersion = gaze_dispersion(capture, working_queue, method)
        if dispersion <= max_dispersion:
            fixation = fixation_result.from_data(
                dispersion, method, working_queue, capture.timestamps
            )
            yield "Detecting fixations...", fixation
            working_queue.clear()  # discard old Q
            continue

        slicable = list(working_queue)  # deque does not support slicing
        right_idx = len(working_queue)

        # binary search
        while left_idx < right_idx - 1:
            middle_idx = (left_idx + right_idx) // 2
            dispersion = gaze_dispersion(
                capture,
                slicable[: middle_idx + 1],
                method,
            )
            if dispersion <= max_dispersion:
                left_idx = middle_idx
            else:
                right_idx = middle_idx

        # left_idx-1 is last valid base datum
        final_base_data = slicable[:left_idx]
        to_be_placed_back = slicable[left_idx:]
        dispersion_result = gaze_dispersion(capture, final_base_data, method)

        fixation = fixation_result.from_data(
            dispersion_result, method, final_base_data, capture.timestamps
        )
        yield "Detecting fixations...", fixation
        working_queue.clear()  # clear queue
        remaining_gaze.extendleft(reversed(to_be_placed_back))

    yield "Fixation detection complete", ()
