//! Bottom-panel wire types (Q#BP15, Q#BP15a, Q#BP16).
//!
//! A panel frame is the daemon's painted projection of one side window.
//! It shares [`crate::wire_grid`]'s cell rules with
//! [`crate::terminal::TerminalFrame`] but not its per-axis PTY caps: a
//! 4K surface at a small font is legitimately wider than 512 columns,
//! and the area bound is what keeps the encoding inside the transport
//! budget.
//!
//! Presence is explicit. [`PanelFramePayload::Absent`] is authoritative
//! and must be sent on close *and* on hide, because the receiver
//! retains its last valid frame: silence would leave a stale band on
//! screen indefinitely.

use crate::cell::{Cell, CellCoord, CellSize};
use crate::ids::BufferId;
use crate::wire_grid::{
    MAX_WIRE_GRID_GLYPH_BYTES, WireGridError, WireGridLimits, validate_wire_grid,
};

/// Shared visible-cell ceiling for a panel grid.
///
/// Identical to the terminal bound: it is the transport-safety limit,
/// not a PTY policy, so both messages answer to it.
pub const MAX_PANEL_VISIBLE_CELLS: usize = crate::wire_grid::MAX_WIRE_GRID_VISIBLE_CELLS;

/// Bounds a panel frame enforces on its cell grid.
///
/// The per-axis ceilings are the area bound itself rather than 512: any
/// axis larger than the area bound is already rejected by the area
/// check, so this expresses "no independent per-axis policy" without
/// leaving the multiplication unchecked.
const PANEL_GRID_LIMITS: WireGridLimits = WireGridLimits {
    max_rows: MAX_PANEL_VISIBLE_CELLS as u32,
    max_cols: MAX_PANEL_VISIBLE_CELLS as u32,
    max_visible_cells: MAX_PANEL_VISIBLE_CELLS,
    max_glyph_bytes: MAX_WIRE_GRID_GLYPH_BYTES,
};

/// The daemon's painted projection of one side window.
///
/// `panel_epoch` is opaque and monotonic per frontend: stable across
/// ordinary frames of one continuously present window/buffer, and
/// changed on buffer replacement, new side-window creation, and every
/// `Absent` → `Present` transition. That is what stops a stale
/// `PanelPointer` from addressing a reopened panel as if it were the
/// old one (Q#BP16).
///
/// `geometry_epoch` answers a *frontend* declaration and moves whenever
/// the frontend declares new effective cell geometry — including a font
/// or scale change that leaves [`CellSize`] identical, which is exactly
/// the case daemon-side value dedup cannot see (Q#BP2S1).
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PanelFrame {
    /// Buffer this frame projects.
    pub buffer_id: BufferId,
    /// Presentation identity, monotonic per frontend.
    pub panel_epoch: u64,
    /// The frontend geometry declaration this frame answers.
    pub geometry_epoch: u64,
    /// Panel grid dimensions in cells.
    pub size: CellSize,
    /// Row-major cells; exactly `size.area()` entries.
    pub cells: Vec<Cell>,
    /// Panel caret, or `None` when the panel shows no cursor.
    ///
    /// `paint_frame` returns the cursor separately from the cells, so a
    /// frame carrying cells alone would lose the caret.
    pub cursor: Option<CellCoord>,
    /// Whether the panel owns focus.
    ///
    /// Presentation and focus-chrome routing only (Q#BP14b) — the
    /// *keys* decision is `DispatchIdle` (Q#BP14a).
    pub focused: bool,
}

/// Explicit panel presence.
///
/// `Absent` is authoritative rather than implied by silence, and is
/// duplicate-suppressed like any other payload.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum PanelFramePayload {
    /// A panel is visible and this is its current frame.
    Present(PanelFrame),
    /// No panel is visible; clear any retained frame.
    Absent,
}

/// Why a [`PanelFrame`] is not structurally valid.
///
/// Validation is atomic: the frame is rejected whole and the receiver
/// retains its previous valid frame.
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
pub enum PanelFrameError {
    /// Rows or columns are zero or above the area-derived bounds.
    #[error("panel size {rows}x{cols} is outside 1..={max_rows}x1..={max_cols}")]
    Size {
        /// Declared rows.
        rows: u32,
        /// Declared columns.
        cols: u32,
        /// Row bound in force.
        max_rows: u32,
        /// Column bound in force.
        max_cols: u32,
    },
    /// The checked area exceeds the shared visible-cell bound.
    #[error("panel area {area} exceeds the visible-cell bound {max}")]
    Area {
        /// Checked `rows * cols`.
        area: usize,
        /// Shared visible-cell bound.
        max: usize,
    },
    /// `cells.len()` disagrees with the declared area.
    #[error("panel frame carries {actual} cells for a {expected}-cell area")]
    CellCount {
        /// Declared area.
        expected: usize,
        /// Supplied cell count.
        actual: usize,
    },
    /// The cursor lies outside the declared grid.
    #[error("panel cursor ({row},{col}) is outside the {rows}x{cols} grid")]
    Cursor {
        /// Cursor row.
        row: u32,
        /// Cursor column.
        col: u32,
        /// Declared rows.
        rows: u32,
        /// Declared columns.
        cols: u32,
    },
    /// A cell's glyph is not a legal wire glyph.
    #[error("panel cell {index} has an invalid glyph: {reason}")]
    Glyph {
        /// Row-major cell index.
        index: usize,
        /// Why the glyph failed.
        reason: &'static str,
    },
    /// A cell carries a frontend attachment, which panels never use.
    #[error("panel cell {index} carries an attachment")]
    Attachment {
        /// Row-major cell index.
        index: usize,
    },
    /// Aggregate glyph bytes exceed the shared budget.
    #[error("panel frame glyph bytes exceed the aggregate bound {max}")]
    GlyphBudget {
        /// Shared aggregate bound.
        max: usize,
    },
    /// An epoch is zero, which is reserved for "never declared".
    #[error("panel {field} epoch is zero, which is reserved for 'never declared'")]
    ZeroEpoch {
        /// Which epoch was zero.
        field: &'static str,
    },
}

impl PanelFrame {
    /// Check every structural rule a panel frame must satisfy.
    ///
    /// Pure: a rejected frame mutates nothing, so callers get atomic
    /// rejection for free.
    pub fn validate(&self) -> Result<(), PanelFrameError> {
        if self.panel_epoch == 0 {
            return Err(PanelFrameError::ZeroEpoch { field: "panel" });
        }
        if self.geometry_epoch == 0 {
            return Err(PanelFrameError::ZeroEpoch { field: "geometry" });
        }
        validate_wire_grid(self.size, &self.cells, self.cursor, PANEL_GRID_LIMITS)
            .map_err(panel_grid_error)
    }
}

/// Map a shared wire-grid failure onto this message's error type.
fn panel_grid_error(error: WireGridError) -> PanelFrameError {
    match error {
        WireGridError::Size {
            rows,
            cols,
            max_rows,
            max_cols,
        } => PanelFrameError::Size {
            rows,
            cols,
            max_rows,
            max_cols,
        },
        WireGridError::Area { area, max } => PanelFrameError::Area { area, max },
        WireGridError::CellCount { expected, actual } => {
            PanelFrameError::CellCount { expected, actual }
        }
        WireGridError::Cursor {
            row,
            col,
            rows,
            cols,
        } => PanelFrameError::Cursor {
            row,
            col,
            rows,
            cols,
        },
        WireGridError::Glyph { index, reason } => PanelFrameError::Glyph { index, reason },
        WireGridError::Attachment { index } => PanelFrameError::Attachment { index },
        WireGridError::GlyphBudget { max } => PanelFrameError::GlyphBudget { max },
    }
}
