// view.rs --- The View trait. Polymorphic interpretive layer over a buffer.

//! Views: the polymorphic interpretive layer attached to a buffer.
//!
//! Implements the view contract from spec §3.3 (Views). A view subscribes
//! to edits, maintains derived state, contributes to rendering, and may
//! intercept edits before they reach the rope.
//!
//! # Re-entry by construction
//!
//! Views never hold a `&Buffer` or `&mut Buffer` of their own. The buffer
//! passes itself into every callback (per spec §2.6, Checkpoint 6). While a
//! callback runs, the buffer's `&mut self` is held by the buffer's own
//! stack frame; no aliasing reference exists for the view to use, so a view
//! cannot recursively call `buffer.apply_edit` from inside its own
//! `on_edit` --- the borrow checker rejects it.
//!
//! # Composition (T M2.9)
//!
//! A window holds one base [`crate::text_view::TextView`] plus a stack
//! of overlay views (`window.overlays`). The frontend renders them in
//! a deterministic order:
//!
//! 1. The base text view runs first. It clears every cell in the
//!    window's viewport and paints buffer text + default style.
//! 2. Overlays then run in attach order (FIFO). Each overlay
//!    observes the cells already written and may read, mutate in
//!    place, or replace any cell inside the viewport.
//! 3. At any cell two views both touch, the later writer wins on the
//!    fields it chooses to write. Overlays are free to read existing
//!    fields and merge — see [`crate::overlay::StyleSpanOverlay`] for
//!    a style-merging overlay and [`crate::overlay::VirtualCellOverlay`]
//!    for a whole-cell-replacement overlay.
//!
//! Cursor placement and scrolling use the base text view's
//! [`View::pos_to_display`] only; overlays do not move the cursor.
//! Composition adds well under 10% overhead over a single-view render
//! when overlays touch only the cells they declare (see
//! `composition_overhead_under_ten_percent` in `editor.rs`).

use crate::buffer::{Buffer, BufferError, BufferId, EditOp};
use crate::cell::{CellCoord, CellGrid, CellSize};
use crate::fold_view::VisibleLineMap;
use crate::rope::{Edit, Position};

// ---------------------------------------------------------------------------
// InterceptContext (T M7.4)
// ---------------------------------------------------------------------------

/// Snapshot of the buffer's identity and shape, passed to
/// [`View::intercept_edit`] in lieu of a `&Buffer` reference.
///
/// # Why a snapshot, not a `&Buffer`
///
/// Pre-M7.4, `intercept_edit` received `&Buffer`. The Lua-bindings
/// layer held the registry's `RefCell::borrow_mut` for the full
/// duration of the call, so an intercept body that re-entered
/// `pmacs.buffer.X` synchronously hit a recursive-borrow error
/// (`BindingError::Reentrant`); the M6.10 audit fix surfaced this as
/// a typed error rather than a panic but did not enable the re-entry.
///
/// M7.4 splits the edit flow into three phases. Phase 2 runs the
/// intercept chain with the registry borrow released, so an intercept
/// body may call back into `pmacs.buffer.X` on any buffer. The cost:
/// the intercept can no longer hold a `&Buffer` (the buffer might be
/// mutated by the re-entrant call). Instead, we hand it an
/// `InterceptContext` snapshot taken at the start of the edit; the
/// fields cover every read the intercept needs (`buf_id` for routing,
/// `buf_len` for clamping positions, `buf_name` for diagnostic
/// messages, `revision` for "did this snapshot drift?" checks).
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InterceptContext {
    /// The buffer's identifier. Stable across the edit.
    pub buf_id: BufferId,
    /// Buffer length in bytes at the moment the snapshot was taken.
    pub buf_len: u64,
    /// Buffer name at the moment the snapshot was taken.
    pub buf_name: String,
    /// Buffer revision at the moment the snapshot was taken. Useful
    /// for re-entrant cross-buffer edits to detect whether the parent
    /// buffer was mutated during their lifetime.
    pub revision: u64,
}

impl InterceptContext {
    /// Build a context snapshot from a buffer reference.
    #[must_use]
    pub fn snapshot(buf: &Buffer) -> Self {
        Self {
            buf_id: buf.id(),
            buf_len: buf.len(),
            buf_name: buf.name().to_string(),
            revision: buf.revision(),
        }
    }
}

// ---------------------------------------------------------------------------
// Coordinates
// ---------------------------------------------------------------------------

/// Coordinate in display space (row, col), measured in cells.
///
/// Distinct from [`CellCoord`] in name only at this stage, but kept separate
/// for documentation: `DisplayCoord` is a *content* coordinate (reported by
/// `pos_to_display`), `CellCoord` is a *grid* coordinate (where to draw).
/// They coincide for plain text but diverge once virtual lines, wrapping,
/// and inline expansions appear.
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
pub struct DisplayCoord {
    /// 0-based **source line** index.
    ///
    /// Deliberately still the source line under wrapping, not a visual
    /// row. Redefining it would have broken every existing consumer —
    /// `overlay_paint`'s `row - view_top`, vertical motion's bounds
    /// check — with no compile error. Adding [`Self::sub_row`] beside it
    /// instead leaves those consumers *correct*, not merely findable.
    pub row: u32,
    /// Which visual row **within** `row`, when the line wraps.
    ///
    /// `0` for every unwrapped line and under
    /// [`WrapMode::Truncate`](crate::view::WrapMode::Truncate), which is
    /// what makes this field additive: code that has never heard of
    /// wrapping keeps computing the right answer.
    pub sub_row: u32,
    /// 0-based column, within the visual row named by `sub_row`.
    pub col: u32,
}

impl DisplayCoord {
    /// Construct a display coordinate on a line's first visual row.
    #[must_use]
    pub const fn new(row: u32, col: u32) -> Self {
        Self {
            row,
            sub_row: 0,
            col,
        }
    }

    /// Construct a display coordinate on a specific visual row of a
    /// wrapped line.
    #[must_use]
    pub const fn wrapped(row: u32, sub_row: u32, col: u32) -> Self {
        Self { row, sub_row, col }
    }
}

/// The layout facts a coordinate mapping needs, which the mapping
/// itself cannot know.
///
/// # Why this is a required parameter
///
/// `pos_to_display` took `(&self, buf, pos)` and had no notion of the
/// grid at all — so under wrapping it could not compute a visual row,
/// and `display_to_pos` could not invert one. Passing the missing
/// facts as a required argument is deliberate: it makes the compiler
/// enumerate every call site rather than leaving an audit to grep.
///
/// That is the opposite choice from [`DisplayCoord::sub_row`], and for
/// the opposite reason. Enforcement is possible on the way in, so it is
/// taken; it is not possible on the way out, so the output is made
/// correct-by-default instead.
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
pub struct LayoutCtx {
    /// Width in cells of the content area the text wraps within.
    ///
    /// `0` means "not rendered yet" — the same convention
    /// `Window::last_visible_rows` uses — and is treated as unwrapped,
    /// since a viewport with no columns has no rows to distinguish.
    pub cols: u32,
    /// The window's resolved wrap mode.
    pub wrap: WrapMode,
    /// First visible display column — the window's horizontal scroll
    /// offset (Stage 4, framing Q#HS7).
    ///
    /// **Stored unsnapped.** It is one per-window column, while "does
    /// this column bisect a wide glyph?" is a *per-line* question, so no
    /// single snapped value could be canonical for every visible line.
    /// Each line derives its own **effective edge** during the walk it
    /// already performs from column 0 (framing Q#HS7(c′)).
    ///
    /// Inert under [`WrapMode::Wrap`]: a wrapped line has nothing past
    /// the right edge to scroll toward, so the wrap path ignores this
    /// entirely and stays byte-identical to Stage 3.
    pub view_left: u32,
}

impl LayoutCtx {
    /// The identity context: no wrapping, width irrelevant.
    ///
    /// Every pre-wrap caller means this, and saying so explicitly is
    /// what makes those call sites readable as decisions rather than
    /// oversights.
    #[must_use]
    pub const fn truncated() -> Self {
        Self {
            cols: 0,
            wrap: WrapMode::Truncate,
            view_left: 0,
        }
    }

    /// Whether this context actually wraps.
    #[must_use]
    pub const fn wrapping(self) -> bool {
        matches!(self.wrap, WrapMode::Wrap) && self.cols > 0
    }

    /// The horizontal offset that actually applies.
    ///
    /// Always `0` when wrapping, which is what makes `view_left` inert
    /// under `wrap` **by construction** rather than by every caller
    /// remembering to check. A wrapped line has no content past the
    /// right edge, so a non-zero offset there could only hide text that
    /// nothing would ever scroll back to.
    #[must_use]
    pub const fn effective_left(self) -> u32 {
        if self.wrapping() { 0 } else { self.view_left }
    }
}

/// How a line wider than the viewport is shown --- the long-lines
/// stage, `docs/long-lines-framing.md`.
///
/// # Why the renderer is told, rather than asking
///
/// This is the *resolved* mode, not the setting's name. `ui.line-wrap`
/// is **buffer-local** (framing Q#LL2), and the config registry has no
/// ambient current buffer by design (`config_registry.rs`, Q#CR4) — a
/// caller wanting buffer-aware behavior must pass the `BufferId`. The
/// render driver holds both the registry and the buffer, resolves once
/// per window per frame, and puts the answer here. Views stay
/// config-agnostic, exactly as they do for folds.
///
/// # `Truncate` is the identity case, deliberately
///
/// Every behavior predating this type is `Truncate`, and it must stay
/// byte-identical under it — which is what lets the wrap work be
/// verified against the existing suite rather than against new
/// assertions.
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Hash)]
pub enum WrapMode {
    /// One source line per row; the remainder is clipped at the right
    /// edge. What every pre-Stage-3 caller did.
    #[default]
    Truncate,
    /// A line longer than the viewport continues on the following rows.
    ///
    /// Character wrap, not word wrap (framing Q#LL5): it matches
    /// Emacs's default, and it is the only break rule both frontends
    /// can implement identically without pulling UAX #14 into the grid.
    Wrap,
}

/// What to render and where.
///
/// The frontend computes the viewport (which buffer range maps to which
/// cell origin) and hands it to the view. The view fills cells inside that
/// origin/size window.
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct Viewport<'a> {
    /// First byte in the buffer to consider rendering.
    pub buffer_start: Position,
    /// One past the last byte to consider.
    pub buffer_end: Position,
    /// Top-left cell in the grid where rendering should start.
    pub cell_origin: CellCoord,
    /// Number of cells the viewport occupies.
    pub cell_size: CellSize,
    /// Width of the line-number gutter reserved to the *left* of
    /// `cell_origin` (UX gutter arc). `0` when no gutter. Overlays that
    /// want to draw in the gutter (e.g. the diagnostic sign) reach it at
    /// `cell_origin.col - gutter_w`; overlays that only touch the text area
    /// ignore it (the origin is already shifted past the gutter).
    pub gutter_w: u32,
    /// The rendering window's collapsed regions (Arc 6 Stage 2, Q#FD12),
    /// or `None` when this window's buffer has no folds — the unfolded
    /// path then stays byte-identical to the pre-folding renderer.
    ///
    /// Borrowed rather than owned so `Viewport` stays [`Copy`]: the frame
    /// builds **one map per rendered window** (never one per frame — a
    /// split may show different buffers) and hands the same shared
    /// reference to every painter of that window.
    pub folds: Option<&'a VisibleLineMap>,
    /// How lines wider than `cell_size.cols` are shown, already
    /// resolved for this window's buffer (see [`WrapMode`]).
    ///
    /// A required field rather than a defaulted one on purpose: every
    /// construction site has to state which behavior it means, so the
    /// pre-Stage-3 sites read as *deliberately* unwrapped rather than
    /// merely untouched.
    pub wrap: WrapMode,
    /// First visible display column (Stage 4). See
    /// [`LayoutCtx::view_left`]; required rather than defaulted for the
    /// same reason `wrap` is.
    pub view_left: u32,
}

impl Viewport<'_> {
    /// The horizontal offset that actually applies — `0` when wrapping,
    /// for the reason [`LayoutCtx::effective_left`] gives.
    #[must_use]
    pub const fn left_edge(&self) -> u32 {
        if matches!(self.wrap, WrapMode::Wrap) {
            0
        } else {
            self.view_left
        }
    }

    /// Clip a **line**-column range to what is on screen, returning
    /// **screen** columns — or `None` when none of it is visible.
    ///
    /// # Why every buffer-coordinate decorator must use this
    ///
    /// Stage 4 translated the base text walk and nothing else, which
    /// split the frame in half: at `view_left = 10` a glyph at source
    /// column 10 painted at screen column 0 while its syntax style,
    /// diagnostic underline, search wash and selection painted at screen
    /// column 10 — or vanished. Decorations drifted off the characters
    /// they describe, silently, and only once a window was scrolled.
    ///
    /// Every such site had the same two lines (`start_col.min(max_cols)`,
    /// `end_col.min(max_cols)`) — correct only while the left edge was
    /// pinned at zero. One helper replaces all of them so a future
    /// decorator inherits the translation instead of re-deriving it.
    ///
    /// **Five adopters**, and the count is the point: syntax/LSP
    /// styling, diagnostic underlines, search washes,
    /// [`crate::overlay::BufferStyleOverlay`], and the selection
    /// painter. The selection was nearly the exception — Stage 4's first
    /// version duplicated the rule there, justified by a width this
    /// painter supposedly needed and the viewport lacked. That was
    /// false: the render viewport's `cell_size.cols` is already
    /// `rect.size.cols - gutter_w`, and its origin already sits past the
    /// gutter. A canonical rule with one honest exception is not
    /// canonical, so the exception went.
    ///
    /// **Not for [`crate::overlay::StyleSpanOverlay`] or
    /// [`crate::overlay::VirtualCellOverlay`]**: those are documented as
    /// viewport-relative, so their columns are already screen columns
    /// and translating them twice would be the mirror defect.
    #[must_use]
    pub fn visible_cols(&self, start_col: u32, end_col: u32) -> Option<(u32, u32)> {
        let left = self.left_edge();
        let right = left.saturating_add(self.cell_size.cols);
        let start = start_col.max(left);
        let end = end_col.min(right);
        // A range that begins off-screen left and reaches past the edge
        // is CLIPPED, not skipped — that is the selection defect this
        // returns `Some` for.
        (end > start).then(|| (start - left, end - left))
    }

    /// Row offset within this viewport for source `line`, given the
    /// viewport's first (visible) source line.
    ///
    /// `None` when `line` is above `start_line` or collapsed away — a
    /// hidden line simply has no row, so its painter skips it. Without a
    /// map this is the identity `line - start_line` every consumer used
    /// before folding.
    ///
    /// The result is monotonically non-decreasing in `line`, so a caller
    /// that bounds its walk with `row_offset >= rows` may still `break`.
    #[must_use]
    pub fn row_offset_of(self, start_line: usize, line: usize) -> Option<u32> {
        let raw = line.checked_sub(start_line)?;
        let rows = match self.folds {
            Some(map) if !map.is_identity() => {
                if map.is_hidden(line) {
                    return None;
                }
                map.visible_rows_between(start_line, line)
            }
            _ => raw,
        };
        u32::try_from(rows).ok()
    }

    /// The inverse of [`Self::row_offset_of`]: the source line rendered
    /// at `row_offset`, given the viewport's first (visible) source line.
    ///
    /// Painters that walk rows rather than spans (the syntax and LSP
    /// style views) use this; the result may exceed the buffer's line
    /// count, which those callers already bound.
    #[must_use]
    pub fn line_at_row_offset(self, start_line: usize, row_offset: u32) -> usize {
        match self.folds {
            Some(map) if !map.is_identity() => {
                map.nth_visible_from(start_line, row_offset as usize)
            }
            _ => start_line + row_offset as usize,
        }
    }
}

// ---------------------------------------------------------------------------
// Trait
// ---------------------------------------------------------------------------

/// Polymorphic interpretive layer attached to a buffer.
///
/// All callbacks receive `&Buffer` rather than `&mut Buffer`: views observe
/// and contribute, they do not mutate the buffer mid-callback. To produce
/// edits, a view returns a transformed [`EditOp`] from `intercept_edit` ---
/// or none of the above, and instead schedules a worker to do expensive work
/// asynchronously (M3+).
///
/// # Threading
///
/// Views run on the main thread. The buffer they attach to lives behind
/// `Rc<RefCell<BufferRegistry>>` and is not sent across threads (workers
/// receive rope snapshots, not buffer or view references). The trait
/// therefore does not require `Send`; this lets views hold types that
/// are not `Send` themselves --- notably `mlua` handles, used by the
/// M6.4 REPL package's `LuaInterceptView` to attach a Lua callback as
/// an intercept-edit chain entry.
pub trait View {
    /// Validate or transform a proposed edit before it reaches the rope.
    ///
    /// Returning the input as-is passes through; returning a different
    /// [`EditOp`] rewrites the edit (used by, e.g., a directory-listing
    /// view that translates filename edits into rename operations).
    /// Returning an error rejects the edit.
    ///
    /// The view receives a [`InterceptContext`] snapshot of the
    /// buffer's identity and shape at the moment the edit began
    /// (T M7.4). Intercept bodies needing live state on another
    /// buffer can re-enter `pmacs.buffer.X` (or the in-process
    /// equivalent); the registry borrow is released for the duration
    /// of this call.
    ///
    /// Default: pass through.
    fn intercept_edit<'a>(
        &mut self,
        _ctx: &InterceptContext,
        op: EditOp<'a>,
    ) -> Result<EditOp<'a>, BufferError> {
        Ok(op)
    }

    /// Called after every successful edit.
    ///
    /// The view updates its derived state. Must be cheap; expensive work
    /// is dispatched to a worker (M3+) and reflected back via a separate
    /// update path.
    ///
    /// Default: no-op.
    fn on_edit(&mut self, _buf: &Buffer, _edit: &Edit) -> Result<(), BufferError> {
        Ok(())
    }

    /// Render the buffer region named by `viewport` into `cells`.
    ///
    /// Pure-ish: same inputs produce the same outputs. The view writes only
    /// inside `viewport.cell_origin .. cell_origin + cell_size`; cells
    /// outside that window are left untouched (composition is the
    /// frontend's job).
    ///
    /// Default: no-op (used by views that participate in `on_edit` but not
    /// in rendering).
    fn render(&mut self, _buf: &Buffer, _viewport: Viewport<'_>, _cells: &mut CellGrid<'_>) {}

    /// Translate a buffer byte position to a display coordinate, if the
    /// view holds a meaningful mapping for that position.
    ///
    /// Default: returns `None` (view has no opinion).
    fn pos_to_display(
        &self,
        _buf: &Buffer,
        _pos: Position,
        _ctx: LayoutCtx,
    ) -> Option<DisplayCoord> {
        None
    }

    /// Translate a display coordinate back to a buffer byte position.
    ///
    /// Default: returns `None`.
    fn display_to_pos(
        &self,
        _buf: &Buffer,
        _coord: DisplayCoord,
        _ctx: LayoutCtx,
    ) -> Option<Position> {
        None
    }

    /// Stable identifier for this view's *kind*. Used by introspection
    /// seams (e.g. `pmacs.window._overlay_kinds()`) to verify that a
    /// specific overlay type actually attached after a wire-up step,
    /// without needing `Any` downcasts. The default `"unknown"` is
    /// fine for views that no test cares about; views that participate
    /// in cross-package wiring (syntax highlight, style overlays)
    /// should override.
    fn kind(&self) -> &'static str {
        "unknown"
    }

    /// Identity of the shared resource this overlay renders, if any
    /// (PR #113 round-6 findings 1 and 3). Two overlay instances
    /// backed by the same store report the same value, which lets a
    /// window attach a resource-backed overlay AT MOST once
    /// ([`crate::window::Window::ensure_overlay`]) and lets disposal
    /// remove every window copy. The default `None` opts out: such
    /// views are never deduplicated or bulk-removed.
    fn overlay_identity(&self) -> Option<usize> {
        None
    }

    /// A copy of this overlay for a freshly split window showing the
    /// same buffer (round-6 finding 1: a same-buffer split starts
    /// with an empty overlay list and fires no switch hook, so
    /// without this the new pane rendered unstyled). `None` (the
    /// default) means the view does not carry across splits;
    /// store-backed render overlays return a clone.
    fn clone_for_split(&self) -> Option<Box<dyn View>> {
        None
    }

    /// Retarget this overlay from `old_uri` to `new_uri` after a
    /// resource rename (dired Stage 2a, §5). Default: no-op — a view
    /// that renders nothing URI-keyed is unaffected.
    ///
    /// Mutates **in place**, so the overlay keeps its position in the
    /// window's composition order. That is the reason this is a trait
    /// hook rather than a remove-and-re-push at the call site: overlays
    /// are an ordered `Vec` merged in sequence, and re-pushing would
    /// move a diagnostic underline to the end of the stack. It is also
    /// how *passive* windows are reached at all — the Lua attach path
    /// (`pmacs.diag._attach_view`) can only touch the active window,
    /// while the sweep that drives this walks every window.
    fn rename_resource(&mut self, _old_uri: &str, _new_uri: &str) {}
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn display_coord_basics() {
        let c = DisplayCoord::new(3, 5);
        assert_eq!(c.row, 3);
        assert_eq!(c.col, 5);
    }

    #[test]
    fn viewport_is_copy() {
        // Compile-time assertion: Viewport must be Copy so the frontend can
        // hand the same descriptor to multiple views without ceremony. Arc 6
        // Stage 2 (Bet B7) keeps this true by borrowing the fold map — a
        // shared reference is itself `Copy`.
        fn assert_copy<T: Copy>() {}
        assert_copy::<Viewport<'_>>();
    }

    #[test]
    fn row_offset_without_folds_is_the_identity_map() {
        let vp = Viewport {
            buffer_start: 0,
            buffer_end: 0,
            cell_origin: CellCoord::new(0, 0),
            cell_size: CellSize::new(10, 10),
            gutter_w: 0,
            folds: None,
            wrap: WrapMode::Truncate,
            view_left: 0,
        };
        assert_eq!(vp.row_offset_of(4, 4), Some(0));
        assert_eq!(vp.row_offset_of(4, 9), Some(5));
        assert_eq!(vp.row_offset_of(4, 3), None, "above the viewport");
    }

    #[test]
    fn row_offset_skips_hidden_lines_and_compacts_rows() {
        // Fold heading line 1, hiding lines 2..=4 (8-byte lines).
        let map =
            VisibleLineMap::build(&[pmacs_protocol::ByteRange { start: 15, end: 39 }], |off| {
                (off / 8) as usize
            });
        let vp = Viewport {
            buffer_start: 0,
            buffer_end: 0,
            cell_origin: CellCoord::new(0, 0),
            cell_size: CellSize::new(10, 10),
            gutter_w: 0,
            folds: Some(&map),
            wrap: WrapMode::Truncate,
            view_left: 0,
        };
        assert_eq!(vp.row_offset_of(0, 1), Some(1), "the head keeps its row");
        assert_eq!(vp.row_offset_of(0, 3), None, "hidden lines have no row");
        assert_eq!(vp.row_offset_of(0, 5), Some(2), "rows below shift up");
    }
}
