//! Viewport state: offset + zoom + terminal size, all in cells.

/// Default cells per canvas pixel along the X axis (horizontal).
/// 1 cell ≈ 8 px gives a usable density for typical Obsidian canvases.
pub const CELLS_PER_PX_X: f32 = 1.0 / 8.0;
/// Default cells per canvas pixel along the Y axis (vertical).
/// 1 cell ≈ 16 px because a terminal cell is roughly twice as tall as wide.
pub const CELLS_PER_PX_Y: f32 = 1.0 / 16.0;

/// Soft clamps for zoom. Beyond these the layout becomes unusable.
pub const MIN_ZOOM: f32 = 0.1;
pub const MAX_ZOOM: f32 = 10.0;

/// State of the visible window over the canvas, expressed in terminal cells.
#[derive(Debug, Clone, Copy)]
pub struct Viewport {
    /// Canvas-cell coordinate that aligns with screen column 0.
    pub offset_x: i32,
    /// Canvas-cell coordinate that aligns with screen row 0.
    pub offset_y: i32,
    pub zoom: f32,
    pub cols: u16,
    pub rows: u16,
}

impl Viewport {
    pub fn new(cols: u16, rows: u16) -> Self {
        Self {
            offset_x: 0,
            offset_y: 0,
            zoom: 1.0,
            cols,
            rows,
        }
    }

    pub fn set_zoom(&mut self, zoom: f32) {
        self.zoom = zoom.clamp(MIN_ZOOM, MAX_ZOOM);
    }

    pub fn set_offset(&mut self, x: i32, y: i32) {
        self.offset_x = x;
        self.offset_y = y;
    }

    pub fn pan(&mut self, dx: i32, dy: i32) {
        self.offset_x = self.offset_x.saturating_add(dx);
        self.offset_y = self.offset_y.saturating_add(dy);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn zoom_is_clamped() {
        let mut v = Viewport::new(80, 24);
        v.set_zoom(100.0);
        assert!((v.zoom - MAX_ZOOM).abs() < f32::EPSILON);
        v.set_zoom(0.0);
        assert!((v.zoom - MIN_ZOOM).abs() < f32::EPSILON);
    }
}
