//! Text selection and clipboard support.
//!
//! Selection lifecycle:
//!
//!   MouseDown in pane → Anchor recorded (no visual yet)
//!   MouseDrag         → Selection becomes active, cells highlighted
//!   MouseUp           → Selection finalized; optionally copied by the caller
//!   Next click / key  → A retained selection is cleared
//!
//! Double-click selects a word; the caller decides whether to copy it immediately.
//!
//! Rows are stored in screen-buffer coordinates instead of viewport-relative
//! coordinates. That keeps selection stable while the pane scrolls.

use ratatui::layout::Rect;
use std::{ffi::OsStr, io::Write};

use crate::{layout::PaneId, pane::ScrollMetrics};

/// Current phase of a selection.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Phase {
    /// Mouse is down but hasn't moved yet. If released without
    /// moving, this was just a click — no selection created.
    Anchored,
    /// Mouse has moved from the anchor point. Cells are being highlighted.
    Dragging,
    /// Mouse released after dragging. Selection is visible and the gesture is
    /// complete. Clipboard policy is owned by the caller.
    Done,
}

/// A text selection within a terminal pane.
#[derive(Debug, Clone)]
pub struct Selection {
    /// Which pane the selection belongs to.
    pub pane_id: PaneId,
    /// Anchor position in screen-buffer coordinates (row, col).
    anchor: (u32, u16),
    /// Current/final position in screen-buffer coordinates (row, col).
    cursor: (u32, u16),
    /// Selection phase.
    phase: Phase,
}

impl Selection {
    /// Start a potential selection. This records the anchor but doesn't
    /// make anything visible yet — the user might just be clicking.
    pub fn anchor(
        pane_id: PaneId,
        viewport_row: u16,
        col: u16,
        metrics: Option<ScrollMetrics>,
    ) -> Self {
        let anchor = (absolute_row_for_viewport_row(viewport_row, metrics), col);
        Self {
            pane_id,
            anchor,
            cursor: anchor,
            phase: Phase::Anchored,
        }
    }

    /// Create an active selection from an explicit viewport-row range.
    pub(crate) fn range(
        pane_id: PaneId,
        viewport_row: u16,
        start_col: u16,
        end_col: u16,
        metrics: Option<ScrollMetrics>,
    ) -> Self {
        let row = absolute_row_for_viewport_row(viewport_row, metrics);
        Self {
            pane_id,
            anchor: (row, start_col),
            cursor: (row, end_col),
            phase: Phase::Dragging,
        }
    }

    pub(crate) fn line_range(
        pane_id: PaneId,
        anchor_row: u32,
        cursor_row: u32,
        end_col: u16,
    ) -> Self {
        let (anchor_col, cursor_col) = if anchor_row <= cursor_row {
            (0, end_col)
        } else {
            (end_col, 0)
        };
        Self {
            pane_id,
            anchor: (anchor_row, anchor_col),
            cursor: (cursor_row, cursor_col),
            phase: Phase::Dragging,
        }
    }

    pub(crate) fn absolute_row_for_viewport(
        viewport_row: u16,
        metrics: Option<ScrollMetrics>,
    ) -> u32 {
        absolute_row_for_viewport_row(viewport_row, metrics)
    }

    /// Convert the anchor's absolute row and pane-relative column back to
    /// screen coordinates. Adds the pane origin before clamping so the
    /// returned (screen_row, screen_col) can be compared directly against
    /// mouse screen positions.
    pub fn anchor_screen_pos(
        &self,
        pane_inner: Rect,
        metrics: Option<ScrollMetrics>,
    ) -> (u16, u16) {
        let viewport_row = viewport_row_for_absolute_row(self.anchor.0, metrics);
        // Convert pane-relative to screen coordinates, then clamp.
        let row = (viewport_row.saturating_add(pane_inner.y)).clamp(
            pane_inner.y,
            pane_inner.y + pane_inner.height.saturating_sub(1),
        );
        let col = (self.anchor.1.saturating_add(pane_inner.x)).clamp(
            pane_inner.x,
            pane_inner.x + pane_inner.width.saturating_sub(1),
        );
        (row, col)
    }

    /// Extend the selection as the mouse drags. Activates highlighting
    /// once the cursor moves to a different cell than the anchor.
    /// Screen coordinates are clamped to the pane boundary.
    pub fn drag(
        &mut self,
        screen_col: u16,
        screen_row: u16,
        pane_inner: Rect,
        metrics: Option<ScrollMetrics>,
    ) {
        let (viewport_row, col) = clamp_to_pane(screen_col, screen_row, pane_inner);
        self.cursor = (absolute_row_for_viewport_row(viewport_row, metrics), col);
        if self.cursor != self.anchor {
            self.phase = Phase::Dragging;
        }
    }

    /// Finalize the selection. Returns the selected range if the user
    /// actually dragged (not just clicked). Returns None for plain clicks.
    pub fn finish(&mut self) -> bool {
        if self.phase == Phase::Dragging {
            self.phase = Phase::Done;
            true
        } else {
            false
        }
    }

    /// Whether this selection should be rendered (highlight visible).
    pub fn is_visible(&self) -> bool {
        self.phase == Phase::Dragging || self.phase == Phase::Done
    }

    /// Whether this selection was already finalized.
    pub fn is_finalized(&self) -> bool {
        self.phase == Phase::Done
    }

    /// Whether the user just clicked without dragging (not a selection).
    pub fn was_just_click(&self) -> bool {
        self.phase == Phase::Anchored
    }

    /// Whether the user just clicked without dragging (not a selection).
    pub fn is_just_click(&self) -> bool {
        self.phase == Phase::Anchored
    }

    /// Force the selection into Dragging phase, used when the mouse
    /// has moved off the anchor cell but drag() couldn't transition
    /// because the cursor was clamped to the same cell as the anchor.
    pub fn force_dragging(&mut self) {
        if self.phase == Phase::Anchored {
            self.phase = Phase::Dragging;
        }
    }

    /// Whether the pointer is still down and the selection can keep extending.
    pub fn is_in_progress(&self) -> bool {
        matches!(self.phase, Phase::Anchored | Phase::Dragging)
    }

    /// Whether the user is actively dragging (cursor moved from anchor).
    pub fn is_dragging(&self) -> bool {
        self.phase == Phase::Dragging
    }

    /// Returns (start, end) in reading order (top-left to bottom-right).
    fn ordered(&self) -> ((u32, u16), (u32, u16)) {
        let (ar, ac) = self.anchor;
        let (cr, cc) = self.cursor;
        if ar < cr || (ar == cr && ac <= cc) {
            ((ar, ac), (cr, cc))
        } else {
            ((cr, cc), (ar, ac))
        }
    }

    pub(crate) fn ordered_cells(&self) -> ((u32, u16), (u32, u16)) {
        self.ordered()
    }

    /// Check whether a pane-relative cell (row, col) is inside the selection.
    pub fn contains(&self, viewport_row: u16, col: u16, metrics: Option<ScrollMetrics>) -> bool {
        if !self.is_visible() {
            return false;
        }
        let row = absolute_row_for_viewport_row(viewport_row, metrics);
        let ((sr, sc), (er, ec)) = self.ordered();
        if row < sr || row > er {
            return false;
        }
        if sr == er {
            col >= sc && col <= ec
        } else if row == sr {
            col >= sc
        } else if row == er {
            col <= ec
        } else {
            true
        }
    }
}

fn viewport_top_row(metrics: Option<ScrollMetrics>) -> u32 {
    metrics
        .map(|metrics| {
            metrics
                .max_offset_from_bottom
                .saturating_sub(metrics.offset_from_bottom)
        })
        .unwrap_or(0) as u32
}

fn absolute_row_for_viewport_row(viewport_row: u16, metrics: Option<ScrollMetrics>) -> u32 {
    viewport_top_row(metrics) + u32::from(viewport_row)
}

fn viewport_row_for_absolute_row(absolute_row: u32, metrics: Option<ScrollMetrics>) -> u16 {
    absolute_row
        .saturating_sub(viewport_top_row(metrics))
        .try_into()
        .unwrap_or(0)
}

fn clamp_to_pane(screen_col: u16, screen_row: u16, pane_inner: Rect) -> (u16, u16) {
    let clamped_col = screen_col.clamp(
        pane_inner.x,
        pane_inner.x + pane_inner.width.saturating_sub(1),
    );
    let clamped_row = screen_row.clamp(
        pane_inner.y,
        pane_inner.y + pane_inner.height.saturating_sub(1),
    );
    (clamped_row - pane_inner.y, clamped_col - pane_inner.x)
}

fn osc52_sequence(bytes: &[u8]) -> String {
    use base64::Engine;
    let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
    format!("\x1b]52;c;{encoded}\x07")
}

fn contains_wsl_marker(value: &str) -> bool {
    let lower = value.to_ascii_lowercase();
    lower.contains("microsoft") || lower.contains("wsl2") || lower.contains("-wsl")
}

fn is_wsl_for_env(
    os_release: Option<&str>,
    proc_version: Option<&str>,
    wsl_distro_name: Option<&OsStr>,
    wsl_interop: Option<&OsStr>,
    runtime_marker_exists: bool,
) -> bool {
    wsl_distro_name.is_some()
        || wsl_interop.is_some()
        || os_release.is_some_and(contains_wsl_marker)
        || proc_version.is_some_and(contains_wsl_marker)
        || runtime_marker_exists
}

fn is_wsl() -> bool {
    let os_release = std::fs::read_to_string("/proc/sys/kernel/osrelease").ok();
    let proc_version = std::fs::read_to_string("/proc/version").ok();
    is_wsl_for_env(
        os_release.as_deref(),
        proc_version.as_deref(),
        std::env::var_os("WSL_DISTRO_NAME").as_deref(),
        std::env::var_os("WSL_INTEROP").as_deref(),
        std::path::Path::new("/run/WSL").exists()
            || std::path::Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists(),
    )
}

fn should_prefer_osc52_for_env(
    ssh_connection: Option<&OsStr>,
    ssh_tty: Option<&OsStr>,
    vscode_ipc_hook_cli: Option<&OsStr>,
    wsl: bool,
) -> bool {
    ssh_connection.is_some() || ssh_tty.is_some() || vscode_ipc_hook_cli.is_some() || wsl
}

fn should_prefer_osc52() -> bool {
    should_prefer_osc52_for_env(
        std::env::var_os("SSH_CONNECTION").as_deref(),
        std::env::var_os("SSH_TTY").as_deref(),
        std::env::var_os("VSCODE_IPC_HOOK_CLI").as_deref(),
        is_wsl(),
    )
}

/// Write clipboard bytes to the system clipboard via native platform tools or OSC 52.
///
/// OSC 52 format: `ESC ] 52 ; c ; <base64> BEL`
///
/// Some terminals still only honor BEL-terminated OSC 52 writes, so herdr
/// emits BEL here even though ST works in newer emulators.
pub fn write_osc52_bytes(bytes: &[u8]) {
    if !should_prefer_osc52() && crate::platform::write_clipboard(bytes) {
        return;
    }

    let sequence = osc52_sequence(bytes);
    let _ = std::io::stdout().write_all(sequence.as_bytes());
    let _ = std::io::stdout().flush();
}

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

    fn make_sel(sr: u32, sc: u16, er: u32, ec: u16) -> Selection {
        let mut sel = Selection::anchor(PaneId::from_raw(0), sr as u16, sc, None);
        sel.anchor = (sr, sc);
        sel.cursor = (er, ec);
        sel.phase = Phase::Dragging;
        sel
    }

    #[test]
    fn osc52_sequence_uses_bel_terminator() {
        assert_eq!(osc52_sequence(b"hello"), "\x1b]52;c;aGVsbG8=\x07");
    }

    #[test]
    fn ssh_sessions_prefer_osc52() {
        assert!(should_prefer_osc52_for_env(
            Some(OsStr::new("1 2 3 4")),
            None,
            None,
            false
        ));
        assert!(should_prefer_osc52_for_env(
            None,
            Some(OsStr::new("/dev/ttys001")),
            None,
            false
        ));
        assert!(!should_prefer_osc52_for_env(None, None, None, false));
    }

    #[test]
    fn wsl_sessions_prefer_osc52() {
        assert!(should_prefer_osc52_for_env(None, None, None, true));
    }

    #[test]
    fn vscode_remote_sessions_prefer_osc52() {
        assert!(should_prefer_osc52_for_env(
            None,
            None,
            Some(OsStr::new("/tmp/vscode-remote-cli.sock")),
            false
        ));
    }

    #[test]
    fn wsl_detection_uses_env_vars() {
        assert!(is_wsl_for_env(
            None,
            None,
            Some(OsStr::new("Ubuntu")),
            None,
            false
        ));
        assert!(is_wsl_for_env(
            None,
            None,
            None,
            Some(OsStr::new("/run/WSL/123_interop")),
            false
        ));
    }

    #[test]
    fn wsl_detection_uses_kernel_markers() {
        assert!(is_wsl_for_env(
            Some("5.15.167.4-microsoft-standard-WSL2"),
            None,
            None,
            None,
            false
        ));
        assert!(is_wsl_for_env(
            None,
            Some("Linux version 5.15.167.4-microsoft-standard-WSL2"),
            None,
            None,
            false
        ));
    }

    #[test]
    fn wsl_detection_ignores_non_wsl_kernel_strings() {
        assert!(!contains_wsl_marker("notwsl-kernel"));
        assert!(!is_wsl_for_env(
            Some("6.8.0-31-generic"),
            Some("Linux version 6.8.0-31-generic"),
            None,
            None,
            false
        ));
    }

    #[test]
    fn wsl_detection_uses_wsl_runtime_markers() {
        assert!(is_wsl_for_env(None, None, None, None, true));
        assert!(!is_wsl_for_env(None, None, None, None, false));
    }

    #[test]
    fn ordering_forward() {
        let sel = make_sel(2, 5, 4, 10);
        assert_eq!(sel.ordered(), ((2, 5), (4, 10)));
    }

    #[test]
    fn ordering_backward() {
        let sel = make_sel(4, 10, 2, 5);
        assert_eq!(sel.ordered(), ((2, 5), (4, 10)));
    }

    #[test]
    fn single_line_contains() {
        let sel = make_sel(2, 5, 2, 15);
        assert!(!sel.contains(2, 4, None));
        assert!(sel.contains(2, 5, None));
        assert!(sel.contains(2, 10, None));
        assert!(sel.contains(2, 15, None));
        assert!(!sel.contains(2, 16, None));
        assert!(!sel.contains(1, 10, None));
        assert!(!sel.contains(3, 10, None));
    }

    #[test]
    fn multi_line_contains() {
        let sel = make_sel(2, 5, 4, 10);
        assert!(!sel.contains(2, 4, None));
        assert!(sel.contains(2, 5, None));
        assert!(sel.contains(2, 79, None));
        assert!(sel.contains(3, 0, None));
        assert!(sel.contains(3, 79, None));
        assert!(sel.contains(4, 0, None));
        assert!(sel.contains(4, 10, None));
        assert!(!sel.contains(4, 11, None));
    }

    #[test]
    fn anchored_not_visible() {
        let sel = Selection::anchor(PaneId::from_raw(0), 5, 10, None);
        assert!(!sel.is_visible());
        assert!(!sel.contains(5, 10, None));
    }

    #[test]
    fn click_without_drag() {
        let mut sel = Selection::anchor(PaneId::from_raw(0), 5, 10, None);
        assert!(sel.was_just_click());
        let copied = sel.finish();
        assert!(!copied);
    }

    #[test]
    fn drag_then_finish() {
        let mut sel = Selection::anchor(PaneId::from_raw(0), 5, 10, None);
        sel.drag(20, 7, Rect::new(10, 5, 80, 24), None);
        assert!(sel.is_visible());
        assert!(!sel.was_just_click());
        let copied = sel.finish();
        assert!(copied);
    }

    #[test]
    fn drag_uses_buffer_rows_when_scrolled() {
        let mut sel = Selection::anchor(
            PaneId::from_raw(0),
            0,
            10,
            Some(ScrollMetrics {
                offset_from_bottom: 1,
                max_offset_from_bottom: 10,
                viewport_rows: 4,
            }),
        );

        sel.drag(
            10,
            5,
            Rect::new(10, 5, 80, 4),
            Some(ScrollMetrics {
                offset_from_bottom: 2,
                max_offset_from_bottom: 10,
                viewport_rows: 4,
            }),
        );

        assert_eq!(sel.ordered_cells(), ((8, 0), (9, 10)));
    }

    #[test]
    fn contains_tracks_current_viewport_after_scroll() {
        let sel = make_sel(8, 2, 10, 4);
        let metrics = Some(ScrollMetrics {
            offset_from_bottom: 2,
            max_offset_from_bottom: 10,
            viewport_rows: 4,
        });

        assert!(sel.contains(0, 2, metrics));
        assert!(sel.contains(1, 40, metrics));
        assert!(sel.contains(2, 4, metrics));
        assert!(!sel.contains(3, 4, metrics));
    }

    #[test]
    fn clamp_to_pane_bounds() {
        let (row, col) = clamp_to_pane(200, 100, Rect::new(10, 5, 80, 24));
        assert_eq!(row, 23);
        assert_eq!(col, 79);

        let (row, col) = clamp_to_pane(0, 0, Rect::new(10, 5, 80, 24));
        assert_eq!(row, 0);
        assert_eq!(col, 0);
    }

    #[test]
    fn anchor_screen_pos_adds_pane_origin() {
        // Pane offset by sidebar (x=10) and tab bar (y=5).
        // Anchor at viewport_row=3, col=5 (pane-relative).
        let sel = Selection::anchor(PaneId::from_raw(0), 3, 5, None);
        let pane_inner = Rect::new(10, 5, 80, 24);
        let (row, col) = sel.anchor_screen_pos(pane_inner, None);
        // Screen row = 3 + 5 = 8, screen col = 5 + 10 = 15
        assert_eq!(row, 8);
        assert_eq!(col, 15);
    }

    #[test]
    fn anchor_screen_pos_same_cell_as_mouse_with_offset() {
        // When the pane has a non-zero origin, anchor and mouse on the same
        // screen cell must compare equal — no false drag detection.
        let pane_inner = Rect::new(10, 5, 80, 24);
        // Mouse clicked at screen (15, 8) → anchor stored as (viewport_row=3, col=5)
        let sel = Selection::anchor(PaneId::from_raw(0), 3, 5, None);
        let (ar, ac) = sel.anchor_screen_pos(pane_inner, None);
        // Screen position of the anchor must match the mouse position
        assert_eq!((ar, ac), (8, 15));
    }
}
