# modules/planarian_tracker.py
'''
Created on 16 avr. 2026

@author: denis
'''

import cv2
import logging
import numpy as np

logger = logging.getLogger(__name__)


class PlanarianTracker:
    """
    Détection et suivi d'une planaire dans un tube.
    Instancié une fois par caméra active, réutilisé frame à frame.
    Utilise la soustraction de fond MOG2 — léger sur Raspberry Pi 4.
    """

    def __init__(self, tube_axis: str = "vertical", min_area_px: int = 20):
        # Axe du tube : "vertical" (cy) ou "horizontal" (cx)
        self.tube_axis   = tube_axis
        self.min_area_px = min_area_px

        # Etat inter-frame
        self._prev_cx  = None
        self._prev_cy  = None
        self._prev_ts  = None

        # Soustracteur de fond adaptatif MOG2
        self._bg_sub = cv2.createBackgroundSubtractorMOG2(
            history      = 50,
            varThreshold = 25,
            detectShadows= False,
        )

    def reset(self):
        """
        Réinitialise l'état inter-frame — appeler lors du changement de puits.
        """
        self._prev_cx = None
        self._prev_cy = None
        self._prev_ts = None
        # Réinitialise le fond appris
        self._bg_sub  = cv2.createBackgroundSubtractorMOG2(
            history      = 50,
            varThreshold = 25,
            detectShadows= False,
        )

    '''
    def process(self, frame: np.ndarray, ts: float) -> dict:
        """
        Analyse une frame décodée numpy.
        Retourne un dict de métriques attachable aux labels ReductStore.

        :param frame: Frame BGR décodée (numpy array)
        :param ts:    Timestamp epoch secondes (float)
        :return:      dict métriques
        """
        result = self._empty_result(ts)

        gray    = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        fg_mask = self._bg_sub.apply(gray)

        # Nettoyage morphologique du masque
        kernel  = np.ones((3, 3), np.uint8)
        fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_OPEN,  kernel)
        fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_CLOSE, kernel)

        contours, _ = cv2.findContours(
            fg_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
        )

        if not contours:
            self._update_prev(None, None, ts)
            return result

        # Plus grand contour = planaire
        largest = max(contours, key=cv2.contourArea)
        area    = cv2.contourArea(largest)

        if area < self.min_area_px:
            self._update_prev(None, None, ts)
            return result

        # Centre de masse
        M = cv2.moments(largest)
        if M["m00"] == 0:
            return result

        cx = int(M["m10"] / M["m00"])
        cy = int(M["m01"] / M["m00"])
        h, w = frame.shape[:2]

        # Position normalisée sur l'axe du tube (0.0 → 1.0)
        axial_pos = (cy / h) if self.tube_axis == "vertical" else (cx / w)

        # Vitesse calculée entre frames
        speed_px_s  = None
        axial_speed = None

        if self._prev_cx is not None and self._prev_ts is not None:
            dt = ts - self._prev_ts
            if dt > 0:
                dx          = cx - self._prev_cx
                dy          = cy - self._prev_cy
                speed_px_s  = float(np.sqrt(dx**2 + dy**2) / dt)
                # Vitesse signée sur l'axe du tube
                # + = vers bas/droite, - = vers haut/gauche
                axial_speed = float((dy / dt) if self.tube_axis == "vertical" else (dx / dt))

        result.update({
            "detected"    : True,
            "cx"          : cx,
            "cy"          : cy,
            "area_px"     : int(area),
            "speed_px_s"  : round(speed_px_s,  3) if speed_px_s  is not None else 0.0,
            "axial_speed" : round(axial_speed,  3) if axial_speed is not None else 0.0,
            "axial_pos"   : round(axial_pos,    4),
        })

        self._update_prev(cx, cy, ts)
        return result
    '''
        
    def process(self, frame: np.ndarray, ts: float) -> tuple[np.ndarray, dict]:
        """
        Analyse une frame et dessine les contours détectés directement sur l'image.
        Retourne (frame_annotée, métriques).
        
            Contours fins    Vert (0,255,0)    Tous les contours valides détectés
            Contour épais    Cyan (255,255,0)    Planaire principale (plus grand contour)
            Croix + cercle    Rouge (0,0,255)    Centre de masse exact
            Texte    Blanc    Vitesse px/s + position axiale normalisée
        """
        result       = self._empty_result(ts)
        frame_out    = frame.copy()    # copie pour ne pas modifier l'original
    
        gray    = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        fg_mask = self._bg_sub.apply(gray)
    
        kernel  = np.ones((3, 3), np.uint8)
        fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_OPEN,  kernel)
        fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_CLOSE, kernel)
    
        contours, _ = cv2.findContours(
            fg_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
        )
    
        if not contours:
            self._update_prev(None, None, ts)
            return frame_out, result
    
        # Filtre les contours significatifs
        valid_contours = [c for c in contours if cv2.contourArea(c) >= self.min_area_px]
    
        if not valid_contours:
            self._update_prev(None, None, ts)
            return frame_out, result
    
        # Dessine tous les contours valides en vert fin
        cv2.drawContours(frame_out, valid_contours, -1, (0, 255, 0), 1)
    
        # Plus grand contour = planaire principale
        largest = max(valid_contours, key=cv2.contourArea)
        area    = cv2.contourArea(largest)
    
        # Contour principal en cyan plus épais
        cv2.drawContours(frame_out, [largest], -1, (255, 255, 0), 2)
    
        M = cv2.moments(largest)
        if M["m00"] == 0:
            return frame_out, result
    
        cx = int(M["m10"] / M["m00"])
        cy = int(M["m01"] / M["m00"])
        h, w = frame.shape[:2]
    
        axial_pos   = (cy / h) if self.tube_axis == "vertical" else (cx / w)
        speed_px_s  = None
        axial_speed = None
    
        if self._prev_cx is not None and self._prev_ts is not None:
            dt = ts - self._prev_ts
            if dt > 0:
                dx          = cx - self._prev_cx
                dy          = cy - self._prev_cy
                speed_px_s  = float(np.sqrt(dx**2 + dy**2) / dt)
                axial_speed = float((dy / dt) if self.tube_axis == "vertical" else (dx / dt))
    
        # Croix sur le centre de masse
        cross_size = 8
        cv2.line(frame_out, (cx - cross_size, cy), (cx + cross_size, cy), (0, 0, 255), 1)
        cv2.line(frame_out, (cx, cy - cross_size), (cx, cy + cross_size), (0, 0, 255), 1)
    
        # Cercle centré sur la planaire
        cv2.circle(frame_out, (cx, cy), 12, (0, 0, 255), 1)
    
        # Texte vitesse + position axiale
        label = f"v={speed_px_s:.1f}px/s  ax={axial_pos:.2f}" if speed_px_s is not None else f"ax={axial_pos:.2f}"
        cv2.putText(
            frame_out, label,
            (max(cx - 60, 0), max(cy - 18, 12)),
            cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA,
        )
    
        result.update({
            "detected"    : True,
            "cx"          : cx,
            "cy"          : cy,
            "area_px"     : int(area),
            "speed_px_s"  : round(speed_px_s,  3) if speed_px_s  is not None else 0.0,
            "axial_speed" : round(axial_speed,  3) if axial_speed is not None else 0.0,
            "axial_pos"   : round(axial_pos,    4),
        })
    
        self._update_prev(cx, cy, ts)
        return frame_out, result              
        
        
    # ------------------------------------------------------------------ #
    def _empty_result(self, ts: float) -> dict:
        return {
            "timestamp"  : ts,
            "detected"   : False,
            "cx"         : 0,
            "cy"         : 0,
            "area_px"    : 0,
            "speed_px_s" : 0.0,
            "axial_speed": 0.0,
            "axial_pos"  : 0.0,
        }

    def _update_prev(self, cx, cy, ts):
        self._prev_cx = cx
        self._prev_cy = cy
        self._prev_ts = ts
  
        