// lua_bindings/fold.rs --- pmacs.fold: the code-folding Lua surface (Arc 6).

//! `pmacs.fold.*` --- the Lua surface over [`crate::fold`]. Installed
//! entirely from Rust (like `pmacs.config`), after `make_syntax_registry`
//! so the tree-consuming operations can reach the parse tree via app-data.
//!
//! ```lua
//! -- data API (explicit buffer, no ambient resolution):
//! pmacs.fold.fold(buffer, { start = ..., ["end"] = ... })
//! pmacs.fold.unfold(buffer, { start = ..., ["end"] = ... })
//! pmacs.fold.folds(buffer)          -- -> { {start=,["end"]=}, ... }
//! pmacs.fold.toggle(buffer, pos)
//!
//! -- interactive helpers the fold.lua commands drive (explicit buffer +
//! -- point resolved from the invoking frontend):
//! pmacs.fold.close(buffer, pos)     -- close innermost open
//! pmacs.fold.open(buffer, pos)      -- open outermost closed
//! pmacs.fold.cycle(buffer, pos)     -- org-TAB toggle
//! pmacs.fold.close_all(buffer)      -- top-level regions only
//! pmacs.fold.open_all(buffer)
//! ```
//!
//! Fold *creation* refuses against an absent or stale parse tree (Q#FD10)
//! and validates the buffer kind / UTF-8 boundaries / >= 1-hidden-line
//! rule (Q#FD11); a rejection reports on the status line and returns
//! `false`. The **interactive** commands (`toggle`/`close`/`cycle`/
//! `close_all`) move the invoking frontend's point to the head line when
//! they fold a range around it (Q#FD3); the **data-API** `fold` is
//! deliberately exempt — a programmatic caller names an explicit buffer
//! and range with no invoking point to relocate.

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

use mlua::{Lua, Table};
use pmacs_protocol::{BufferId, ByteRange};

use super::{
    BufferIdLua, SharedCore, resolve, resolve_mut, u64_from_lua, with_registry, with_registry_mut,
};
use crate::buffer::Buffer;
use crate::fold::{self, FoldStore, SharedFoldRegistry};
use crate::syntax::{ParseTreeBundle, SharedSyntaxRegistry};

/// Install `pmacs.fold` over `fold_registry` (the same `Rc` the core owns).
#[allow(
    clippy::too_many_lines,
    reason = "linear per-function registration of the pmacs.fold surface, \
              mirroring install_config; splitting fragments the wiring"
)]
pub fn install_fold(lua: &Lua, fold_registry: &SharedFoldRegistry) -> mlua::Result<()> {
    // Also stash the registry as app-data so the buffer-remove cleanup
    // (`after_buffer_removed`) can drop a killed buffer's store, mirroring
    // the keymap/config registries.
    lua.set_app_data(fold_registry.clone());
    let fold_mod = lua.create_table()?;

    // ---- data API ---------------------------------------------------------

    {
        let reg = fold_registry.clone();
        fold_mod.set(
            "fold",
            lua.create_function(
                move |lua, (buf, range): (BufferIdLua, Table)| -> mlua::Result<bool> {
                    let id = buf.id();
                    let requested = range_from_table(&range)?;
                    let Some(bytes) = document_bytes(lua, id)? else {
                        set_status(lua, "fold rejected: not a document buffer");
                        return Ok(false);
                    };
                    if requested.start > bytes.len() as u64
                        || requested.end > bytes.len() as u64
                        || !is_char_boundary(&bytes, requested.start)
                        || !is_char_boundary(&bytes, requested.end)
                    {
                        set_status(lua, "fold rejected: out of bounds or not a char boundary");
                        return Ok(false);
                    }
                    let Some(normalized) = fold::normalize_arbitrary_range(&bytes, requested)
                    else {
                        set_status(lua, "fold rejected: range hides no full line");
                        return Ok(false);
                    };
                    let store = store_for(lua, &reg, id)?;
                    let added = lock(&store).insert(normalized);
                    Ok(added)
                },
            )?,
        )?;
    }

    {
        let reg = fold_registry.clone();
        fold_mod.set(
            "unfold",
            lua.create_function(
                move |lua, (buf, range): (BufferIdLua, Table)| -> mlua::Result<bool> {
                    let id = buf.id();
                    let requested = range_from_table(&range)?;
                    let Some(store) = reg.store(id) else {
                        return Ok(false);
                    };
                    // Accept an exact stored range (the `folds()` round-trip)
                    // or an arbitrary range that normalizes to a stored one.
                    if lock(&store).remove(requested) {
                        return Ok(true);
                    }
                    if let Ok(Some(bytes)) = document_bytes(lua, id)
                        && let Some(normalized) = fold::normalize_arbitrary_range(&bytes, requested)
                    {
                        return Ok(lock(&store).remove(normalized));
                    }
                    Ok(false)
                },
            )?,
        )?;
    }

    {
        let reg = fold_registry.clone();
        fold_mod.set(
            "folds",
            lua.create_function(move |lua, buf: BufferIdLua| -> mlua::Result<Table> {
                let out = lua.create_table()?;
                for (i, r) in reg.folds(buf.id()).into_iter().enumerate() {
                    out.set(i + 1, range_to_table(lua, r)?)?;
                }
                Ok(out)
            })?,
        )?;
    }

    {
        let reg = fold_registry.clone();
        fold_mod.set(
            "toggle",
            lua.create_function(
                move |lua, (buf, pos): (BufferIdLua, i64)| -> mlua::Result<bool> {
                    let id = buf.id();
                    let p = u64_from_lua(pos)?;
                    // A stored fold at the point unfolds without needing a tree.
                    if let Some(store) = reg.store(id) {
                        let mut s = lock(&store);
                        if !s.containing(p).is_empty() {
                            s.unfold_containing(p);
                            return Ok(true);
                        }
                    }
                    let Some(bundle) = bundle_or_status(lua, id) else {
                        return Ok(false);
                    };
                    let store = store_for(lua, &reg, id)?;
                    match fold::toggle_at(&mut lock(&store), &bundle, p) {
                        fold::ToggleOutcome::Folded(r) => {
                            maybe_move_point(lua, id, r);
                            Ok(true)
                        }
                        fold::ToggleOutcome::Unfolded(_) => Ok(true),
                        fold::ToggleOutcome::Nothing => {
                            set_status(lua, "nothing foldable here");
                            Ok(false)
                        }
                    }
                },
            )?,
        )?;
    }

    // ---- interactive helpers (state-aware; driven by fold.lua) ------------

    {
        let reg = fold_registry.clone();
        fold_mod.set(
            "close",
            lua.create_function(
                move |lua, (buf, pos): (BufferIdLua, i64)| -> mlua::Result<bool> {
                    let id = buf.id();
                    let p = u64_from_lua(pos)?;
                    let Some(bundle) = bundle_or_status(lua, id) else {
                        return Ok(false);
                    };
                    let store = store_for(lua, &reg, id)?;
                    if let Some(r) = fold::close_at(&mut lock(&store), &bundle, p) {
                        maybe_move_point(lua, id, r);
                        Ok(true)
                    } else {
                        set_status(lua, "no more folds to close here");
                        Ok(false)
                    }
                },
            )?,
        )?;
    }

    {
        let reg = fold_registry.clone();
        fold_mod.set(
            "open",
            lua.create_function(
                move |_, (buf, pos): (BufferIdLua, i64)| -> mlua::Result<bool> {
                    let id = buf.id();
                    let p = u64_from_lua(pos)?;
                    let Some(store) = reg.store(id) else {
                        return Ok(false);
                    };
                    Ok(fold::open_at(&mut lock(&store), p).is_some())
                },
            )?,
        )?;
    }

    {
        let reg = fold_registry.clone();
        fold_mod.set(
            "cycle",
            lua.create_function(
                move |lua, (buf, pos): (BufferIdLua, i64)| -> mlua::Result<bool> {
                    let id = buf.id();
                    let p = u64_from_lua(pos)?;
                    let Some(bundle) = bundle_or_status(lua, id) else {
                        return Ok(false);
                    };
                    let store = store_for(lua, &reg, id)?;
                    match fold::cycle_at(&mut lock(&store), &bundle, p) {
                        fold::CycleOutcome::Closed(r) => {
                            maybe_move_point(lua, id, r);
                            Ok(true)
                        }
                        fold::CycleOutcome::OpenedAll(_) => Ok(true),
                        fold::CycleOutcome::Nothing => {
                            set_status(lua, "nothing foldable here");
                            Ok(false)
                        }
                    }
                },
            )?,
        )?;
    }

    {
        let reg = fold_registry.clone();
        fold_mod.set(
            "close_all",
            lua.create_function(move |lua, buf: BufferIdLua| -> mlua::Result<i64> {
                let id = buf.id();
                let Some(bundle) = bundle_or_status(lua, id) else {
                    return Ok(0);
                };
                let targets = fold::top_level_fold_targets(&bundle);
                let store = store_for(lua, &reg, id)?;
                let inserted: Vec<ByteRange> = {
                    let mut s = lock(&store);
                    targets.into_iter().filter(|t| s.insert(*t)).collect()
                };
                // close-all is interactive: if a newly-closed top-level
                // fold contains the invoking point, move it to the head
                // (Q#FD3). At most one top-level fold can contain it.
                for r in &inserted {
                    maybe_move_point(lua, id, *r);
                }
                Ok(i64::try_from(inserted.len()).unwrap_or(i64::MAX))
            })?,
        )?;
    }

    {
        let reg = fold_registry.clone();
        fold_mod.set(
            "open_all",
            lua.create_function(move |_, buf: BufferIdLua| -> mlua::Result<bool> {
                match reg.store(buf.id()) {
                    Some(store) => Ok(lock(&store).clear()),
                    None => Ok(false),
                }
            })?,
        )?;
    }

    let pmacs: Table = lua.globals().get("pmacs")?;
    pmacs.set("fold", fold_mod)?;
    Ok(())
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn lock(store: &Arc<Mutex<FoldStore>>) -> std::sync::MutexGuard<'_, FoldStore> {
    store.lock().expect("fold store mutex poisoned")
}

fn range_from_table(t: &Table) -> mlua::Result<ByteRange> {
    let start = u64_from_lua(t.raw_get::<i64>("start")?)?;
    let end = u64_from_lua(t.raw_get::<i64>("end")?)?;
    Ok(ByteRange { start, end })
}

fn range_to_table(lua: &Lua, r: ByteRange) -> mlua::Result<Table> {
    let t = lua.create_table()?;
    // Byte offsets are always well within `i64` range; the Lua integer
    // type is `i64`.
    t.set("start", r.start.cast_signed())?;
    t.set("end", r.end.cast_signed())?;
    Ok(t)
}

/// The buffer's bytes if it is a normal document buffer, or `None` if it is
/// read-only (a terminal identity buffer or other non-document buffer —
/// the Q#FD11 "normal document buffer" guard).
fn document_bytes(lua: &Lua, buf: BufferId) -> mlua::Result<Option<Vec<u8>>> {
    with_registry(lua, |r| {
        let buffer = resolve(r, buf)?;
        if buffer.is_read_only() {
            return Ok(None);
        }
        Ok(Some(buffer_bytes(buffer)))
    })
}

fn buffer_bytes(buf: &Buffer) -> Vec<u8> {
    let len = buf.len();
    let mut bytes = vec![0u8; len as usize];
    buf.snapshot_rope().slice(0, len, &mut bytes);
    bytes
}

fn is_char_boundary(bytes: &[u8], pos: u64) -> bool {
    let p = pos as usize;
    p == 0 || p == bytes.len() || (p < bytes.len() && (bytes[p] & 0xC0) != 0x80)
}

/// The get-or-attach store handle for `buf`, materializing the store and
/// attaching its translator view on first use.
fn store_for(
    lua: &Lua,
    reg: &SharedFoldRegistry,
    buf: BufferId,
) -> mlua::Result<Arc<Mutex<FoldStore>>> {
    with_registry_mut(lua, |r| {
        let buffer = resolve_mut(r, buf)?;
        Ok(reg.store_or_attach(buffer))
    })
}

/// The settled parse bundle for `buf`, or `None` after reporting the
/// stale/absent-tree rejection on the status line (Q#FD10).
fn bundle_or_status(lua: &Lua, buf: BufferId) -> Option<Arc<ParseTreeBundle>> {
    match settled_bundle(lua, buf) {
        Ok(bundle) => Some(bundle),
        Err(reason) => {
            set_status(lua, reason);
            None
        }
    }
}

fn settled_bundle(lua: &Lua, buf: BufferId) -> Result<Arc<ParseTreeBundle>, &'static str> {
    let syntax = lua
        .app_data_ref::<SharedSyntaxRegistry>()
        .ok_or("fold: no syntax registry")?;
    let handle = syntax.view(buf).ok_or("fold: no parse for this buffer")?;
    if handle.pending_edit_count() > 0 {
        return Err("fold: parse is stale (edits pending); try again");
    }
    handle.current().ok_or("fold: no parse yet; try again")
}

/// Set the editor status line (rejection reporting).
fn set_status(lua: &Lua, msg: &str) {
    if let Some(core) = lua.app_data_ref::<SharedCore>() {
        core.borrow_mut().status = msg.to_string();
    }
}

/// Move the invoking frontend's point to the head line when a just-folded
/// range `r` contains it (Q#FD3). No-op if the folded buffer is not the
/// active one or the point is outside the fold.
fn maybe_move_point(lua: &Lua, buf: BufferId, r: ByteRange) {
    if let Some(core) = lua.app_data_ref::<SharedCore>() {
        let mut c = core.borrow_mut();
        if c.active_buffer_id() == buf {
            let point = c.active_window().cursor;
            // `(start, end]` containment: a point strictly inside the fold
            // moves to `start` (the end of the visible head line).
            if r.start < point && point <= r.end {
                c.set_cursor_byte(r.start);
            }
        }
    }
}
