// instance_render.rs --- Instance-side cell-buffer ownership and paint-and-diff.

//! Instance-side rendering (T M5.2).
//!
//! Spec §sec:m5-remote, §sec:v01-remote-scope deliverable 1.
//!
//! The `prev`/`next` cell buffers live on the instance, not on the frontend.
//! [`paint_frame`](crate::editor::paint_frame) writes into `next`; the diff
//! against `prev` produces a [`InstanceMessage::CellDelta`] the transport
//! ships to the frontend. The frontend (whether in-process TUI or network)
//! consumes the messages and emits whatever it needs to display them.
//!
//! This factoring is the architectural payoff promised by the spec
//! (§sec:remote): a frontend that diffs full grids locally cannot exist
//! over a network without making the network case pathologically expensive.
//! Doing the diff once, on the instance side, is what makes the SSH
//! transport (T M5.7) cheap.

use crate::cell::{Cell, CellGrid, CellSize, diff};
use crate::editor::{EditorState, paint_frame};
use crate::protocol::{CursorState, InstanceMessage};

/// Owns the cell buffers and runs the paint-and-diff cycle.
pub struct RenderState {
    size: CellSize,
    /// Last frame as shipped to the frontend. After [`Self::render_frame`],
    /// this is the most recent painted state.
    prev: Vec<Cell>,
    /// Next frame being assembled. Cleared between frames so each render
    /// starts from a blank slate (windows that shrink on resize do not
    /// leak old contents).
    next: Vec<Cell>,
    /// True until the first frame is emitted, and after every resize.
    /// Tells [`Self::render_frame`] to mark the next [`InstanceMessage::CellDelta`]
    /// as a full-grid sync. Remote frontends use this flag to know they
    /// must blank their local buffer before applying the deltas.
    needs_full_grid: bool,
}

impl RenderState {
    /// Construct a render state for a grid of the given dimensions.
    #[must_use]
    pub fn new(size: CellSize) -> Self {
        let cells = (size.rows as usize) * (size.cols as usize);
        Self {
            size,
            prev: vec![Cell::default(); cells],
            next: vec![Cell::default(); cells],
            needs_full_grid: true,
        }
    }

    /// Current grid size in cells.
    #[must_use]
    pub fn size(&self) -> CellSize {
        self.size
    }

    /// Resize the grid. Reallocates `prev`/`next` and flags the next
    /// emitted [`InstanceMessage::CellDelta`] as a full-grid sync, since
    /// the old buffer's contents are no longer applicable.
    ///
    /// No-op if `new_size` matches the current size.
    pub fn resize(&mut self, new_size: CellSize) {
        if new_size == self.size {
            return;
        }
        self.size = new_size;
        let cells = (new_size.rows as usize) * (new_size.cols as usize);
        self.prev = vec![Cell::default(); cells];
        self.next = vec![Cell::default(); cells];
        self.needs_full_grid = true;
    }

    /// Force the next emitted [`InstanceMessage::CellDelta`] to be a
    /// full-grid sync, even when the buffer has not changed shape.
    /// Used on fresh attach when a frontend connects to an
    /// already-running instance and needs an authoritative starting
    /// state.
    pub fn force_full_grid_resync(&mut self) {
        self.needs_full_grid = true;
    }

    /// Paint one frame and return the messages to ship.
    ///
    /// Returns a `CellDelta` (with the changed spans) followed by a
    /// `Cursor` message. Returns an empty vec if the grid is too small
    /// to render meaningfully (`rows < 2` or `cols == 0`).
    ///
    /// `other_presences` (T M10.9): other attached frontends' cursor
    ///   and selection snapshots, with their assigned color slots.
    ///   The overlay paint pass modifies cells in `next` AFTER the
    ///   main paint and BEFORE the diff. Empty slice → no overlays
    ///   (in-process TUI use; M10.6/7 daemon use).
    pub fn render_frame(
        &mut self,
        state: &EditorState,
        other_presences: &[crate::overlay_paint::OtherPresence],
    ) -> Vec<InstanceMessage> {
        if self.size.rows < 2 || self.size.cols == 0 {
            return Vec::new();
        }

        let cursor_coord = {
            let mut grid = CellGrid {
                cells: &mut self.next,
                stride: self.size.cols,
                size: self.size,
            };
            let coord = paint_frame(state, &mut grid, self.size);
            // T M10.9 — overlay paint after main paint, before diff.
            // Modifies cells in `next`; diff captures the changes
            // as ordinary style updates.
            crate::overlay_paint::paint_other_frontend_overlays(
                state,
                &mut grid,
                self.size,
                other_presences,
            );
            coord
        };

        // Full-grid sync semantics (T M5.3): when `needs_full_grid` is
        // set, the `prev` buffer no longer represents what the consumer
        // sees on screen — either it is freshly-allocated (just-resized
        // or just-constructed), or a new frontend is attaching to a
        // running instance whose `prev` reflects the previous
        // frontend's view, not the new one's. Compare `next` against an
        // all-default grid so the consumer receives every non-default
        // cell of the current screen, regardless of what the previous
        // frontend was shown.
        let spans = if self.needs_full_grid {
            let blank = vec![Cell::default(); self.next.len()];
            diff(&blank, &self.next, self.size.cols, self.size)
        } else {
            diff(&self.prev, &self.next, self.size.cols, self.size)
        };
        let full_grid = self.needs_full_grid;
        self.needs_full_grid = false;

        std::mem::swap(&mut self.prev, &mut self.next);
        for cell in &mut self.next {
            *cell = Cell::default();
        }

        let cursor_state = cursor_coord.map(|coord| CursorState {
            coord,
            visible: true,
        });

        vec![
            InstanceMessage::CellDelta { spans, full_grid },
            InstanceMessage::Cursor(cursor_state),
        ]
    }
}

#[cfg(test)]
mod tests {
    // Acceptance home for T M5.2 (TUI re-architected as protocol consumer)
    // and T M5.3 (cell-delta diffing happens instance-side). The M5.2
    // contract — `RenderState` lives instance-side, `Frontend` becomes
    // a transport sink — is structural and verified by the existing
    // `paint_frame` tests passing unchanged. The M5.3 contract is
    // exercised by the `m5_3_*`-prefixed tests below: full-grid sync on
    // fresh attach, differential subsequent frames, fresh full-grid on
    // re-attach. See tests/INDEX.md for the full M5.x → coverage map.

    use super::*;
    use crate::cell::CellCoord;
    use crate::editor::EditorState;
    use crate::protocol::FrontendId;

    fn empty_state() -> EditorState {
        EditorState::new()
    }

    #[test]
    fn new_allocates_full_buffers() {
        let r = RenderState::new(CellSize::new(24, 80));
        assert_eq!(r.prev.len(), 24 * 80);
        assert_eq!(r.next.len(), 24 * 80);
        assert!(r.needs_full_grid);
    }

    #[test]
    fn render_returns_cell_delta_and_cursor() {
        let mut r = RenderState::new(CellSize::new(24, 80));
        let msgs = r.render_frame(&empty_state(), &[]);
        assert_eq!(msgs.len(), 2);
        assert!(matches!(msgs[0], InstanceMessage::CellDelta { .. }));
        assert!(matches!(msgs[1], InstanceMessage::Cursor(_)));
    }

    #[test]
    fn first_frame_is_full_grid_sync() {
        let mut r = RenderState::new(CellSize::new(24, 80));
        let msgs = r.render_frame(&empty_state(), &[]);
        match &msgs[0] {
            InstanceMessage::CellDelta { full_grid, .. } => assert!(*full_grid),
            _ => panic!("expected CellDelta first"),
        }
    }

    #[test]
    fn second_frame_is_differential() {
        let mut r = RenderState::new(CellSize::new(24, 80));
        let _ = r.render_frame(&empty_state(), &[]);
        let msgs = r.render_frame(&empty_state(), &[]);
        match &msgs[0] {
            InstanceMessage::CellDelta { full_grid, .. } => assert!(!*full_grid),
            _ => panic!("expected CellDelta first"),
        }
    }

    #[test]
    fn unchanged_state_produces_empty_spans_after_first_frame() {
        let state = empty_state();
        let mut r = RenderState::new(CellSize::new(24, 80));
        let _ = r.render_frame(&state, &[]);
        let msgs = r.render_frame(&state, &[]);
        match &msgs[0] {
            InstanceMessage::CellDelta { spans, .. } => assert!(
                spans.is_empty(),
                "expected no changes between identical frames; got {spans:?}"
            ),
            _ => unreachable!(),
        }
    }

    #[test]
    fn resize_reallocates_and_flags_full_grid() {
        let mut r = RenderState::new(CellSize::new(24, 80));
        let _ = r.render_frame(&empty_state(), &[]);
        assert!(!r.needs_full_grid);

        r.resize(CellSize::new(40, 120));
        assert_eq!(r.size(), CellSize::new(40, 120));
        assert_eq!(r.prev.len(), 40 * 120);
        assert_eq!(r.next.len(), 40 * 120);
        assert!(r.needs_full_grid);

        let msgs = r.render_frame(&empty_state(), &[]);
        match &msgs[0] {
            InstanceMessage::CellDelta { full_grid, .. } => assert!(*full_grid),
            _ => unreachable!(),
        }
    }

    #[test]
    fn resize_to_same_size_is_noop() {
        let mut r = RenderState::new(CellSize::new(24, 80));
        let _ = r.render_frame(&empty_state(), &[]);
        assert!(!r.needs_full_grid);
        r.resize(CellSize::new(24, 80));
        // No reallocation, no full-grid flip.
        assert!(!r.needs_full_grid);
    }

    #[test]
    fn force_full_grid_resync_flips_flag() {
        let mut r = RenderState::new(CellSize::new(24, 80));
        let _ = r.render_frame(&empty_state(), &[]);
        assert!(!r.needs_full_grid);
        r.force_full_grid_resync();
        assert!(r.needs_full_grid);
    }

    #[test]
    fn too_small_grid_returns_empty_messages() {
        // rows < 2 means we can't paint a text-area + status row.
        let mut r = RenderState::new(CellSize::new(1, 80));
        assert!(r.render_frame(&empty_state(), &[]).is_empty());

        let mut r = RenderState::new(CellSize::new(24, 0));
        assert!(r.render_frame(&empty_state(), &[]).is_empty());
    }

    #[test]
    fn cursor_message_carries_coord_when_paint_returns_one() {
        let mut r = RenderState::new(CellSize::new(24, 80));
        let msgs = r.render_frame(&empty_state(), &[]);
        match &msgs[1] {
            InstanceMessage::Cursor(Some(cs)) => {
                assert!(cs.visible);
                // Empty *scratch* buffer puts the cursor at row 0, col 0.
                assert_eq!(cs.coord, CellCoord::new(0, 0));
            }
            InstanceMessage::Cursor(None) => {
                // Acceptable too — empty state may suppress cursor.
            }
            _ => panic!("expected Cursor message"),
        }
    }

    // -------------------------------------------------------------------
    // T M5.3 acceptance criteria
    //
    // Spec §sec:v01-remote-scope deliverable 2:
    //   1. A fresh attach receives a full-grid CellDelta on the first frame.
    //   2. A subsequent paint with one changed line emits only the changed
    //      cells (not a full grid).
    //   3. Detach + re-attach produces a fresh full-grid sync (the instance
    //      does not preserve frontend-specific state).
    // -------------------------------------------------------------------

    #[test]
    fn m5_3_fresh_attach_receives_full_grid_celldelta() {
        // Criterion 1: the first frame after construction is a full-grid
        // CellDelta carrying every non-default cell.
        let mut r = RenderState::new(CellSize::new(24, 80));
        let msgs = r.render_frame(&empty_state(), &[]);
        match &msgs[0] {
            InstanceMessage::CellDelta { full_grid, spans } => {
                assert!(*full_grid, "first frame must be flagged full_grid=true");
                // Empty *scratch* still paints a status row + cursor.
                assert!(
                    !spans.is_empty(),
                    "fresh attach must surface the current screen, got empty spans"
                );
            }
            _ => panic!("expected CellDelta as the first message"),
        }
    }

    #[test]
    fn m5_3_differential_frame_after_small_edit_is_proportionally_small() {
        // Criterion 2: a state change that only affects a small region
        // of the grid produces a delta that touches only a small fraction
        // of cells, not the full grid.
        use crate::editor::EditorState;
        use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};

        let size = CellSize::new(24, 80);
        let mut state = EditorState::new();
        let mut r = RenderState::new(size);
        // Seat the prev buffer.
        let _ = r.render_frame(&state, &[]);

        // Single character insert.
        state.dispatch_key(
            FrontendId::LOCAL,
            KeyEvent {
                code: KeyCode::Char('x'),
                modifiers: KeyModifiers::NONE,
                kind: KeyEventKind::Press,
                state: KeyEventState::empty(),
            },
        );
        let msgs = r.render_frame(&state, &[]);
        match &msgs[0] {
            InstanceMessage::CellDelta { full_grid, spans } => {
                assert!(!*full_grid, "differential frame must not flag full_grid");
                let changed: usize = spans.iter().map(|s| s.cells.len()).sum();
                let total = (size.rows * size.cols) as usize;
                assert!(
                    changed * 10 < total,
                    "differential delta must be much smaller than full grid; \
                     got {changed} changed cells of {total} total"
                );
            }
            _ => panic!("expected CellDelta first"),
        }
    }

    #[test]
    fn m5_3_force_full_grid_resync_replays_full_screen_against_blank() {
        // Criterion 3: detach + re-attach produces a fresh full-grid
        // sync. The instance does not preserve frontend-specific state,
        // so the next frame after `force_full_grid_resync` reflects the
        // full screen against a blank baseline (not against what the
        // previous frontend saw).
        let size = CellSize::new(5, 20);
        let mut r = RenderState::new(size);

        // First render: seats prev with the painted frame.
        let first = r.render_frame(&empty_state(), &[]);
        let baseline_changed: usize = match &first[0] {
            InstanceMessage::CellDelta { spans, .. } => spans.iter().map(|s| s.cells.len()).sum(),
            _ => unreachable!(),
        };
        assert!(baseline_changed > 0, "baseline must paint some cells");

        // A second render with no state change normally produces zero
        // spans (the state matches prev exactly).
        let unchanged = r.render_frame(&empty_state(), &[]);
        match &unchanged[0] {
            InstanceMessage::CellDelta { full_grid, spans } => {
                assert!(!*full_grid);
                assert!(spans.is_empty(), "unchanged state should diff to nothing");
            }
            _ => unreachable!(),
        }

        // Now simulate detach + re-attach: the new frontend has no idea
        // what's on screen. force_full_grid_resync flags the next frame
        // for full sync.
        r.force_full_grid_resync();
        let resync = r.render_frame(&empty_state(), &[]);
        match &resync[0] {
            InstanceMessage::CellDelta { full_grid, spans } => {
                assert!(*full_grid, "post-resync frame must be full_grid=true");
                let resync_changed: usize = spans.iter().map(|s| s.cells.len()).sum();
                assert_eq!(
                    resync_changed, baseline_changed,
                    "fresh-attach resync must surface the same number of cells \
                     as the original full-grid render; \
                     baseline={baseline_changed}, resync={resync_changed}"
                );
            }
            _ => panic!("expected CellDelta first"),
        }
    }
}
