use hjkl_engine::Host;

// ---- registers -------------------------------------------------------------

/// `:reg` / `:registers` — tabular dump of every non-empty register slot.
pub fn format_registers<H: Host>(editor: &hjkl_engine::Editor<hjkl_buffer::View, H>) -> String {
    editor.with_registers(|r| {
        let mut lines = vec!["--- Registers ---".to_string()];
        let mut push = |sel: &str, text: &str, linewise: bool| {
            if text.is_empty() {
                return;
            }
            let marker = if linewise { "L" } else { " " };
            lines.push(format!("{sel:<3} {marker} {}", display_register(text)));
        };
        push("\"\"", &r.unnamed.text, r.unnamed.linewise);
        push("\"0", &r.yank_zero.text, r.yank_zero.linewise);
        for (i, slot) in r.delete_ring.iter().enumerate() {
            let sel = format!("\"{}", i + 1);
            push(&sel, &slot.text, slot.linewise);
        }
        for (i, slot) in r.named.iter().enumerate() {
            let sel = format!("\"{}", (b'a' + i as u8) as char);
            push(&sel, &slot.text, slot.linewise);
        }
        if lines.len() == 1 {
            lines.push("(no registers set)".to_string());
        }
        lines.join("\n")
    })
}

/// Escape control chars + truncate so a multi-line register fits a single row
/// of the toast table.
fn display_register(text: &str) -> String {
    let escaped: String = text
        .chars()
        .map(|c| match c {
            '\n' => "\\n".to_string(),
            '\t' => "\\t".to_string(),
            '\r' => "\\r".to_string(),
            c => c.to_string(),
        })
        .collect();
    const MAX: usize = 60;
    if escaped.chars().count() > MAX {
        let head: String = escaped.chars().take(MAX - 3).collect();
        format!("{head}...")
    } else {
        escaped
    }
}

// ---- marks -----------------------------------------------------------------

/// `:marks` — list every set mark with `(line, col)`. Lines are 1-based to
/// match vim; cols are 0-based. Uppercase global marks include the buffer id.
pub fn format_marks<H: Host>(editor: &hjkl_engine::Editor<hjkl_buffer::View, H>) -> String {
    let mut lines = vec!["--- Marks ---".to_string(), "mark  line  col".to_string()];
    // Lowercase (buffer-local) marks — from the unified `Editor::marks` map.
    for (c, (r, col)) in editor.marks() {
        if c.is_ascii_lowercase() || !c.is_ascii_alphabetic() {
            lines.push(format!(" {c}    {:>4}  {:>3}", r + 1, col));
        }
    }
    // Uppercase global marks — include buffer_id for cross-buffer context.
    for (c, bid, r, col) in editor.global_marks_iter() {
        lines.push(format!(" {c}    {:>4}  {:>3}  buf:{bid}", r + 1, col));
    }
    if let Some((r, col)) = editor.last_jump_back() {
        lines.push(format!(" '    {:>4}  {:>3}", r + 1, col));
    }
    if let Some((r, col)) = editor.last_edit_pos() {
        lines.push(format!(" .    {:>4}  {:>3}", r + 1, col));
    }
    if lines.len() == 2 {
        lines.push("(no marks set)".to_string());
    }
    lines.join("\n")
}

// ---- jumps -----------------------------------------------------------------

/// `:jumps` — list the jump-back and jump-forward lists.
/// Format mirrors vim: `jump  line  col  file` columns. Newest items
/// have the smallest `jump` number; current position is `0`.
pub fn format_jumps<H: Host>(editor: &hjkl_engine::Editor<hjkl_buffer::View, H>) -> String {
    let (back, fwd) = editor.jump_list();
    if back.is_empty() && fwd.is_empty() {
        return "(no jumps recorded)".to_string();
    }
    let mut lines = vec![
        "--- Jump list ---".to_string(),
        " jump  line   col".to_string(),
    ];
    // jump_back: oldest at index 0, newest at the end.
    // Display as descending jump numbers: back.len() at oldest, 1 at newest, 0 = current.
    // Then jump_fwd (forward history) at negative-1, -2, …  vim shows >0 for fwd.
    // We keep it simple: back list reversed with ascending index, then fwd list.
    for (i, &(row, col)) in back.iter().rev().enumerate() {
        let jump_num = i + 1;
        lines.push(format!("{jump_num:>5}  {:>4}  {:>4}", row + 1, col));
    }
    // Mark current position (not in list — just a separator).
    lines.push(format!("{:>5}  (current position)", 0));
    for (i, &(row, col)) in fwd.iter().enumerate() {
        let jump_num = -(i as isize + 1);
        lines.push(format!("{jump_num:>5}  {:>4}  {:>4}", row + 1, col));
    }
    lines.join("\n")
}

// ---- changes ---------------------------------------------------------------

/// `:changes` — list the change list (bounded ring of recent edit positions).
/// Newest entries have lower index numbers, matching vim's `:changes` output.
pub fn format_changes<H: Host>(editor: &hjkl_engine::Editor<hjkl_buffer::View, H>) -> String {
    let (list, cursor) = editor.change_list();
    if list.is_empty() {
        return "(no changes recorded)".to_string();
    }
    let mut lines = vec![
        "--- Change list ---".to_string(),
        "change  line   col".to_string(),
    ];
    let len = list.len();
    // List is oldest-at-front, newest-at-back; display newest first (change 1 = most recent).
    for (display_idx, &(row, col)) in list.iter().rev().enumerate() {
        let change_num = display_idx + 1;
        // Mark the current walk position, if any. `change_list_cursor` is an
        // index into the original (oldest-first) vec; invert to display-index.
        let marker = match cursor {
            Some(c) if c == len - 1 - display_idx => " <",
            _ => "",
        };
        lines.push(format!(
            "{change_num:>6}  {:>4}  {:>4}{marker}",
            row + 1,
            col
        ));
    }
    lines.join("\n")
}

// ---- undolist --------------------------------------------------------------

/// Render a `SystemTime` as nvim's relative `:undolist` "when" text
/// (`"N seconds ago"`, `"N minutes ago"`, `"N hours ago"`, `"N days ago"`).
/// Future/unknown timestamps clamp to `"0 seconds ago"`.
fn format_relative_time(ts: std::time::SystemTime) -> String {
    let secs = ts.elapsed().map_or(0, |d| d.as_secs());
    let (n, unit) = if secs < 60 {
        (secs, "second")
    } else if secs < 3600 {
        (secs / 60, "minute")
    } else if secs < 86_400 {
        (secs / 3600, "hour")
    } else {
        (secs / 86_400, "day")
    };
    let plural = if n == 1 { "" } else { "s" };
    format!("{n} {unit}{plural} ago")
}

/// `:undolist` (`:undol`) — list the LEAVES of the undo tree, mirroring nvim:
/// `number` (change/`seq`), `changes` (depth from the original state), and
/// `when` (relative time). A trailing `>` marks the leaf the buffer is on. Like
/// nvim, intermediate (non-leaf) branch nodes are not listed.
pub fn format_undolist<H: Host>(editor: &hjkl_engine::Editor<hjkl_buffer::View, H>) -> String {
    let leaves = editor.undo_leaves();
    if leaves.is_empty() {
        return "Nothing to undo".to_string();
    }
    let mut lines = vec!["number changes  when               saved".to_string()];
    for (seq, changes, ts, is_current) in leaves {
        let when = format_relative_time(ts);
        let marker = if is_current { "  >" } else { "" };
        lines.push(format!("{seq:>6} {changes:>7}  {when:<17}{marker}"));
    }
    lines.join("\n")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_util::make_editor;
    use hjkl_vim::VimEditorExt;

    // ---- registers ---------------------------------------------------------

    #[test]
    fn format_registers_empty() {
        let editor = make_editor();
        let out = format_registers(&editor);
        assert_eq!(out, "--- Registers ---\n(no registers set)");
    }

    #[test]
    fn format_registers_after_yank() {
        let editor = make_editor();
        editor.with_registers_mut(|r| r.record_yank("hello".into(), false, None));
        let out = format_registers(&editor);
        assert!(out.contains("hello"), "expected 'hello' in: {out}");
        assert!(out.starts_with("--- Registers ---"));
    }

    // ---- marks -------------------------------------------------------------

    #[test]
    fn format_marks_empty() {
        let editor = make_editor();
        let out = format_marks(&editor);
        assert_eq!(out, "--- Marks ---\nmark  line  col\n(no marks set)");
    }

    #[test]
    fn format_marks_after_set_mark() {
        let mut editor = make_editor();
        editor.set_mark_at_cursor('a');
        let out = format_marks(&editor);
        assert!(out.contains("a"), "expected mark 'a' in: {out}");
        assert!(out.starts_with("--- Marks ---"));
    }

    // ---- jumps -------------------------------------------------------------

    #[test]
    fn format_jumps_empty() {
        let editor = make_editor();
        let out = format_jumps(&editor);
        assert_eq!(out, "(no jumps recorded)");
    }

    // ---- changes -----------------------------------------------------------

    #[test]
    fn format_changes_empty() {
        let editor = make_editor();
        let out = format_changes(&editor);
        assert_eq!(out, "(no changes recorded)");
    }
}
