//! A minimal inline-viewport terminal, vendored from ratatui 0.30 and extended
//! so the viewport **height can change between frames**.
//!
//! ratatui's own inline viewport bakes its height into the (private) `viewport`
//! and never lets you resize it — which is exactly why the composer couldn't
//! grow. Here the viewport height is just `viewport_area.height`, recomputed
//! each frame by the caller: growing past the screen bottom scrolls earlier
//! rows up into the terminal's native scrollback (so history "scrolls up" as
//! you type), and shrinking gives the rows back. Finalized transcript blocks
//! still land in native scrollback via [`Terminal::insert_before`], ported
//! verbatim from ratatui so its scroll bookkeeping stays faithful.
//!
//! Double-buffer diffing is reused from ratatui (`Buffer::diff` + the `Backend`
//! trait), so this module only owns the viewport geometry, not the rendering.

use std::io::{self, BufWriter, Stdout};

use crossterm::queue;
use crossterm::terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate};
use ratatui::backend::{Backend, ClearType, CrosstermBackend};
use ratatui::buffer::{Buffer, Cell};
use ratatui::layout::{Position, Rect, Size};

/// stdout wrapped so a whole frame flushes as one write. The bare `io::stdout()`
/// is a `LineWriter` that flushes on every `\n`, which would let the scroll step
/// (newlines) reach the terminal before the repaint — a flash on terminals
/// without synchronized-update support. Buffering coalesces the frame.
type Out = CrosstermBackend<BufWriter<Stdout>>;

/// An inline terminal over stdout. The transcript accumulates in the terminal's
/// real scrollback *above* the viewport; only the bottom `viewport_area` rows
/// are ours to paint.
pub struct Terminal {
    backend: Out,
    /// Double buffer; `current` selects the active one, the other holds the
    /// previously-flushed frame so we only write changed cells.
    buffers: [Buffer; 2],
    current: usize,
    /// The bottom region we own. Its height tracks the composer.
    viewport_area: Rect,
    /// Last known full terminal size, as a `Rect` at the origin.
    screen: Rect,
    hidden_cursor: bool,
    /// One-shot request to repaint the whole viewport on the next `draw` (not a
    /// diff), even without a resize. Set by [`Terminal::request_full_repaint`]
    /// for modal overlays (pickers/approval/bootstrap), whose frames replace
    /// differently-shaped content beneath them; a full paint overwrites every
    /// cell so no stale glyphs from the prior frame can show through.
    force_full: bool,
}

impl Terminal {
    /// Create the terminal with an initial viewport `height`, anchoring it at
    /// the current cursor row (reserving rows below, scrolling if near the
    /// bottom) — the same anchoring ratatui's `compute_inline_size` performs.
    pub fn new(height: u16) -> io::Result<Self> {
        let mut backend = CrosstermBackend::new(BufWriter::new(io::stdout()));
        let size = backend.size()?;
        // Probe the cursor row to anchor the viewport. Some terminals/PTYs do
        // not answer the cursor query; fall back to the bottom rather than fail.
        let cursor = backend.get_cursor_position().unwrap_or(Position {
            x: 0,
            y: size.height.saturating_sub(1),
        });
        let area = compute_inline_area(&mut backend, height, size, cursor)?;
        Ok(Self {
            backend,
            buffers: [Buffer::empty(area), Buffer::empty(area)],
            current: 0,
            viewport_area: area,
            screen: Rect::new(0, 0, size.width, size.height),
            hidden_cursor: false,
            force_full: false,
        })
    }

    /// Repaint the entire viewport (every cell, not just the diff) on the next
    /// `draw`. Use when the frame's content footprint shrinks or changes shape
    /// versus the prior frame — e.g. opening a picker/modal over the composer —
    /// so leftover glyphs from the wider previous frame are fully overwritten.
    pub fn request_full_repaint(&mut self) {
        self.force_full = true;
    }

    /// The real terminal size.
    pub fn size(&self) -> io::Result<Size> {
        self.backend.size()
    }

    /// Resize both buffers to `area` and record it as the viewport.
    fn set_viewport_area(&mut self, area: Rect) {
        self.buffers[self.current].resize(area);
        self.buffers[1 - self.current].resize(area);
        self.viewport_area = area;
    }

    /// Clear the viewport region (cursor to viewport top, erase to end of
    /// screen) and force the next flush to repaint everything.
    fn clear_viewport(&mut self) -> io::Result<()> {
        self.backend
            .set_cursor_position(self.viewport_area.as_position())?;
        self.backend.clear_region(ClearType::AfterCursor)?;
        self.buffers[1 - self.current].reset();
        Ok(())
    }

    /// If the real terminal size changed, re-anchor the viewport to the bottom
    /// at its current height and clear (a resize invalidates the whole screen).
    fn autoresize(&mut self) -> io::Result<()> {
        let size = self.backend.size()?;
        if size.width != self.screen.width || size.height != self.screen.height {
            self.screen = Rect::new(0, 0, size.width, size.height);
            let height = self.viewport_area.height.min(size.height);
            let area = Rect::new(0, size.height.saturating_sub(height), size.width, height);
            self.backend.clear_region(ClearType::All)?;
            self.set_viewport_area(area);
            self.buffers[1 - self.current].reset();
        }
        Ok(())
    }

    /// Render one frame with the viewport resized to `viewport_height` rows.
    ///
    /// The viewport grows downward; if that would run past the bottom of the
    /// screen, earlier rows are scrolled up into scrollback and the viewport is
    /// re-anchored to the bottom. `render_fn` is handed the active buffer and
    /// the viewport rect and must paint the whole region (unpainted cells become
    /// blanks, exactly as in ratatui).
    pub fn draw(
        &mut self,
        viewport_height: u16,
        render_fn: impl FnOnce(&mut Buffer, Rect),
    ) -> io::Result<()> {
        self.autoresize()?;

        let screen_h = self.screen.height;
        let mut area = self.viewport_area;
        area.width = self.screen.width;
        area.height = viewport_height.clamp(1, screen_h.max(1));

        // Batch the frame into one synchronized update (DEC mode 2026) where the
        // terminal supports it — belt-and-suspenders on top of the no-clear
        // repaint below.
        queue!(self.backend, BeginSynchronizedUpdate)?;

        // Grow past the screen bottom → scroll earlier rows up into scrollback
        // and re-anchor the viewport to the bottom.
        if area.bottom() > screen_h {
            let overflow = area.bottom() - screen_h;
            self.scroll_up(overflow)?;
            area.y = screen_h - area.height;
        }

        let old_bottom = self.viewport_area.bottom();
        let resized = area != self.viewport_area;
        if resized {
            self.set_viewport_area(area);
        }

        let viewport = self.viewport_area;
        render_fn(&mut self.buffers[self.current], viewport);

        // A resize repaints the whole viewport by necessity; an explicit
        // `request_full_repaint` (consumed here) forces the same full pass for a
        // same-size frame so a modal overwrites every cell beneath it.
        let full = resized || std::mem::take(&mut self.force_full);
        if full {
            // Repaint the whole viewport in a single pass — overwriting the rows
            // it just grew over (and the old composer) cell-by-cell, with **no**
            // blanking clear first. That's the key to flicker-free resize: there
            // is no half-cleared intermediate frame, whether or not the terminal
            // honors synchronized updates. Then erase only the rows a shrink
            // vacated below the viewport.
            self.redraw_viewport()?;
            if old_bottom > area.bottom() {
                self.clear_rows(area.bottom(), old_bottom.min(screen_h))?;
            }
        } else {
            // Steady state (e.g. a keystroke): write only the changed cells.
            self.flush()?;
        }

        if !self.hidden_cursor {
            self.backend.hide_cursor()?;
            self.hidden_cursor = true;
        }
        queue!(self.backend, EndSynchronizedUpdate)?;
        self.backend.flush()?;

        // Swap: the freshly-drawn buffer becomes "previous"; reset the other.
        self.buffers[1 - self.current].reset();
        self.current = 1 - self.current;
        Ok(())
    }

    /// Write the diff between the previous and current buffers to the backend.
    fn flush(&mut self) -> io::Result<()> {
        let updates = self.buffers[1 - self.current].diff(&self.buffers[self.current]);
        self.backend.draw(updates.into_iter())
    }

    /// Write **every** cell of the current viewport in one pass (not a diff), so
    /// a resized viewport repaints over whatever was there with no intervening
    /// clear — and therefore no flicker. Used only on the frames that resize.
    fn redraw_viewport(&mut self) -> io::Result<()> {
        let buf = &self.buffers[self.current];
        let area = buf.area;
        let width = area.width.max(1) as usize;
        let cells: Vec<(u16, u16, &Cell)> = buf
            .content
            .iter()
            .enumerate()
            .map(|(i, cell)| {
                (
                    area.x + (i % width) as u16,
                    area.y + (i / width) as u16,
                    cell,
                )
            })
            .collect();
        self.backend.draw(cells.into_iter())
    }

    /// Erase whole rows `[from, to)` — the rows a shrinking viewport gives up.
    fn clear_rows(&mut self, from: u16, to: u16) -> io::Result<()> {
        for y in from..to {
            self.backend.set_cursor_position(Position::new(0, y))?;
            self.backend.clear_region(ClearType::CurrentLine)?;
        }
        Ok(())
    }

    /// Scroll the whole screen up by `lines` (top rows go into scrollback).
    fn scroll_up(&mut self, lines: u16) -> io::Result<()> {
        if lines > 0 {
            self.backend
                .set_cursor_position(Position::new(0, self.screen.height.saturating_sub(1)))?;
            self.backend.append_lines(lines)?;
        }
        Ok(())
    }

    /// Insert `height` rows of content into scrollback just above the viewport,
    /// scrolling as needed and repositioning the viewport. Ported verbatim from
    /// ratatui 0.30's `insert_before_no_scrolling_regions`.
    pub fn insert_before(
        &mut self,
        height: u16,
        draw_fn: impl FnOnce(&mut Buffer),
    ) -> io::Result<()> {
        let area = Rect::new(0, 0, self.screen.width, height);
        let mut buffer = Buffer::empty(area);
        draw_fn(&mut buffer);
        let mut buffer = buffer.content.as_slice();

        // i32 so additions don't overflow u16 and subtractions can't underflow.
        let mut drawn_height: i32 = self.viewport_area.top().into();
        let mut buffer_height: i32 = height.into();
        let viewport_height: i32 = self.viewport_area.height.into();
        let screen_height: i32 = self.screen.height.into();

        while buffer_height + viewport_height > screen_height {
            let to_draw = buffer_height.min(screen_height);
            let scroll_up = 0.max(drawn_height + to_draw - screen_height);
            self.scroll_up(scroll_up as u16)?;
            buffer = self.draw_lines((drawn_height - scroll_up) as u16, to_draw as u16, buffer)?;
            drawn_height += to_draw - scroll_up;
            buffer_height -= to_draw;
        }

        let scroll_up = 0.max(drawn_height + buffer_height + viewport_height - screen_height);
        self.scroll_up(scroll_up as u16)?;
        self.draw_lines(
            (drawn_height - scroll_up) as u16,
            buffer_height as u16,
            buffer,
        )?;
        drawn_height += buffer_height - scroll_up;

        self.set_viewport_area(Rect {
            y: drawn_height as u16,
            ..self.viewport_area
        });
        self.clear_viewport()?;
        Ok(())
    }

    /// Draw `lines_to_draw` rows from `cells` at the given vertical offset,
    /// returning the unconsumed remainder. Ported from ratatui 0.30.
    fn draw_lines<'a>(
        &mut self,
        y_offset: u16,
        lines_to_draw: u16,
        cells: &'a [Cell],
    ) -> io::Result<&'a [Cell]> {
        let width: usize = self.screen.width.into();
        let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
        if lines_to_draw > 0 {
            let iter = to_draw
                .iter()
                .enumerate()
                .map(|(i, c)| ((i % width) as u16, y_offset + (i / width) as u16, c));
            self.backend.draw(iter)?;
            self.backend.flush()?;
        }
        Ok(remainder)
    }

    /// Reclaim the terminal after an external program (e.g. `$EDITOR`) ran:
    /// wipe the physical viewport region, reset both buffers, and force the next
    /// `draw` to re-hide the cursor and full-repaint. Keeps the viewport anchored
    /// where it was (a well-behaved editor restores the screen on exit), so the
    /// composer redraws in place — nothing scrolls into scrollback. Without the
    /// wipe the next cell-diff would skip space cells and leave the editor's
    /// glyphs showing through; scrollback above the viewport is untouched.
    pub fn reclaim(&mut self) -> io::Result<()> {
        self.clear_viewport()?;
        self.buffers[self.current].reset();
        // The editor may have shown the cursor; re-hide it on the next draw.
        self.hidden_cursor = false;
        self.backend.flush()
    }

    /// Clear the visible screen so the viewport re-anchors at the **top** of the
    /// screen (Ctrl-L), preserving native scrollback. The rows currently above
    /// the viewport are scrolled up into the scrollback buffer (not erased), the
    /// viewport region is wiped, and it's re-anchored to row 0 so the composer
    /// redraws at the top on the next frame.
    pub fn clear_screen(&mut self) -> io::Result<()> {
        // Wipe the viewport region first so its current contents (the composer /
        // live tail) aren't duplicated into scrollback by the scroll below.
        self.clear_viewport()?;
        // Scroll the rows above the viewport up into scrollback. `scroll_up`
        // appends that many lines at the bottom, pushing the top rows off-screen
        // (and into the scrollback buffer, exactly as `insert_before` relies on).
        self.scroll_up(self.viewport_area.top())?;
        // Re-anchor the viewport to the top of the screen and clear everything
        // below it; the next `draw` repaints the composer there.
        let height = self.viewport_area.height.min(self.screen.height.max(1));
        self.set_viewport_area(Rect::new(0, 0, self.screen.width, height));
        self.clear_viewport()?;
        self.buffers[self.current].reset();
        self.hidden_cursor = false;
        self.backend.flush()
    }

    /// Wipe the visible screen **and** the scrollback buffer, then re-anchor the
    /// viewport at the top of the screen. Unlike [`clear_screen`] (Ctrl-L), which
    /// *preserves* history by scrolling it up, this discards scrollback outright
    /// (`\e[3J`) so the caller can reprint the transcript from scratch — used by
    /// the resize reflow, which re-wraps the whole transcript to the new width
    /// (committed scrollback can't be re-wrapped in place, so it's purge + reprint
    /// or nothing). Leaves both buffers reset so the next `draw` full-repaints.
    pub fn clear_all(&mut self) -> io::Result<()> {
        use crossterm::cursor::MoveTo;
        use crossterm::terminal::{Clear, ClearType as CtClearType};
        queue!(
            self.backend,
            MoveTo(0, 0),
            Clear(CtClearType::All),   // \e[2J — the visible screen
            Clear(CtClearType::Purge), // \e[3J — the scrollback buffer
        )?;
        let height = self.viewport_area.height.min(self.screen.height.max(1));
        self.set_viewport_area(Rect::new(0, 0, self.screen.width, height));
        // `set_viewport_area` only refills a buffer whose cell *count* changed; a
        // same-size move past the same width keeps stale cells, so reset both.
        self.buffers[0].reset();
        self.buffers[1].reset();
        self.hidden_cursor = false;
        self.backend.flush()
    }

    /// Restore the cursor for shell hand-back: show it and drop onto a fresh
    /// line below the viewport so the prompt isn't glued to our last row.
    pub fn restore(&mut self) -> io::Result<()> {
        self.insert_before(1, |_| {})?;
        if self.hidden_cursor {
            self.backend.show_cursor()?;
            self.hidden_cursor = false;
        }
        self.backend.flush()
    }
}

/// Anchor an inline viewport of `height` rows at `cursor`, reserving the rows
/// below it (scrolling if the cursor is near the bottom). Mirrors ratatui
/// 0.30's `compute_inline_size` with `offset_in_previous_viewport == 0`.
fn compute_inline_area(
    backend: &mut Out,
    height: u16,
    size: Size,
    cursor: Position,
) -> io::Result<Rect> {
    let mut row = cursor.y;
    let max_height = size.height.min(height);
    let lines_after_cursor = height.saturating_sub(1);
    backend.append_lines(lines_after_cursor)?;
    let available_lines = size.height.saturating_sub(row).saturating_sub(1);
    let missing_lines = lines_after_cursor.saturating_sub(available_lines);
    if missing_lines > 0 {
        row = row.saturating_sub(missing_lines);
    }
    Ok(Rect {
        x: 0,
        y: row,
        width: size.width,
        height: max_height,
    })
}
