#include "editor.h"
#include "markdown_io.h"
#include "hittest.h"
#include "utf8.h"
#include "lang.h"
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

static void reset_preferred_x(EditorState *ed) {
    ed->preferredX = -1.0f;
}

/* Collapses the selection anchor onto the current cursor position (extend=false), or leaves
   it untouched to extend the existing selection (extend=true). */
static void snap_anchor_to_cursor(EditorState *ed, bool extend) {
    if (extend) return;
    ed->selAnchorBlock = ed->cursorBlock;
    ed->selAnchorOffset = ed->cursorOffset;
}

void editor_init(EditorState *ed) {
    document_init(&ed->doc);
    ed->cursorBlock = 0;
    ed->cursorOffset = 0;
    ed->preferredX = -1.0f;
    ed->filePath = NULL;
    ed->dirty = false;
    undo_manager_init(&ed->undo, 200);
    ed->selAnchorBlock = 0;
    ed->selAnchorOffset = 0;
}

void editor_free(EditorState *ed) {
    document_free(&ed->doc);
    free(ed->filePath);
    ed->filePath = NULL;
    undo_manager_free(&ed->undo);
}

bool editor_load(EditorState *ed, const char *path) {
    free(ed->filePath);
    size_t n = strlen(path);
    ed->filePath = malloc(n + 1);
    memcpy(ed->filePath, path, n + 1);

    bool ok = document_load_file(&ed->doc, path);
    ed->cursorBlock = 0;
    ed->cursorOffset = 0;
    ed->dirty = false;
    undo_manager_reset(&ed->undo);
    ed->selAnchorBlock = 0;
    ed->selAnchorOffset = 0;
    reset_preferred_x(ed);
    return ok;
}

bool editor_save(EditorState *ed) {
    if (!ed->filePath) return false;
    bool ok = document_save_file(&ed->doc, ed->filePath);
    if (ok) ed->dirty = false;
    return ok;
}

const char *editor_display_name(const EditorState *ed) {
    if (!ed->filePath) return lang_get(STR_UNTITLED);
    const char *slash = strrchr(ed->filePath, '/');
    return slash ? slash + 1 : ed->filePath;
}

/* Converts a leading run of paragraph text (e.g. "#", "##", "-", "1.") typed just before the
   space that was just inserted into the corresponding block type, WYSIWYG-style. */
static void maybe_autoformat_block(EditorState *ed) {
    Block *b = &ed->doc.blocks[ed->cursorBlock];
    int upto = ed->cursorOffset - 1; /* text before the space just typed */
    if (upto <= 0) return;
    const char *t = b->text.data;

    if (b->type == BLOCK_BULLET) {
        /* "- " already turned this block into a bullet; typing "[ ] " or "[x] " right after,
           at the very start of the item's own text, turns it into a task-list item. */
        if (upto == 3 && t[0] == '[' && (t[1] == ' ' || t[1] == 'x' || t[1] == 'X') && t[2] == ']') {
            BlockType newType = (t[1] == ' ') ? BLOCK_TASK_UNCHECKED : BLOCK_TASK_CHECKED;
            sb_delete(&b->text, 0, ed->cursorOffset);
            ed->cursorOffset = 0;
            b->type = newType;
        }
        return;
    }

    if (b->type != BLOCK_PARAGRAPH) return;

    if (upto == 3 && (memcmp(t, "---", 3) == 0 || memcmp(t, "***", 3) == 0 || memcmp(t, "___", 3) == 0)) {
        sb_delete(&b->text, 0, ed->cursorOffset);
        ed->cursorOffset = 0;
        b->type = BLOCK_HR;
        document_insert_block(&ed->doc, ed->cursorBlock + 1, BLOCK_PARAGRAPH, "", 0);
        ed->cursorBlock++;
        ed->cursorOffset = 0;
        return;
    }

    BlockType newType;
    int hashes = 0;
    while (hashes < upto && hashes < 6 && t[hashes] == '#') hashes++;
    if (hashes > 0 && hashes == upto) {
        static const BlockType headingTypes[6] = { BLOCK_H1, BLOCK_H2, BLOCK_H3, BLOCK_H4, BLOCK_H5, BLOCK_H6 };
        newType = headingTypes[hashes - 1];
    }
    else if (upto == 1 && (t[0] == '-' || t[0] == '*')) newType = BLOCK_BULLET;
    else if (upto == 1 && t[0] == '>') newType = BLOCK_QUOTE;
    else if (upto >= 2 && isdigit((unsigned char)t[0])) {
        int i = 0;
        while (i < upto && isdigit((unsigned char)t[i])) i++;
        if (i < upto && t[i] == '.' && i + 1 == upto) newType = BLOCK_NUMBERED;
        else return;
    } else {
        return;
    }

    sb_delete(&b->text, 0, ed->cursorOffset);
    ed->cursorOffset = 0;
    b->type = newType;
}

bool editor_has_selection(const EditorState *ed) {
    return ed->selAnchorBlock != ed->cursorBlock || ed->selAnchorOffset != ed->cursorOffset;
}

void editor_selection_range(const EditorState *ed, int *startBlock, int *startOffset, int *endBlock, int *endOffset) {
    int ab = ed->selAnchorBlock, ao = ed->selAnchorOffset, cb = ed->cursorBlock, co = ed->cursorOffset;
    if (ab < cb || (ab == cb && ao <= co)) {
        *startBlock = ab; *startOffset = ao; *endBlock = cb; *endOffset = co;
    } else {
        *startBlock = cb; *startOffset = co; *endBlock = ab; *endOffset = ao;
    }
}

/* If a selection is active, deletes it (merging a cross-block selection into the start
   block, which keeps its type) and collapses the cursor/anchor to the deletion point.
   Returns whether there was a selection to delete. */
static bool delete_selection_if_any(EditorState *ed) {
    if (!editor_has_selection(ed)) return false;
    int sb, so, eb, eo;
    editor_selection_range(ed, &sb, &so, &eb, &eo);

    bool spansTableCell = false;
    for (int i = sb; i <= eb; i++) {
        if (block_type_is_table_cell(ed->doc.blocks[i].type)) { spansTableCell = true; break; }
    }

    if (spansTableCell) {
        /* Never merge text across cells or remove a cell block here -- that would corrupt the
           table's fixed rows*cols run (tableCol/tableCols arithmetic in rendering and
           vertical-nav trusts a clean, unbroken sequence downstream). Clear each selected
           block's own covered range in place instead -- for a same-cell selection (sb == eb)
           this is identical to the plain-text path below; it only changes behavior once the
           selection actually spans more than one block. */
        for (int i = sb; i <= eb; i++) {
            Block *b = &ed->doc.blocks[i];
            int start = (i == sb) ? so : 0;
            int end = (i == eb) ? eo : b->text.len;
            sb_delete(&b->text, start, end - start);
        }
    } else if (sb == eb) {
        sb_delete(&ed->doc.blocks[sb].text, so, eo - so);
    } else {
        Block *startBlock = &ed->doc.blocks[sb];
        Block *endBlock = &ed->doc.blocks[eb];
        sb_delete(&startBlock->text, so, startBlock->text.len - so);
        sb_append(&startBlock->text, endBlock->text.data + eo, endBlock->text.len - eo);
        for (int i = eb; i > sb; i--) document_remove_block(&ed->doc, i);
    }
    ed->cursorBlock = ed->selAnchorBlock = sb;
    ed->cursorOffset = ed->selAnchorOffset = so;
    ed->dirty = true;
    reset_preferred_x(ed);
    return true;
}

char *editor_selection_to_text(const EditorState *ed, int *outLen) {
    if (!editor_has_selection(ed)) { if (outLen) *outLen = 0; return NULL; }
    int sb, so, eb, eo;
    editor_selection_range(ed, &sb, &so, &eb, &eo);
    StrBuf out;
    sb_init(&out);
    for (int i = sb; i <= eb; i++) {
        Block *b = &ed->doc.blocks[i];
        int start = (i == sb) ? so : 0;
        int end = (i == eb) ? eo : b->text.len;
        if (i > sb) sb_append_char(&out, '\n');
        sb_append(&out, b->text.data + start, end - start);
    }
    char *result = malloc((size_t)out.len + 1);
    memcpy(result, out.data, (size_t)out.len);
    result[out.len] = '\0';
    if (outLen) *outLen = out.len;
    sb_free(&out);
    return result;
}

static void insert_utf8_raw(EditorState *ed, const char *bytes, int n) {
    Block *b = &ed->doc.blocks[ed->cursorBlock];
    sb_insert(&b->text, ed->cursorOffset, bytes, n);
    ed->cursorOffset += n;
    ed->dirty = true;
    if (n == 1 && bytes[0] == ' ') maybe_autoformat_block(ed);
    reset_preferred_x(ed);
}

void editor_insert_utf8(EditorState *ed, const char *bytes, int n) {
    if (n <= 0) return;
    undo_manager_before_edit(&ed->undo, EDIT_TYPE, &ed->doc, ed->cursorBlock, ed->cursorOffset);
    delete_selection_if_any(ed);
    insert_utf8_raw(ed, bytes, n);
    snap_anchor_to_cursor(ed, false);
    undo_manager_after_edit(&ed->undo, EDIT_TYPE, ed->cursorBlock, ed->cursorOffset);
}

void editor_paste(EditorState *ed, const char *text, int len) {
    if (len <= 0) return;
    char *tmp = malloc((size_t)len);
    int n = 0;
    for (int i = 0; i < len; i++) {
        char c = text[i];
        if (c == '\r') continue;
        if (c == '\n' || c == '\t') c = ' ';
        if ((unsigned char)c < 0x20) continue;
        tmp[n++] = c;
    }
    if (n > 0) {
        undo_manager_before_edit(&ed->undo, EDIT_PASTE, &ed->doc, ed->cursorBlock, ed->cursorOffset);
        delete_selection_if_any(ed);
        insert_utf8_raw(ed, tmp, n);
        snap_anchor_to_cursor(ed, false);
        undo_manager_after_edit(&ed->undo, EDIT_PASTE, ed->cursorBlock, ed->cursorOffset);
    }
    free(tmp);
}

/* Removes every block belonging to the table that starts at `headerFirstIdx` (the header row's
   first cell), replacing them all with a single empty BLOCK_PARAGRAPH at that position. This is
   the one deliberate, discoverable way to remove a whole table (mirrors BLOCK_HR's
   demote-then-remove precedent) -- needed because every other deleting gesture is guarded to
   never touch a table's cell blocks at all. */
static void remove_table_at(EditorState *ed, int headerFirstIdx) {
    int cols = ed->doc.blocks[headerFirstIdx].tableCols;
    int tableEnd = headerFirstIdx;
    while (tableEnd < ed->doc.count && block_type_is_table_cell(ed->doc.blocks[tableEnd].type)
           && ed->doc.blocks[tableEnd].tableCols == cols) {
        tableEnd++;
    }
    for (int i = tableEnd - 1; i >= headerFirstIdx; i--) document_remove_block(&ed->doc, i);
    document_insert_block(&ed->doc, headerFirstIdx, BLOCK_PARAGRAPH, "", 0);
    ed->cursorBlock = headerFirstIdx;
    ed->cursorOffset = 0;
}

static void backspace_raw(EditorState *ed) {
    Block *b = &ed->doc.blocks[ed->cursorBlock];
    if (ed->cursorOffset > 0) {
        int prevStart = utf8_prev_start(b->text.data, ed->cursorOffset);
        sb_delete(&b->text, prevStart, ed->cursorOffset - prevStart);
        ed->cursorOffset = prevStart;
        ed->dirty = true;
    } else if (block_type_is_table_cell(b->type)) {
        /* Checked before the generic b->type != BLOCK_PARAGRAPH demote branch below, or a cell
           would silently become an orphaned paragraph via that existing path. No-op at any cell
           except the table's very first (tableCol == 0 of the header row), which removes the
           whole table -- never merge into the preceding block, that would smash two cells' (or
           a cell and unrelated) text together across a structural boundary. */
        if (b->type == BLOCK_TABLE_HEADER_CELL && b->tableCol == 0) {
            remove_table_at(ed, ed->cursorBlock);
            ed->dirty = true;
        }
    } else if (b->type != BLOCK_PARAGRAPH) {
        if (b->type == BLOCK_CODE) {
            for (int i = 0; i < b->text.len; i++) if (b->text.data[i] == '\n') b->text.data[i] = ' ';
        }
        if (b->type == BLOCK_IMAGE) {
            sb_clear(&b->alt);
        }
        b->type = BLOCK_PARAGRAPH;
        ed->dirty = true;
    } else if (ed->cursorBlock > 0 && !block_type_is_table_cell(ed->doc.blocks[ed->cursorBlock - 1].type)) {
        /* Symmetric guard: never merge an ordinary block's text into a preceding table cell
           either -- same principle, just the other direction. */
        Block *prev = &ed->doc.blocks[ed->cursorBlock - 1];
        int mergeAt = prev->text.len;
        sb_append(&prev->text, b->text.data, b->text.len);
        document_remove_block(&ed->doc, ed->cursorBlock);
        ed->cursorBlock--;
        ed->cursorOffset = mergeAt;
        ed->dirty = true;
    }
    reset_preferred_x(ed);
}

void editor_backspace(EditorState *ed) {
    undo_manager_before_edit(&ed->undo, EDIT_BACKSPACE, &ed->doc, ed->cursorBlock, ed->cursorOffset);
    if (delete_selection_if_any(ed)) {
        undo_manager_after_edit(&ed->undo, EDIT_BACKSPACE, ed->cursorBlock, ed->cursorOffset);
        return;
    }
    backspace_raw(ed);
    snap_anchor_to_cursor(ed, false);
    undo_manager_after_edit(&ed->undo, EDIT_BACKSPACE, ed->cursorBlock, ed->cursorOffset);
}

static void delete_forward_raw(EditorState *ed) {
    Block *b = &ed->doc.blocks[ed->cursorBlock];
    if (ed->cursorOffset < b->text.len) {
        int nlen = utf8_next_len(b->text.data, ed->cursorOffset, b->text.len);
        sb_delete(&b->text, ed->cursorOffset, nlen);
        ed->dirty = true;
    } else if (ed->cursorBlock < ed->doc.count - 1 && !block_type_is_table_cell(b->type)
               && !block_type_is_table_cell(ed->doc.blocks[ed->cursorBlock + 1].type)) {
        /* Guarded both ways: if the current block is a table cell, merging the next block into
           it would smash foreign text into a cell; if the *next* block is a table cell (even
           though the current one isn't), merging it away would shrink that row out from under
           the fixed tableCols arithmetic everything downstream trusts. Both are no-ops. */
        Block *next = &ed->doc.blocks[ed->cursorBlock + 1];
        if (b->type == BLOCK_CODE && next->type != BLOCK_CODE) {
            sb_append_char(&b->text, '\n');
        } else if (next->type == BLOCK_CODE && b->type != BLOCK_CODE) {
            for (int i = 0; i < next->text.len; i++) if (next->text.data[i] == '\n') next->text.data[i] = ' ';
        }
        sb_append(&b->text, next->text.data, next->text.len);
        document_remove_block(&ed->doc, ed->cursorBlock + 1);
        ed->dirty = true;
    }
    reset_preferred_x(ed);
}

void editor_delete_forward(EditorState *ed) {
    undo_manager_before_edit(&ed->undo, EDIT_DELETE_FWD, &ed->doc, ed->cursorBlock, ed->cursorOffset);
    if (delete_selection_if_any(ed)) {
        undo_manager_after_edit(&ed->undo, EDIT_DELETE_FWD, ed->cursorBlock, ed->cursorOffset);
        return;
    }
    delete_forward_raw(ed);
    snap_anchor_to_cursor(ed, false);
    undo_manager_after_edit(&ed->undo, EDIT_DELETE_FWD, ed->cursorBlock, ed->cursorOffset);
}

static void enter_raw(EditorState *ed) {
    Block *b = &ed->doc.blocks[ed->cursorBlock];

    if (b->type == BLOCK_CODE) {
        int lineStart = ed->cursorOffset;
        while (lineStart > 0 && b->text.data[lineStart - 1] != '\n') lineStart--;
        int lineEnd = ed->cursorOffset;
        while (lineEnd < b->text.len && b->text.data[lineEnd] != '\n') lineEnd++;

        bool closingFence = (lineEnd - lineStart == 3 && memcmp(b->text.data + lineStart, "```", 3) == 0);
        /* Also exit on Enter-on-an-already-blank-trailing-line ("double Enter"), mirroring the
           list-exit precedent below (block_type_is_continuable + empty text -> demote). This is
           the only *reliable* keyboard-only way to leave a code block: producing a literal
           backtick to type a real closing fence is a dead-key combination on some layouts (e.g.
           Swedish, ` + space) that's easy to get stuck on, exactly like the already-documented
           Ctrl+/ layout gap elsewhere in this codebase -- don't make escaping a code block
           depend on it too. b->text.len > 0 excludes a just-created empty code block, so a
           single Enter there still just adds a blank first line rather than immediately exiting. */
        bool doubleEnterExit = (lineStart == lineEnd && ed->cursorOffset == b->text.len && b->text.len > 0);

        if (closingFence || doubleEnterExit) {
            int removeFrom = lineStart > 0 ? lineStart - 1 : lineStart;
            sb_delete(&b->text, removeFrom, b->text.len - removeFrom);
            document_insert_block(&ed->doc, ed->cursorBlock + 1, BLOCK_PARAGRAPH, "", 0);
            ed->cursorBlock++;
            ed->cursorOffset = 0;
            ed->dirty = true;
            reset_preferred_x(ed);
            return;
        }

        sb_insert(&b->text, ed->cursorOffset, "\n", 1);
        ed->cursorOffset++;
        ed->dirty = true;
        reset_preferred_x(ed);
        return;
    }

    if (b->type == BLOCK_PARAGRAPH && b->text.len >= 3 && memcmp(b->text.data, "```", 3) == 0) {
        sb_append(&b->lang, b->text.data + 3, b->text.len - 3);
        b->type = BLOCK_CODE;
        sb_clear(&b->text);
        ed->cursorOffset = 0;
        ed->dirty = true;
        reset_preferred_x(ed);
        return;
    }

    if (b->type == BLOCK_PARAGRAPH && b->text.len == 3
        && (memcmp(b->text.data, "---", 3) == 0 || memcmp(b->text.data, "***", 3) == 0 || memcmp(b->text.data, "___", 3) == 0)) {
        b->type = BLOCK_HR;
        sb_clear(&b->text);
        document_insert_block(&ed->doc, ed->cursorBlock + 1, BLOCK_PARAGRAPH, "", 0);
        ed->cursorBlock++;
        ed->cursorOffset = 0;
        ed->dirty = true;
        reset_preferred_x(ed);
        return;
    }

    if (block_type_is_continuable(b->type) && b->text.len == 0) {
        b->type = BLOCK_PARAGRAPH;
        ed->dirty = true;
        reset_preferred_x(ed);
        return;
    }

    if (b->type == BLOCK_IMAGE) {
        /* text is the image's url -- never split/truncate it, regardless of cursor position in
           the caption. Mirrors the BLOCK_HR-insertion pattern above: always land in a fresh
           empty paragraph right after it. */
        document_insert_block(&ed->doc, ed->cursorBlock + 1, BLOCK_PARAGRAPH, "", 0);
        ed->cursorBlock++;
        ed->cursorOffset = 0;
        ed->dirty = true;
        reset_preferred_x(ed);
        return;
    }

    if (block_type_is_table_cell(b->type)) {
        /* A cell's text is one line of GFM table syntax -- a literal '\n' would break it.
           Enter behaves like Tab instead: move to the next cell, or exit the table. */
        editor_table_move_cell(ed, 1);
        ed->dirty = true;
        return;
    }

    int splitAt = ed->cursorOffset;
    int tailLen = b->text.len - splitAt;
    BlockType newType = block_type_is_continuable(b->type) ? b->type : BLOCK_PARAGRAPH;
    if (newType == BLOCK_TASK_CHECKED) newType = BLOCK_TASK_UNCHECKED; /* a freshly split task starts unchecked */

    document_insert_block(&ed->doc, ed->cursorBlock + 1, newType, b->text.data + splitAt, tailLen);
    b = &ed->doc.blocks[ed->cursorBlock]; /* doc.blocks may have been reallocated */
    sb_delete(&b->text, splitAt, tailLen);

    ed->cursorBlock++;
    ed->cursorOffset = 0;
    ed->dirty = true;
    reset_preferred_x(ed);
}

void editor_enter(EditorState *ed) {
    undo_manager_before_edit(&ed->undo, EDIT_ENTER, &ed->doc, ed->cursorBlock, ed->cursorOffset);
    delete_selection_if_any(ed);
    enter_raw(ed);
    snap_anchor_to_cursor(ed, false);
    undo_manager_after_edit(&ed->undo, EDIT_ENTER, ed->cursorBlock, ed->cursorOffset);
}

void editor_move_left(EditorState *ed, bool extend) {
    if (ed->cursorOffset > 0) {
        Block *b = &ed->doc.blocks[ed->cursorBlock];
        ed->cursorOffset = utf8_prev_start(b->text.data, ed->cursorOffset);
    } else if (ed->cursorBlock > 0) {
        ed->cursorBlock--;
        ed->cursorOffset = ed->doc.blocks[ed->cursorBlock].text.len;
    }
    snap_anchor_to_cursor(ed, extend);
    reset_preferred_x(ed);
}

void editor_move_right(EditorState *ed, bool extend) {
    Block *b = &ed->doc.blocks[ed->cursorBlock];
    if (ed->cursorOffset < b->text.len) {
        ed->cursorOffset += utf8_next_len(b->text.data, ed->cursorOffset, b->text.len);
    } else if (ed->cursorBlock < ed->doc.count - 1) {
        ed->cursorBlock++;
        ed->cursorOffset = 0;
    }
    snap_anchor_to_cursor(ed, extend);
    reset_preferred_x(ed);
}

void editor_move_home(EditorState *ed, bool extend) {
    ed->cursorOffset = 0;
    snap_anchor_to_cursor(ed, extend);
    reset_preferred_x(ed);
}

void editor_move_end(EditorState *ed, bool extend) {
    ed->cursorOffset = ed->doc.blocks[ed->cursorBlock].text.len;
    snap_anchor_to_cursor(ed, extend);
    reset_preferred_x(ed);
}

void editor_move_vertical(EditorState *ed, LayoutCache *cache, SDL2_Font *fonts, int dir, bool extend) {
    Block *cur = &ed->doc.blocks[ed->cursorBlock];
    if (block_type_is_table_cell(cur->type)) {
        /* hittest_vertical's curLine +/- 1 is a flat, declaration-order step through
           cache->lines -- correct for ordinary blocks (each produces a vertically-stacked
           sequence of lines) but wrong for a grid: cells are declared row-by-row left-to-right,
           so stepping one adjacent line index from a cell would move to the cell to the right,
           not the one below. Jump directly to the same column in the next/previous row instead. */
        if (ed->preferredX < 0.0f) {
            float x, y, h;
            if (hittest_caret(cache, &ed->doc, fonts, ed->cursorBlock, ed->cursorOffset, &x, &y, &h)) {
                ed->preferredX = x;
            } else {
                ed->preferredX = 0.0f;
            }
        }
        int target = ed->cursorBlock + dir * cur->tableCols;
        if (target >= 0 && target < ed->doc.count && block_type_is_table_cell(ed->doc.blocks[target].type)
            && ed->doc.blocks[target].tableCols == cur->tableCols) {
            ed->cursorBlock = target;
            ed->cursorOffset = hittest_offset_for_block_x(cache, &ed->doc, fonts, target, ed->preferredX);
            snap_anchor_to_cursor(ed, extend);
            return;
        }
        /* Stepped off the top/bottom of the table -- fall through to the normal path below,
           which correctly steps to whatever non-table block precedes/follows it. */
    }

    if (ed->preferredX < 0.0f) {
        float x, y, h;
        if (hittest_caret(cache, &ed->doc, fonts, ed->cursorBlock, ed->cursorOffset, &x, &y, &h)) {
            ed->preferredX = x;
        } else {
            ed->preferredX = 0.0f;
        }
    }
    VerticalMoveResult r = hittest_vertical(cache, &ed->doc, fonts, ed->cursorBlock, ed->cursorOffset, ed->preferredX, dir);
    if (r.found) {
        ed->cursorBlock = r.blockIndex;
        ed->cursorOffset = r.offset;
    }
    snap_anchor_to_cursor(ed, extend);
}

void editor_click(EditorState *ed, LayoutCache *cache, SDL2_Font *fonts, Clay_Vector2 point) {
    HitResult r = hittest_point(cache, &ed->doc, fonts, point);
    ed->cursorBlock = r.blockIndex;
    ed->cursorOffset = r.offset;
    ed->selAnchorBlock = r.blockIndex;
    ed->selAnchorOffset = r.offset;
    reset_preferred_x(ed);
}

void editor_drag_to(EditorState *ed, LayoutCache *cache, SDL2_Font *fonts, Clay_Vector2 point) {
    HitResult r = hittest_point(cache, &ed->doc, fonts, point);
    ed->cursorBlock = r.blockIndex;
    ed->cursorOffset = r.offset;
    reset_preferred_x(ed);
}

bool editor_undo(EditorState *ed) {
    Snapshot s;
    if (!snapshot_stack_pop(&ed->undo.undo, &s)) return false;
    snapshot_stack_push(&ed->undo.redo, &ed->doc, ed->cursorBlock, ed->cursorOffset);
    document_free(&ed->doc);
    ed->doc = s.doc;
    ed->cursorBlock = s.cursorBlock;
    ed->cursorOffset = s.cursorOffset;
    ed->selAnchorBlock = s.cursorBlock;
    ed->selAnchorOffset = s.cursorOffset;
    ed->dirty = true;
    ed->undo.lastKind = EDIT_NONE;
    reset_preferred_x(ed);
    return true;
}

bool editor_redo(EditorState *ed) {
    Snapshot s;
    if (!snapshot_stack_pop(&ed->undo.redo, &s)) return false;
    snapshot_stack_push(&ed->undo.undo, &ed->doc, ed->cursorBlock, ed->cursorOffset);
    document_free(&ed->doc);
    ed->doc = s.doc;
    ed->cursorBlock = s.cursorBlock;
    ed->cursorOffset = s.cursorOffset;
    ed->selAnchorBlock = s.cursorBlock;
    ed->selAnchorOffset = s.cursorOffset;
    ed->dirty = true;
    ed->undo.lastKind = EDIT_NONE;
    reset_preferred_x(ed);
    return true;
}

bool editor_toggle_task(EditorState *ed, int blockIndex) {
    if (blockIndex < 0 || blockIndex >= ed->doc.count) return false;
    Block *b = &ed->doc.blocks[blockIndex];
    if (b->type != BLOCK_TASK_UNCHECKED && b->type != BLOCK_TASK_CHECKED) return false;

    undo_manager_before_edit(&ed->undo, EDIT_TOGGLE_TASK, &ed->doc, ed->cursorBlock, ed->cursorOffset);
    b->type = (b->type == BLOCK_TASK_UNCHECKED) ? BLOCK_TASK_CHECKED : BLOCK_TASK_UNCHECKED;
    ed->dirty = true;
    undo_manager_after_edit(&ed->undo, EDIT_TOGGLE_TASK, ed->cursorBlock, ed->cursorOffset);
    return true;
}

/* Shared by editor_quick_insert_apply/editor_turn_into_apply below. Always targets
   ed->cursorBlock -- both callers are anchored there by construction (the quick-insert popup
   and the Turn Into icon both only ever act on the block the cursor is currently in). */
static void set_block_type_raw(EditorState *ed, BlockType newType) {
    Block *b = &ed->doc.blocks[ed->cursorBlock];
    if (b->type == BLOCK_CODE && newType != BLOCK_CODE) {
        /* No other block type's renderer expects an embedded '\n' -- scrub it the same way
           backspace_raw's code-block demotion already does. */
        for (int i = 0; i < b->text.len; i++) if (b->text.data[i] == '\n') b->text.data[i] = ' ';
        sb_clear(&b->lang);
    }
    if (b->type == BLOCK_IMAGE && newType != BLOCK_IMAGE) {
        sb_clear(&b->alt);
    }
    if (newType == BLOCK_HR) {
        /* HR can't hold a cursor, so give it an empty paragraph to land in right after --
           mirrors the existing "---" + space/Enter autoformat paths exactly. */
        sb_clear(&b->text);
        b->type = BLOCK_HR;
        document_insert_block(&ed->doc, ed->cursorBlock + 1, BLOCK_PARAGRAPH, "", 0);
        ed->cursorBlock++;
        ed->cursorOffset = 0;
    } else {
        b->type = newType;
    }
    ed->dirty = true;
    reset_preferred_x(ed);
}

/* Called from the "@" quick-insert popup: the target block is always just the bare "@word" the
   user typed (see block_type_menu_quick_insert_trigger_active), so it's cleared before the type
   is set -- there's nothing worth preserving. */
void editor_quick_insert_apply(EditorState *ed, BlockType newType) {
    undo_manager_before_edit(&ed->undo, EDIT_QUICK_INSERT, &ed->doc, ed->cursorBlock, ed->cursorOffset);
    Block *b = &ed->doc.blocks[ed->cursorBlock];
    sb_clear(&b->text);
    ed->cursorOffset = 0;
    set_block_type_raw(ed, newType);
    snap_anchor_to_cursor(ed, false);
    undo_manager_after_edit(&ed->undo, EDIT_QUICK_INSERT, ed->cursorBlock, ed->cursorOffset);
}

/* Called from the left-margin "Turn Into" menu: unlike quick-insert, the target block may hold
   real content, which this always preserves (newType == BLOCK_HR is never offered there, since
   HR can't hold any). Ignores any active selection -- Turn Into acts on the whole block. */
void editor_turn_into_apply(EditorState *ed, BlockType newType) {
    undo_manager_before_edit(&ed->undo, EDIT_TURN_INTO, &ed->doc, ed->cursorBlock, ed->cursorOffset);
    set_block_type_raw(ed, newType);
    snap_anchor_to_cursor(ed, false);
    undo_manager_after_edit(&ed->undo, EDIT_TURN_INTO, ed->cursorBlock, ed->cursorOffset);
}

void editor_table_move_cell(EditorState *ed, int dir) {
    Block *b = &ed->doc.blocks[ed->cursorBlock];
    if (!block_type_is_table_cell(b->type)) return;

    int target = ed->cursorBlock + dir;
    if (target >= 0 && target < ed->doc.count && block_type_is_table_cell(ed->doc.blocks[target].type)) {
        ed->cursorBlock = target;
        ed->cursorOffset = 0;
    } else if (dir > 0) {
        if (target < ed->doc.count) {
            ed->cursorBlock = target;
        } else {
            document_insert_block(&ed->doc, ed->doc.count, BLOCK_PARAGRAPH, "", 0);
            ed->cursorBlock = ed->doc.count - 1;
            ed->dirty = true;
        }
        ed->cursorOffset = 0;
    } else if (target >= 0) {
        ed->cursorBlock = target;
        ed->cursorOffset = 0;
    }
    /* else: Shift+Tab from the table's very first cell with nothing before it -- no-op. */

    snap_anchor_to_cursor(ed, false);
    reset_preferred_x(ed);
}

void editor_insert_table(EditorState *ed, int rows, int cols) {
    undo_manager_before_edit(&ed->undo, EDIT_INSERT_TABLE, &ed->doc, ed->cursorBlock, ed->cursorOffset);

    /* Like editor_quick_insert_apply, this is only ever called on the bare "@word" the user just
       typed (see the Table sentinel handling in main.c) -- always replace ed->cursorBlock,
       discarding whatever it held, rather than only when it happens to already be empty. */
    int insertAt = ed->cursorBlock;
    document_remove_block(&ed->doc, ed->cursorBlock);

    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            BlockType type = (r == 0) ? BLOCK_TABLE_HEADER_CELL : BLOCK_TABLE_CELL;
            int idx = insertAt + r * cols + c;
            document_insert_block(&ed->doc, idx, type, "", 0);
            Block *cell = &ed->doc.blocks[idx];
            cell->tableCol = c;
            cell->tableCols = cols;
            cell->tableAlign = 'l';
        }
    }

    ed->cursorBlock = insertAt;
    ed->cursorOffset = 0;
    ed->dirty = true;
    snap_anchor_to_cursor(ed, false);
    undo_manager_after_edit(&ed->undo, EDIT_INSERT_TABLE, ed->cursorBlock, ed->cursorOffset);
}
