import cv2
import numpy as np

from config import *
from helpers import clamp, box_center, box_wh, clip_box

# Kalman and KLT trackers
# =========================
# KALMAN 8D
# =========================

class Kalman8D:
    def __init__(self):
        self.x = np.zeros((8, 1), dtype=np.float32)
        self.P = np.eye(8, dtype=np.float32) * 50.0

        self.H = np.zeros((4, 8), dtype=np.float32)
        self.H[0, 0] = 1.0
        self.H[1, 1] = 1.0
        self.H[2, 2] = 1.0
        self.H[3, 3] = 1.0

        self.R = np.eye(4, dtype=np.float32)
        # Снижено с 16→9 и 25→16: новая YOLO даёт точные детекты,
        # Kalman должен им больше доверять и быстрее подстраиваться.
        self.R[0, 0] = 9.0
        self.R[1, 1] = 9.0
        self.R[2, 2] = 16.0
        self.R[3, 3] = 16.0

        self.initialized = False
        # Процесс-шум повышен для реактивности на резкие манёвры.
        # Было: q_pos=2.0, q_size=3.0, q_vel=4.0 — для плавного tracking'а
        # Стало: q_pos=5.0, q_size=3.5, q_vel=12.0 — цель до 160 px/s со швырками
        self.q_pos = 5.0
        self.q_size = 3.5
        self.q_vel = 12.0

    def F(self, dt):
        F = np.eye(8, dtype=np.float32)
        F[0, 4] = dt
        F[1, 5] = dt
        F[2, 6] = dt
        F[3, 7] = dt
        return F

    def Q(self, dt, q_scale=1.0):
        q_scale = float(max(0.25, q_scale))
        q = np.zeros((8, 8), dtype=np.float32)
        s1 = dt * dt
        s2 = dt
        q[0, 0] = self.q_pos * s1 * q_scale
        q[1, 1] = self.q_pos * s1 * q_scale
        q[2, 2] = self.q_size * s1 * q_scale
        q[3, 3] = self.q_size * s1 * q_scale
        q[4, 4] = self.q_vel * s2 * q_scale
        q[5, 5] = self.q_vel * s2 * q_scale
        q[6, 6] = self.q_vel * s2 * q_scale
        q[7, 7] = self.q_vel * s2 * q_scale
        return q

    def predict(self, dt, q_scale=1.0):
        dt = float(max(1e-3, dt))
        F = self.F(dt)
        self.x = F @ self.x
        self.P = F @ self.P @ F.T + self.Q(dt, q_scale=q_scale)
        return self.x[:4].flatten()

    def update(self, z):
        z = np.array(z, dtype=np.float32).reshape(4, 1)
        y = z - self.H @ self.x
        S = self.H @ self.P @ self.H.T + self.R
        K = self.P @ self.H.T @ np.linalg.inv(S)
        self.x = self.x + K @ y
        I = np.eye(8, dtype=np.float32)
        self.P = (I - K @ self.H) @ self.P

    def init_from_box(self, box):
        cx, cy = box_center(box)
        w, h = box_wh(box)
        self.x[:] = 0
        self.x[0, 0] = cx
        self.x[1, 0] = cy
        self.x[2, 0] = w
        self.x[3, 0] = h
        self.P = np.eye(8, dtype=np.float32) * 50.0
        self.initialized = True

    def to_box(self):
        cx = float(self.x[0, 0])
        cy = float(self.x[1, 0])
        w = float(max(2.0, self.x[2, 0]))
        h = float(max(2.0, self.x[3, 0]))
        return np.array([cx - w * 0.5, cy - h * 0.5, cx + w * 0.5, cy + h * 0.5], dtype=np.float32)

    def uncertainty(self):
        return float(self.P[0, 0] + self.P[1, 1] + self.P[2, 2] + self.P[3, 3])


# =========================
# KLT TRACKER
# =========================

class KLTTracker:
    def __init__(self):
        self.prev_gray = None
        self.pts = None
        self.box = None
        self.good_count = 0
        self.quality = 0.0

    def reset(self):
        self.prev_gray = None
        self.pts = None
        self.box = None
        self.good_count = 0
        self.quality = 0.0

    def init(self, frame_bgr, box):
        gray = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2GRAY)
        x1, y1, x2, y2 = map(int, box)
        mask = np.zeros_like(gray)
        cv2.rectangle(mask, (x1, y1), (x2, y2), 255, -1)

        pts = cv2.goodFeaturesToTrack(
            gray,
            maxCorners=KLT_MAX_CORNERS,
            qualityLevel=KLT_QUALITY,
            minDistance=KLT_MIN_DIST,
            mask=mask
        )
        self.prev_gray = gray
        self.pts = pts
        self.box = box.copy()
        self.good_count = 0
        self.quality = 0.0

    def update(self, frame_bgr):
        if self.prev_gray is None or self.pts is None or len(self.pts) == 0 or self.box is None:
            self.quality = 0.0
            self.good_count = 0
            return None

        gray = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2GRAY)
        next_pts, st, err = cv2.calcOpticalFlowPyrLK(
            self.prev_gray, gray, self.pts, None,
            winSize=KLT_WIN,
            maxLevel=KLT_MAX_LEVEL,
            criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 30, 0.01)
        )

        if next_pts is None or st is None:
            self.quality = 0.0
            self.good_count = 0
            self.prev_gray = gray
            return None

        st = st.reshape(-1)
        good_old = self.pts[st == 1].reshape(-1, 2)
        good_new = next_pts[st == 1].reshape(-1, 2)

        self.good_count = int(len(good_new))
        if self.good_count < 6:
            self.quality = 0.0
            self.prev_gray = gray
            self.pts = good_new.reshape(-1, 1, 2) if self.good_count > 0 else None
            return None

        disp = good_new - good_old
        med = np.median(disp, axis=0)
        d = np.linalg.norm(disp - med[None, :], axis=1)
        inliers = d < KLT_OUTLIER_THRESH
        in_cnt = int(np.count_nonzero(inliers))

        if in_cnt < 6:
            self.quality = 0.0
            self.prev_gray = gray
            self.pts = good_new.reshape(-1, 1, 2)
            return None

        disp_in = disp[inliers]
        med = np.median(disp_in, axis=0)
        dx, dy = float(med[0]), float(med[1])

        x1, y1, x2, y2 = self.box
        new_box = np.array([x1 + dx, y1 + dy, x2 + dx, y2 + dy], dtype=np.float32)

        frac = float(in_cnt) / float(len(inliers))
        self.quality = clamp(frac * (self.good_count / max(1, KLT_MAX_CORNERS)), 0.0, 1.0)

        self.prev_gray = gray
        self.pts = good_new[inliers].reshape(-1, 1, 2)
        self.box = new_box
        return new_box


# =========================
