/*:
 * @target MZ
 * @plugindesc Full stitched map screenshot with optional exit labels/boxes and robust capture logic. Options toggle at bottom. Press O. NW.js only.
 *
 * @help
 * Features:
 * - Full map screenshot with multi-segment stitching
 * - Optional exit labels and boxes
 * - Robust recheck to ensure no segments are missing
 * - Camera restored after capture
 * - Saves as JPEG in "(FullMapScreenshot)"
 * - Options toggle at bottom
 */

(() => {
    const KEY_O = 79;
    const FRAME_WAIT = 5;
    const CROP_TOP_FRAC = 0.15;
    const CROP_RIGHT_FRAC = 0.35;
    const BOX_X_OFFSET = -4;
    const BOX_Y_OFFSET = -2;
    const MAX_CHARS_PER_LINE = 14;
    const LINE_HEIGHT = 24;

    let capturing = false;

    // --- ConfigManager defaults ---
    if (ConfigManager.mapScreenshotsEnabled === undefined) ConfigManager.mapScreenshotsEnabled = false;
    if (ConfigManager.mapScreenshotsShowLabels === undefined) ConfigManager.mapScreenshotsShowLabels = true;

    const _ConfigManager_makeData = ConfigManager.makeData;
    ConfigManager.makeData = function() {
        const config = _ConfigManager_makeData.call(this);
        config.mapScreenshotsEnabled = this.mapScreenshotsEnabled;
        config.mapScreenshotsShowLabels = this.mapScreenshotsShowLabels;
        return config;
    };

    const _ConfigManager_applyData = ConfigManager.applyData;
    ConfigManager.applyData = function(config) {
        _ConfigManager_applyData.call(this, config);
        this.mapScreenshotsEnabled = config.mapScreenshotsEnabled ?? false;
        this.mapScreenshotsShowLabels = config.mapScreenshotsShowLabels ?? true;
    };

    // --- Options menu patch ---
    const _Scene_Boot_start = Scene_Boot.prototype.start;
    Scene_Boot.prototype.start = function() {
        _Scene_Boot_start.call(this);
        if (!Window_Options.prototype._mapScreenshotPatched) {
            patchOptionsWindow();
            Window_Options.prototype._mapScreenshotPatched = true;
        }
    };

    function patchOptionsWindow() {
        const _Window_Options_makeCommandList = Window_Options.prototype.makeCommandList;
        Window_Options.prototype.makeCommandList = function() {
            _Window_Options_makeCommandList.call(this);
            if (!this._list.some(c => c.symbol === "mapScreenshotsEnabled")) {
                this.addCommand("Map Screenshot Mod (O)", "mapScreenshotsEnabled");
            }
            if (!this._list.some(c => c.symbol === "mapScreenshotsShowLabels")) {
                this.addCommand("  Show Labels/Boxes", "mapScreenshotsShowLabels");
            }
        };
    }

    // --- Hotkey ---
    document.addEventListener("keydown", async e => {
        if (
            e.keyCode === KEY_O &&
            ConfigManager.mapScreenshotsEnabled &&
            !capturing &&
            SceneManager._scene instanceof Scene_Map
        ) {
            capturing = true;
            try {
                await captureFullMap();
            } catch (err) {
                console.error(err);
            }
            capturing = false;
        }
    });

    // --- Capture logic ---
    async function captureFullMap() {
        const scene = SceneManager._scene;
        const spriteset = scene._spriteset;
        const mapW = $gameMap.width() * $gameMap.tileWidth();
        const mapH = $gameMap.height() * $gameMap.tileHeight();
        const screenW = Graphics.width;
        const screenH = Graphics.height;
        const cropTop = Math.floor(screenH * CROP_TOP_FRAC);
        const cropRight = Math.floor(screenW * CROP_RIGHT_FRAC);
        const effW = screenW - cropRight;
        const effH = screenH - cropTop;
        const cols = Math.ceil(mapW / effW);
        const rows = Math.ceil(mapH / effH);
        const canvas = document.createElement("canvas");
        canvas.width = mapW;
        canvas.height = mapH;
        const ctx = canvas.getContext("2d");
        const oldX = $gameMap.displayX();
        const oldY = $gameMap.displayY();
        const oldPlayerX = $gamePlayer.x;
        const oldPlayerY = $gamePlayer.y;

        const exitPoints = collectExitPoints();
        const originalUpdate = Scene_Map.prototype.update;
        Scene_Map.prototype.update = function () {};

        const renderer = Graphics.app.renderer;
        const segments = [];
        for (let y = 0; y < rows; y++) {
            for (let x = 0; x < cols; x++) {
                const dx = (x * effW) / $gameMap.tileWidth();
                const dy = (y * effH) / $gameMap.tileHeight();
                $gameMap.setDisplayPos(dx, dy);
                const tmpCanvas = await captureSegment(spriteset, scene, effW, effH, cropTop, FRAME_WAIT);
                ctx.drawImage(tmpCanvas, x * effW, y * effH);
                segments.push({ x, y, dx, dy, tmpCanvas });
            }
        }

        await verifySegments(canvas, segments, effW, effH, spriteset, scene, cropTop);

        if (ConfigManager.mapScreenshotsShowLabels) {
            drawExitLabels(ctx, exitPoints);
        }

        Scene_Map.prototype.update = originalUpdate;
        $gameMap.setDisplayPos(oldX, oldY);
        $gamePlayer.center(oldPlayerX, oldPlayerY);

        saveCanvas(canvas);
    }

    async function captureSegment(spriteset, scene, effW, effH, cropTop, frameWait) {
        const renderer = Graphics.app.renderer;
        for (let f = 0; f < frameWait; f++) {
            scene.update();
            spriteset.update();
            Graphics.app.render();
            await nextFrame();
        }
        const snapCanvas = renderer.extract.canvas(spriteset);
        const tmp = document.createElement("canvas");
        tmp.width = effW;
        tmp.height = effH;
        tmp.getContext("2d").drawImage(snapCanvas, 0, cropTop, effW, effH, 0, 0, effW, effH);
        return tmp;
    }

    async function verifySegments(canvas, segments, effW, effH, spriteset, scene, cropTop) {
        const ctx = canvas.getContext("2d");
        for (const seg of segments) {
            const data = ctx.getImageData(seg.x * effW, seg.y * effH, effW, effH).data;
            let fullyDrawn = true;
            for (let i = 0; i < data.length; i += 4) {
                if (data[i + 3] === 0) {
                    fullyDrawn = false;
                    break;
                }
            }
            if (!fullyDrawn) {
                $gameMap.setDisplayPos(seg.dx, seg.dy);
                const fixed = await captureSegment(spriteset, scene, effW, effH, cropTop, FRAME_WAIT);
                ctx.drawImage(fixed, seg.x * effW, seg.y * effH);
            }
        }
    }

    function collectExitPoints() {
        const exits = [];
        const currentMapId = $gameMap.mapId();
        for (const ev of $gameMap.events()) {
            if (!ev) continue;
            for (const page of ev.event().pages) {
                for (const cmd of page.list) {
                    if (cmd.code === 201) {
                        const destMapId = cmd.parameters[1];
                        if (destMapId === currentMapId) continue;
                        const mapInfo = $dataMapInfos[destMapId];
                        if (mapInfo) exits.push({ name: mapInfo.name, x: ev.x, y: ev.y });
                        break;
                    }
                }
            }
        }
        return exits;
    }

    function drawExitLabels(ctx, exits) {
        const tw = $gameMap.tileWidth();
        const th = $gameMap.tileHeight();
        ctx.font = "22px sans-serif";
        ctx.textAlign = "center";
        ctx.textBaseline = "bottom";
        const visited = new Set();
        const clusters = [];
        for (let i = 0; i < exits.length; i++) {
            if (visited.has(i)) continue;
            const queue = [i];
            const cluster = [];
            while (queue.length) {
                const idx = queue.shift();
                if (visited.has(idx)) continue;
                visited.add(idx);
                cluster.push(exits[idx]);
                for (let j = 0; j < exits.length; j++) {
                    if (visited.has(j)) continue;
                    if (exits[j].name !== exits[idx].name) continue;
                    const dx = Math.abs(exits[idx].x - exits[j].x);
                    const dy = Math.abs(exits[idx].y - exits[j].y);
                    if (Math.max(dx, dy) <= 1) queue.push(j);
                }
            }
            clusters.push(cluster);
        }

        for (const cluster of clusters) {
            cluster.sort((a, b) => (a.y - b.y) || (a.x - b.x));
            const mid = cluster[Math.floor(cluster.length / 2)];
            ctx.lineWidth = 2;
            ctx.strokeStyle = "red";
            for (const ex of cluster) {
                const x = (ex.x + BOX_X_OFFSET) * tw;
                const y = (ex.y + BOX_Y_OFFSET) * th;
                ctx.strokeRect(x, y, tw, th);
            }
            const lines = wrapTextWords(mid.name, MAX_CHARS_PER_LINE);
            const blockHeight = lines.length * LINE_HEIGHT;
            let px = (mid.x + BOX_X_OFFSET + 0.5) * tw;
            let baseY = (mid.y + BOX_Y_OFFSET) * th - 4;
            const halfWidth = Math.max(...lines.map(l => ctx.measureText(l).width)) / 2;
            px = Math.max(halfWidth, Math.min(ctx.canvas.width - halfWidth, px));
            if (baseY < blockHeight) baseY = blockHeight;
            for (let i = 0; i < lines.length; i++) {
                const y = baseY - (blockHeight - i * LINE_HEIGHT);
                ctx.lineWidth = 6;
                ctx.strokeStyle = "black";
                ctx.strokeText(lines[i], px, y);
                ctx.lineWidth = 4;
                ctx.strokeText(lines[i], px, y);
                ctx.fillStyle = "#b862ffff";
                ctx.fillText(lines[i], px, y);
            }
        }
    }

    function wrapTextWords(text, maxChars) {
        const words = text.split(" ");
        const lines = [];
        let current = "";
        for (const word of words) {
            if ((current + (current ? " " : "") + word).length <= maxChars) {
                current += (current ? " " : "") + word;
            } else {
                if (current) lines.push(current);
                current = word;
            }
        }
        if (current) lines.push(current);
        return lines;
    }

    function nextFrame() {
        return new Promise(r => requestAnimationFrame(r));
    }

    function saveCanvas(canvas) {
        if (!Utils.isNwjs()) return;
        const fs = require("fs");
        const path = require("path");
        const dir = path.join(process.cwd(), "(FullMapScreenshot)");
        if (!fs.existsSync(dir)) fs.mkdirSync(dir);
        const file = path.join(dir, `map_${$gameMap.mapId()}_${Date.now()}.jpg`);
        const data = canvas.toDataURL("image/jpeg", 0.9).replace(/^data:image\/jpeg;base64,/, "");
        fs.writeFileSync(file, Buffer.from(data, "base64"));
    }

})();