use serde::{Deserialize, Serialize};

const MIN_CELL_EXTENT: f32 = 1.0e-6;

#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct FixationPoint {
    pub x: f32,
    pub y: f32,
    pub scale: f32,
    pub confidence: f32,
    #[serde(default)]
    pub width: f32,
    #[serde(default)]
    pub height: f32,
    #[serde(default, skip_serializing_if = "is_zero")]
    pub grid: usize,
}

impl FixationPoint {
    pub fn new(x: f32, y: f32, scale: f32, confidence: f32) -> Self {
        Self::with_extent(x, y, scale, scale, confidence)
    }

    pub fn with_extent(x: f32, y: f32, width: f32, height: f32, confidence: f32) -> Self {
        let width = width.clamp(MIN_CELL_EXTENT, 1.0);
        let height = height.clamp(MIN_CELL_EXTENT, 1.0);
        Self {
            x: x.clamp(0.0, 1.0),
            y: y.clamp(0.0, 1.0),
            scale: width.max(height),
            confidence: confidence.clamp(0.0, 1.0),
            width,
            height,
            grid: 0,
        }
    }

    pub fn with_grid_extent(
        x: f32,
        y: f32,
        width: f32,
        height: f32,
        confidence: f32,
        grid: usize,
    ) -> Self {
        let mut point = Self::with_extent(x, y, width, height, confidence);
        point.grid = grid;
        point
    }

    pub fn cell_grid(&self) -> Option<usize> {
        (self.grid > 0).then_some(self.grid)
    }

    pub fn cell_width(&self) -> f32 {
        if self.width > 0.0 {
            self.width.clamp(MIN_CELL_EXTENT, 1.0)
        } else {
            self.scale.clamp(MIN_CELL_EXTENT, 1.0)
        }
    }

    pub fn cell_height(&self) -> f32 {
        if self.height > 0.0 {
            self.height.clamp(MIN_CELL_EXTENT, 1.0)
        } else {
            self.scale.clamp(MIN_CELL_EXTENT, 1.0)
        }
    }

    pub fn bounds(&self) -> FixationBounds {
        self.scaled_bounds(1.0)
    }

    pub fn scaled_bounds(&self, scale: f32) -> FixationBounds {
        let scale = scale.max(MIN_CELL_EXTENT);
        let half_width = (self.cell_width() * scale * 0.5).clamp(0.0, 0.5);
        let half_height = (self.cell_height() * scale * 0.5).clamp(0.0, 0.5);
        FixationBounds {
            x_min: (self.x - half_width).clamp(0.0, 1.0),
            y_min: (self.y - half_height).clamp(0.0, 1.0),
            x_max: (self.x + half_width).clamp(0.0, 1.0),
            y_max: (self.y + half_height).clamp(0.0, 1.0),
        }
    }
}

fn is_zero(value: &usize) -> bool {
    *value == 0
}

#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct FixationBounds {
    pub x_min: f32,
    pub y_min: f32,
    pub x_max: f32,
    pub y_max: f32,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FixationSet {
    pub points: Vec<FixationPoint>,
    pub stop_probability: f32,
}

impl FixationSet {
    pub fn new(mut points: Vec<FixationPoint>, stop_probability: f32, k: usize) -> Self {
        points.truncate(k.max(1));
        while points.len() < k.max(1) {
            points.push(FixationPoint::new(0.5, 0.5, 0.25, 0.0));
        }
        Self {
            points,
            stop_probability: stop_probability.clamp(0.0, 1.0),
        }
    }

    pub fn top_point(&self) -> FixationPoint {
        self.points
            .first()
            .copied()
            .unwrap_or_else(|| FixationPoint::new(0.5, 0.5, 0.25, 0.0))
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FrameFixationTrace {
    pub frames: Vec<FixationSet>,
}

impl FrameFixationTrace {
    pub fn new(frames: Vec<FixationSet>) -> Self {
        Self { frames }
    }

    pub fn len(&self) -> usize {
        self.frames.len()
    }

    pub fn is_empty(&self) -> bool {
        self.frames.is_empty()
    }
}
