// completion.rs --- T M4.7 LSP-backed completion: store + popup view.

//! Completion state and the [`CompletionView`] that renders it as a
//! popup.
//!
//! Per spec §M4.7: when the user types a completion trigger character
//! (or invokes completion explicitly), pmacs sends a
//! `textDocument/completion` request to the LSP. The response arrives
//! later --- the [`CompletionStore`] keeps the most-recent items per
//! server, and [`CompletionView`] renders them as a list popup.
//!
//! # Why a separate module
//!
//! Mirrors [`crate::diag`]: a shared store with a writer (the LSP
//! manager) and many readers (every active popup view). The
//! `Send` bound on `View` forces `Arc<Mutex<...>>`.
//!
//! # Trigger characters
//!
//! [`CompletionTriggers::should_fire`] inspects a character against
//! the trigger set negotiated during `initialize` (e.g. `.` for
//! method access, `::` for paths, `<` for generics). Identifier
//! characters are *not* triggers --- those are explicit-invocation
//! only --- so the editor decides whether to fire on identifier typing
//! based on its own UI policy.

use std::collections::HashMap;
use std::sync::{Arc, Mutex};

use serde_json::Value;
use unicode_width::UnicodeWidthChar;

use crate::buffer::Buffer;
use crate::cell::{Cell, CellCoord, CellGrid, Color, Glyph, Style};
use crate::view::{View, Viewport};

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/// LSP `CompletionItemKind`. Mirrors the LSP enum with the same
/// numeric values; `Text` is the safe fallback for unknown numbers
/// (the LSP spec says clients must accept extended kinds gracefully).
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CompletionItemKind {
    /// Plain text snippet.
    Text = 1,
    /// Method on an object.
    Method = 2,
    /// Free function.
    Function = 3,
    /// Constructor / factory.
    Constructor = 4,
    /// Field on a struct.
    Field = 5,
    /// Local or global variable.
    Variable = 6,
    /// Class / type.
    Class = 7,
    /// Interface / trait.
    Interface = 8,
    /// Module / namespace.
    Module = 9,
    /// Property accessor.
    Property = 10,
    /// Numeric / language unit.
    Unit = 11,
    /// Constant value literal.
    Value = 12,
    /// Enum.
    Enum = 13,
    /// Keyword.
    Keyword = 14,
    /// Snippet template.
    Snippet = 15,
    /// Color literal.
    Color = 16,
    /// File path.
    File = 17,
    /// Cross-reference.
    Reference = 18,
    /// Folder path.
    Folder = 19,
    /// Enum member.
    EnumMember = 20,
    /// Constant binding.
    Constant = 21,
    /// Struct.
    Struct = 22,
    /// Event.
    Event = 23,
    /// Operator.
    Operator = 24,
    /// Type parameter.
    TypeParameter = 25,
}

impl CompletionItemKind {
    /// Short single-letter glyph for the popup's left gutter.
    #[must_use]
    pub fn glyph(self) -> char {
        match self {
            Self::Method | Self::Function | Self::Constructor => 'f',
            Self::Field | Self::Property => 'p',
            Self::Variable | Self::Constant => 'v',
            Self::Class | Self::Struct => 'C',
            Self::Interface => 'I',
            Self::Module => 'M',
            Self::Enum | Self::EnumMember => 'E',
            Self::Keyword => 'k',
            Self::Snippet => 's',
            Self::TypeParameter => 't',
            Self::File | Self::Folder => '/',
            _ => '.',
        }
    }

    #[allow(
        clippy::match_same_arms,
        reason = "kept explicit code 1 arm for documentation; Text is also the safe fallback"
    )]
    fn from_lsp_value(v: Option<&Value>) -> Self {
        match v.and_then(Value::as_i64) {
            Some(1) => Self::Text,
            Some(2) => Self::Method,
            Some(3) => Self::Function,
            Some(4) => Self::Constructor,
            Some(5) => Self::Field,
            Some(6) => Self::Variable,
            Some(7) => Self::Class,
            Some(8) => Self::Interface,
            Some(9) => Self::Module,
            Some(10) => Self::Property,
            Some(11) => Self::Unit,
            Some(12) => Self::Value,
            Some(13) => Self::Enum,
            Some(14) => Self::Keyword,
            Some(15) => Self::Snippet,
            Some(16) => Self::Color,
            Some(17) => Self::File,
            Some(18) => Self::Reference,
            Some(19) => Self::Folder,
            Some(20) => Self::EnumMember,
            Some(21) => Self::Constant,
            Some(22) => Self::Struct,
            Some(23) => Self::Event,
            Some(24) => Self::Operator,
            Some(25) => Self::TypeParameter,
            _ => Self::Text,
        }
    }
}

/// One completion candidate, parsed from `textDocument/completion`.
#[derive(Clone, Debug)]
pub struct CompletionItem {
    /// Display label (always present).
    pub label: String,
    /// Item kind.
    pub kind: CompletionItemKind,
    /// Optional one-line detail (e.g. a function signature).
    pub detail: Option<String>,
    /// Optional documentation; we collapse `MarkupContent` to its
    /// text body and store a plain string.
    pub documentation: Option<String>,
    /// Text inserted on accept; falls back to `label` if absent.
    pub insert_text: Option<String>,
    /// `sortText` LSP field; if absent the receiver may sort by `label`.
    pub sort_text: Option<String>,
    /// `filterText` LSP field; receiver may filter by typed prefix.
    pub filter_text: Option<String>,
}

impl CompletionItem {
    /// Parse a single LSP `CompletionItem` JSON object.
    #[must_use]
    pub fn from_lsp_value(v: &Value) -> Option<Self> {
        let label = v.get("label")?.as_str()?.to_owned();
        let kind = CompletionItemKind::from_lsp_value(v.get("kind"));
        let detail = v.get("detail").and_then(Value::as_str).map(str::to_owned);
        let documentation = v.get("documentation").and_then(extract_markup_text);
        let insert_text = v
            .get("insertText")
            .and_then(Value::as_str)
            .map(str::to_owned);
        let sort_text = v.get("sortText").and_then(Value::as_str).map(str::to_owned);
        let filter_text = v
            .get("filterText")
            .and_then(Value::as_str)
            .map(str::to_owned);
        Some(Self {
            label,
            kind,
            detail,
            documentation,
            insert_text,
            sort_text,
            filter_text,
        })
    }

    /// What gets typed into the buffer when the user accepts this
    /// item. Falls back to `label` per the LSP spec.
    #[must_use]
    pub fn effective_insert_text(&self) -> &str {
        self.insert_text.as_deref().unwrap_or(&self.label)
    }
}

/// Decode an LSP `MarkupContent` or legacy `MarkedString` into
/// plain text. Both forms surface as the same in pmacs (we don't
/// render markdown today).
fn extract_markup_text(v: &Value) -> Option<String> {
    if let Some(s) = v.as_str() {
        return Some(s.to_owned());
    }
    if let Some(obj) = v.as_object()
        && let Some(s) = obj.get("value").and_then(Value::as_str)
    {
        return Some(s.to_owned());
    }
    None
}

/// Parsed `textDocument/completion` response. Either the
/// `CompletionList` shape or the bare `CompletionItem[]` shape.
#[derive(Clone, Debug, Default)]
pub struct CompletionResponse {
    /// All items in the response.
    pub items: Vec<CompletionItem>,
    /// `isIncomplete` from the response (false if the server returned
    /// a bare item list).
    pub is_incomplete: bool,
}

impl CompletionResponse {
    /// Parse the LSP response for a `textDocument/completion` request.
    /// Accepts both the `CompletionList` and `CompletionItem[]` shapes.
    /// Returns an empty response (`items` empty, `is_incomplete` false)
    /// for `null` or `Value::Null`.
    #[must_use]
    pub fn from_lsp_value(v: &Value) -> Self {
        if v.is_null() {
            return Self::default();
        }
        if let Some(arr) = v.as_array() {
            return Self {
                items: arr
                    .iter()
                    .filter_map(CompletionItem::from_lsp_value)
                    .collect(),
                is_incomplete: false,
            };
        }
        let is_incomplete = v
            .get("isIncomplete")
            .and_then(Value::as_bool)
            .unwrap_or(false);
        let items = v
            .get("items")
            .and_then(Value::as_array)
            .map(|arr| {
                arr.iter()
                    .filter_map(CompletionItem::from_lsp_value)
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();
        Self {
            items,
            is_incomplete,
        }
    }
}

/// Per-server, per-uri completion state. Keyed by `(server_id, uri)`
/// because the same buffer might in principle be served by multiple
/// LSPs (e.g. a Python file with both pyright and ruff-lsp); each
/// keeps its own lane.
#[derive(Default)]
pub struct CompletionStore {
    by_key: HashMap<CompletionKey, CompletionResponse>,
    /// Currently-selected index per key. Maintained outside
    /// `CompletionResponse` so `set` can preserve / clamp it.
    selection: HashMap<CompletionKey, usize>,
}

/// Composite key into the completion store. `server` is the
/// stringified [`crate::lsp::LspServerId`]; the URI matches the
/// document the completion was requested for.
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct CompletionKey {
    /// LSP server id (decimal).
    pub server: String,
    /// Document URI the completion targets.
    pub uri: String,
}

impl CompletionKey {
    /// Construct a key.
    #[must_use]
    pub fn new(server: impl Into<String>, uri: impl Into<String>) -> Self {
        Self {
            server: server.into(),
            uri: uri.into(),
        }
    }
}

impl CompletionStore {
    /// Empty store.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Replace the response at `key`. Resets the selection to 0
    /// (or none if there are no items).
    pub fn set(&mut self, key: CompletionKey, resp: CompletionResponse) {
        if resp.items.is_empty() {
            self.by_key.remove(&key);
            self.selection.remove(&key);
            return;
        }
        self.selection.insert(key.clone(), 0);
        self.by_key.insert(key, resp);
    }

    /// Drop the response at `key`.
    pub fn clear(&mut self, key: &CompletionKey) {
        self.by_key.remove(key);
        self.selection.remove(key);
    }

    /// Look up the response at `key`.
    #[must_use]
    pub fn get(&self, key: &CompletionKey) -> Option<&CompletionResponse> {
        self.by_key.get(key)
    }

    /// Items at `key`, or empty.
    #[must_use]
    pub fn items(&self, key: &CompletionKey) -> &[CompletionItem] {
        self.by_key.get(key).map_or(&[], |r| &r.items)
    }

    /// Currently selected index at `key`, or 0 if absent.
    #[must_use]
    pub fn selected(&self, key: &CompletionKey) -> usize {
        self.selection.get(key).copied().unwrap_or(0)
    }

    /// Move the selection. Saturates at `[0, items.len() - 1]`.
    pub fn select(&mut self, key: &CompletionKey, idx: usize) {
        let len = self.by_key.get(key).map_or(0, |r| r.items.len());
        if len == 0 {
            return;
        }
        let clamped = idx.min(len - 1);
        self.selection.insert(key.clone(), clamped);
    }

    /// Move selection by `delta`, wrapping at the ends.
    #[allow(
        clippy::cast_possible_wrap,
        reason = "selection indices are bounded by Vec::len() which fits in isize on every supported target"
    )]
    pub fn move_selection(&mut self, key: &CompletionKey, delta: isize) {
        let len = self.by_key.get(key).map_or(0, |r| r.items.len());
        if len == 0 {
            return;
        }
        let cur = self.selected(key) as isize;
        let len_i = len as isize;
        let mut next = (cur + delta) % len_i;
        if next < 0 {
            next += len_i;
        }
        self.selection.insert(key.clone(), next as usize);
    }

    /// True if the response at `key` was flagged `isIncomplete`.
    #[must_use]
    pub fn is_incomplete(&self, key: &CompletionKey) -> bool {
        self.by_key.get(key).is_some_and(|r| r.is_incomplete)
    }

    /// All keys currently in the store.
    pub fn keys(&self) -> impl Iterator<Item = &CompletionKey> {
        self.by_key.keys()
    }
}

/// Cheaply-cloneable shared handle.
pub type SharedCompletionStore = Arc<Mutex<CompletionStore>>;

/// Build a fresh shared store.
#[must_use]
pub fn make_shared_store() -> SharedCompletionStore {
    Arc::new(Mutex::new(CompletionStore::new()))
}

// ---------------------------------------------------------------------------
// Trigger machinery
// ---------------------------------------------------------------------------

/// Trigger character set negotiated with the LSP. Built from the
/// server's `completionProvider.triggerCharacters` capability.
#[derive(Clone, Debug, Default)]
pub struct CompletionTriggers {
    chars: Vec<char>,
}

impl CompletionTriggers {
    /// Empty trigger set --- only explicit invocation fires completion.
    #[must_use]
    pub fn empty() -> Self {
        Self::default()
    }

    /// Build from an LSP `completionProvider` capability object.
    /// Reads `completionProvider.triggerCharacters` (a JSON array of
    /// single-char strings). Unknown shapes yield an empty set.
    #[must_use]
    pub fn from_capabilities(caps: &Value) -> Self {
        let Some(arr) = caps
            .get("completionProvider")
            .and_then(|p| p.get("triggerCharacters"))
            .and_then(Value::as_array)
        else {
            return Self::empty();
        };
        let mut chars = Vec::with_capacity(arr.len());
        for v in arr {
            if let Some(s) = v.as_str()
                && let Some(ch) = s.chars().next()
            {
                chars.push(ch);
            }
        }
        Self { chars }
    }

    /// True if `ch` is in the trigger set.
    #[must_use]
    pub fn should_fire(&self, ch: char) -> bool {
        self.chars.contains(&ch)
    }

    /// All trigger characters.
    #[must_use]
    pub fn chars(&self) -> &[char] {
        &self.chars
    }
}

// ---------------------------------------------------------------------------
// View
// ---------------------------------------------------------------------------

/// Default popup width in cells when nothing better is available.
/// Wide enough for "method-name : detail" without wrapping the typical
/// rust-analyzer reply.
const DEFAULT_POPUP_WIDTH: u32 = 40;

/// Style for the currently-selected row (reverse video so it pops on
/// any base palette).
fn selected_style() -> Style {
    Style {
        reverse: true,
        ..Style::default()
    }
}

/// Style for the kind-glyph column (dim foreground).
fn kind_style() -> Style {
    Style {
        fg: Color::Indexed(8),
        ..Style::default()
    }
}

/// Popup view that draws the completion list at its viewport's
/// origin. The viewport's `cell_size` bounds the popup; the host
/// (Lua) decides where to put it.
///
/// Unlike [`crate::diag::DiagnosticView`], this view does **not**
/// compose over a buffer's text --- it owns every cell inside its
/// viewport. The `_buf` parameter is unused.
pub struct CompletionView {
    key: CompletionKey,
    store: SharedCompletionStore,
}

impl CompletionView {
    /// Construct a popup view for `key` against `store`.
    #[must_use]
    pub fn new(key: CompletionKey, store: SharedCompletionStore) -> Self {
        Self { key, store }
    }

    /// The key this view is keyed under.
    #[must_use]
    pub fn key(&self) -> &CompletionKey {
        &self.key
    }
}

impl View for CompletionView {
    fn render(&mut self, _buf: &Buffer, viewport: Viewport, cells: &mut CellGrid<'_>) {
        let (items, selected) = {
            let guard = self.store.lock().expect("completion store poisoned");
            let items = guard.items(&self.key).to_vec();
            (items, guard.selected(&self.key))
        };

        let max_rows = viewport.cell_size.rows;
        let max_cols = viewport.cell_size.cols.max(1);
        let origin = viewport.cell_origin;
        let popup_cols = max_cols.min(DEFAULT_POPUP_WIDTH);

        // Clear the popup region.
        for r in 0..max_rows {
            for c in 0..max_cols {
                *cells.at(CellCoord::new(origin.row + r, origin.col + c)) = Cell::default();
            }
        }
        if items.is_empty() {
            return;
        }

        for row in 0..max_rows.min(items.len() as u32) {
            let item = &items[row as usize];
            let row_style = if row as usize == selected {
                selected_style()
            } else {
                Style::default()
            };
            // Paint the whole row's background first so the selected
            // row's reverse video covers the trailing whitespace.
            for c in 0..popup_cols {
                let cell = cells.at(CellCoord::new(origin.row + row, origin.col + c));
                cell.glyph = Glyph::Char(' ');
                cell.style = row_style;
                cell.attachment = None;
            }
            // Column 0: kind glyph.
            let kind_cell = cells.at(CellCoord::new(origin.row + row, origin.col));
            kind_cell.glyph = Glyph::Char(item.kind.glyph());
            kind_cell.style = if row as usize == selected {
                selected_style()
            } else {
                kind_style()
            };
            // Columns 2..: label, optionally followed by " : detail".
            let mut text = String::with_capacity(item.label.len() + 4);
            text.push_str(&item.label);
            if let Some(detail) = item.detail.as_deref() {
                text.push_str("  ");
                text.push_str(detail);
            }
            let mut col: u32 = 2;
            for ch in text.chars() {
                if col >= popup_cols {
                    break;
                }
                let width = char_display_width(ch);
                if width == 0 {
                    continue;
                }
                let cell = cells.at(CellCoord::new(origin.row + row, origin.col + col));
                cell.glyph = Glyph::Char(ch);
                cell.style = row_style;
                cell.attachment = None;
                col += 1;
                if width == 2 && col < popup_cols {
                    let cont = cells.at(CellCoord::new(origin.row + row, origin.col + col));
                    cont.glyph = Glyph::Continuation;
                    cont.style = row_style;
                    cont.attachment = None;
                    col += 1;
                }
            }
        }
    }
}

fn char_display_width(ch: char) -> u32 {
    UnicodeWidthChar::width(ch).unwrap_or(0) as u32
}

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

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

    #[test]
    fn from_lsp_value_parses_completion_list() {
        let v = json!({
            "isIncomplete": true,
            "items": [
                { "label": "println", "kind": 3, "detail": "macro println!", "insertText": "println!" },
                { "label": "print", "kind": 3 }
            ]
        });
        let r = CompletionResponse::from_lsp_value(&v);
        assert!(r.is_incomplete);
        assert_eq!(r.items.len(), 2);
        assert_eq!(r.items[0].label, "println");
        assert_eq!(r.items[0].kind, CompletionItemKind::Function);
        assert_eq!(r.items[0].detail.as_deref(), Some("macro println!"));
        assert_eq!(r.items[0].effective_insert_text(), "println!");
        // Falls back to label.
        assert_eq!(r.items[1].effective_insert_text(), "print");
    }

    #[test]
    fn from_lsp_value_parses_bare_array() {
        let v = json!([
            { "label": "a", "kind": 6 },
            { "label": "b" } // missing kind → Text
        ]);
        let r = CompletionResponse::from_lsp_value(&v);
        assert!(!r.is_incomplete);
        assert_eq!(r.items.len(), 2);
        assert_eq!(r.items[0].kind, CompletionItemKind::Variable);
        assert_eq!(r.items[1].kind, CompletionItemKind::Text);
    }

    #[test]
    fn from_lsp_value_collapses_markup_documentation() {
        let v = json!({
            "label": "x",
            "documentation": { "kind": "markdown", "value": "**bold** doc" }
        });
        let item = CompletionItem::from_lsp_value(&v).unwrap();
        assert_eq!(item.documentation.as_deref(), Some("**bold** doc"));
        // String form also works.
        let v2 = json!({ "label": "y", "documentation": "plain" });
        assert_eq!(
            CompletionItem::from_lsp_value(&v2)
                .unwrap()
                .documentation
                .as_deref(),
            Some("plain")
        );
    }

    #[test]
    fn store_set_clears_when_empty() {
        let mut s = CompletionStore::new();
        let key = CompletionKey::new("1", "file:///a");
        s.set(
            key.clone(),
            CompletionResponse {
                items: vec![CompletionItem::from_lsp_value(&json!({"label":"a"})).unwrap()],
                is_incomplete: false,
            },
        );
        assert_eq!(s.items(&key).len(), 1);
        s.set(key.clone(), CompletionResponse::default());
        assert!(s.items(&key).is_empty());
        assert!(!s.is_incomplete(&key));
    }

    #[test]
    fn store_selection_wraps() {
        let mut s = CompletionStore::new();
        let key = CompletionKey::new("1", "file:///a");
        s.set(
            key.clone(),
            CompletionResponse {
                items: (0..3)
                    .map(|i| {
                        CompletionItem::from_lsp_value(&json!({"label": format!("x{i}")})).unwrap()
                    })
                    .collect(),
                is_incomplete: false,
            },
        );
        assert_eq!(s.selected(&key), 0);
        s.move_selection(&key, 1);
        assert_eq!(s.selected(&key), 1);
        s.move_selection(&key, 5); // 1 + 5 = 6, mod 3 = 0
        assert_eq!(s.selected(&key), 0);
        s.move_selection(&key, -1); // -1 mod 3 = 2
        assert_eq!(s.selected(&key), 2);
    }

    #[test]
    fn store_select_clamps() {
        let mut s = CompletionStore::new();
        let key = CompletionKey::new("1", "file:///a");
        s.set(
            key.clone(),
            CompletionResponse {
                items: vec![
                    CompletionItem::from_lsp_value(&json!({"label":"a"})).unwrap(),
                    CompletionItem::from_lsp_value(&json!({"label":"b"})).unwrap(),
                ],
                is_incomplete: false,
            },
        );
        s.select(&key, 99);
        assert_eq!(s.selected(&key), 1);
    }

    #[test]
    fn triggers_from_capabilities() {
        let caps = json!({
            "completionProvider": { "triggerCharacters": [".", "::", ">"] }
        });
        let t = CompletionTriggers::from_capabilities(&caps);
        assert!(t.should_fire('.'));
        // `::` is recorded as ':' (the first char) per the LSP spec
        // wording --- triggers are single chars.
        assert!(t.should_fire(':'));
        assert!(t.should_fire('>'));
        assert!(!t.should_fire('a'));
    }

    #[test]
    fn triggers_empty_for_capabilities_without_completion_provider() {
        let caps = json!({ "hoverProvider": true });
        let t = CompletionTriggers::from_capabilities(&caps);
        assert!(t.chars().is_empty());
        assert!(!t.should_fire('.'));
    }

    #[test]
    fn item_kind_glyph_is_stable() {
        assert_eq!(CompletionItemKind::Function.glyph(), 'f');
        assert_eq!(CompletionItemKind::Field.glyph(), 'p');
        assert_eq!(CompletionItemKind::Variable.glyph(), 'v');
        assert_eq!(CompletionItemKind::Class.glyph(), 'C');
        assert_eq!(CompletionItemKind::Keyword.glyph(), 'k');
        assert_eq!(CompletionItemKind::Text.glyph(), '.');
    }

    #[test]
    fn null_response_is_empty() {
        let r = CompletionResponse::from_lsp_value(&Value::Null);
        assert!(r.items.is_empty());
        assert!(!r.is_incomplete);
    }
}
