// clipboard.js — Phase 4 of editable-cell mode.
//
// Bidirectional clipboard interop with Excel / Google Sheets / any
// other spreadsheet that uses RFC-4180-ish TSV on the text/plain
// clipboard mime.
//
// Copy: when a single cell is selected, Ctrl/Cmd+C writes that
// cell's value as plain text. Range selection (Phase 5) extends
// this to a TSV rectangle.
//
// Paste: Ctrl/Cmd+V on the focused cell parses text/plain as TSV
// (tabs between columns, newlines between rows; embedded newlines
// or tabs are quoted with double-quotes; doubled "" escapes).
//
//   - 1×1 clipboard into selected cell → writes that one cell.
//   - N×M clipboard into selected cell → SPILLS from the anchor
//     cell down/right to (anchor.row + N - 1, anchor.col + M - 1).
//     Out-of-bounds cells are silently dropped (Excel convention).
//
// Each pasted cell goes through the same draft-buffer write path
// as a normal edit — the row-blur save trigger picks them up,
// and the per-cell schema-driven coercion (Phase 2) applies.
// Per-cell validation runs on the next save attempt; invalid
// cells get the red-corner mark.
(function (app) {
    'use strict';

    function editor() { return app.modules.editor; }

    // --- TSV parsing --------------------------------------------------

    // parseTSV(text) → string[][]. Honors RFC-4180-ish quoting:
    //   - A field surrounded by " can contain tabs, newlines, and
    //     literal " characters escaped as "".
    //   - An unquoted field ends at the next tab, newline, or end.
    //   - Bare \r is treated as part of \r\n (Windows line endings).
    function parseTSV(text) {
        const rows = [];
        let row = [];
        let field = '';
        let inQuotes = false;
        const s = String(text == null ? '' : text);

        for (let i = 0; i < s.length; i++) {
            const ch = s[i];
            if (inQuotes) {
                if (ch === '"') {
                    if (s[i + 1] === '"') {
                        // Escaped quote inside a quoted field.
                        field += '"';
                        i++;
                    } else {
                        // End of quoted field.
                        inQuotes = false;
                    }
                } else {
                    field += ch;
                }
                continue;
            }
            if (ch === '"' && field === '') {
                // Open quote — only at start of field.
                inQuotes = true;
                continue;
            }
            if (ch === '\t') {
                row.push(field);
                field = '';
                continue;
            }
            if (ch === '\n' || ch === '\r') {
                // \r\n — consume the \n too.
                if (ch === '\r' && s[i + 1] === '\n') i++;
                row.push(field);
                field = '';
                rows.push(row);
                row = [];
                continue;
            }
            field += ch;
        }
        // Trailing field (no terminator).
        if (field.length > 0 || row.length > 0) {
            row.push(field);
            rows.push(row);
        }
        // Excel often appends a trailing empty row from the final \n;
        // drop one trailing all-empty row to match that convention.
        if (rows.length > 0) {
            const last = rows[rows.length - 1];
            if (last.length === 1 && last[0] === '') rows.pop();
        }
        return rows;
    }

    // formatTSV(grid) → string. Reverse of parseTSV. Quotes any
    // field containing tab, newline, or double-quote.
    function formatTSV(grid) {
        const lines = [];
        for (let r = 0; r < grid.length; r++) {
            const row = grid[r];
            const cells = [];
            for (let c = 0; c < row.length; c++) {
                cells.push(formatCell(row[c]));
            }
            lines.push(cells.join('\t'));
        }
        return lines.join('\n');
    }

    function formatCell(v) {
        const s = (v == null) ? '' : String(v);
        if (/[\t\n\r"]/.test(s)) {
            return '"' + s.replace(/"/g, '""') + '"';
        }
        return s;
    }

    // --- Apply paste --------------------------------------------------

    function applyPaste(anchorRowIdx, anchorColIdx, grid) {
        // grid is string[][]. Returns {applied: int, skipped: int}.
        const ed = editor();
        const totalRows = visibleRowCount();
        const cols = (app.context && app.context.columns) || [];
        const totalCols = cols.length;
        let applied = 0, skipped = 0;

        for (let r = 0; r < grid.length; r++) {
            const dstR = anchorRowIdx + r;
            if (dstR >= totalRows) { skipped += grid[r].length; continue; }
            const row = rowDataAtIndex(dstR);
            if (!row) { skipped += grid[r].length; continue; }
            for (let c = 0; c < grid[r].length; c++) {
                const dstC = anchorColIdx + c;
                if (dstC >= totalCols) { skipped++; continue; }
                const col = cols[dstC];
                if (!col) { skipped++; continue; }
                const newValue = coerceCell(grid[r][c], col, row);
                ed.setDraft(ed.rowKey(row), col.field, newValue);
                applied++;
            }
        }
        return { applied: applied, skipped: skipped };
    }

    function visibleRowCount() {
        return document.querySelectorAll('#table-root tbody > tr').length;
    }

    function rowDataAtIndex(r) {
        const tr = document.querySelectorAll('#table-root tbody > tr')[r];
        if (!tr) return null;
        const rowId = tr.getAttribute('data-row-id');
        if (rowId == null) return null;
        const all = (app.state && app.state.rows) || [];
        for (let i = 0; i < all.length; i++) {
            if (editor().rowKey(all[i]) === rowId) return all[i];
        }
        return null;
    }

    function coerceCell(raw, col, _row) {
        // Phase 2's editor coerces values typed into a number/checkbox/
        // select widget. Pasted cells arrive as raw strings; coerce
        // here so the draft holds the right JS type. Falls back to the
        // raw string when coercion is ambiguous.
        const fmt = col.format;
        if (fmt === 'number' || fmt === 'integer' || isNumericSchema(col)) {
            const n = Number(raw);
            if (raw.trim() !== '' && !Number.isNaN(n)) return n;
        }
        if (isBooleanSchema(col)) {
            const t = String(raw).trim().toLowerCase();
            if (t === 'true' || t === 'yes' || t === '1') return true;
            if (t === 'false' || t === 'no' || t === '0' || t === '') return false;
        }
        return raw;
    }

    function isNumericSchema(col) {
        const s = propSchema(col);
        return !!(s && (s.type === 'number' || s.type === 'integer'));
    }

    function isBooleanSchema(col) {
        const s = propSchema(col);
        return !!(s && s.type === 'boolean');
    }

    function propSchema(col) {
        const ctx = app.context || {};
        if (!ctx.rowSchema || !ctx.rowSchema.properties) return null;
        return ctx.rowSchema.properties[col.field] || null;
    }

    // --- Event handlers ----------------------------------------------

    function onPaste(ev) {
        if (!app.state || !app.state.selected) return;
        if (app.state.editing) return; // input owns its own paste
        const text = ev.clipboardData && ev.clipboardData.getData('text/plain');
        if (!text) return;
        ev.preventDefault();
        const grid = parseTSV(text);
        if (!grid.length) return;
        const { row: r, col: c } = app.state.selected;
        const result = applyPaste(r, c, grid);
        // Trigger a re-paint so draft values display.
        if (typeof app.repaint === 'function') app.repaint();
        if (result.skipped > 0) {
            notifyToast(
                'Pasted ' + result.applied + ' cell' + plural(result.applied) +
                '; ' + result.skipped + ' dropped (out of bounds)'
            );
        }
    }

    function onCopy(ev) {
        if (!app.state || !app.state.selected) return;
        if (app.state.editing) return; // input owns its own copy
        const { row: r, col: c } = app.state.selected;
        const row = rowDataAtIndex(r);
        const cols = (app.context && app.context.columns) || [];
        const col = cols[c];
        if (!row || !col) return;
        const value = editor().effectiveCellValue(row, col);
        ev.preventDefault();
        if (ev.clipboardData) {
            ev.clipboardData.setData('text/plain', formatCell(value));
        }
    }

    function plural(n) { return n === 1 ? '' : 's'; }

    function notifyToast(msg) {
        // Cheap toast: write to #table-status, auto-clear after 4s.
        // Coexists with save.js's stale-row prompt — just don't fire
        // if a prompt is currently up.
        const el = document.getElementById('table-status');
        if (!el) return;
        if (el.classList.contains('table-status--prompt')) return;
        el.textContent = msg;
        el.hidden = false;
        clearTimeout(notifyToast._t);
        notifyToast._t = setTimeout(() => {
            if (el.textContent === msg) {
                el.hidden = true;
                el.textContent = '';
            }
        }, 4000);
    }

    function attach() {
        // Listen at the document level so paste events bubble from
        // any cell with focus. No element-specific binding because
        // Phase 1's roving tabindex moves focus around.
        document.addEventListener('paste', onPaste);
        document.addEventListener('copy', onCopy);
    }

    // Auto-wire on bootstrap. table-mode only — the dispatcher hides
    // form-mode in this bundle, but be defensive if both modes ever
    // coexist on a page (test fixtures): attach unconditionally; the
    // handler bails when there's no selected cell.
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', attach, { once: true });
    } else {
        attach();
    }

    app.modules.clipboard = {
        parseTSV: parseTSV,
        formatTSV: formatTSV,
        applyPaste: applyPaste,
    };
})(window.tablesApp);
