/**
 * ScriptSandbox — main-side обёртка над одним Web Worker'ом, исполняющим
 * пользовательский скрипт.
 *
 * Использование:
 *   const sandbox = new ScriptSandbox(code);
 *   sandbox.setOnCommand((cmd, payload) => { ... });   // команды от Worker'а
 *   sandbox.start();                                    // запустить
 *   sandbox.tick(dt, sceneState);                       // вызывать каждый кадр
 *   sandbox.stop();                                     // остановить и освободить Worker
 *
 * Команды от Worker'а:
 *   { cmd: 'log', payload: { level, text } }
 *   { cmd: 'player.teleport', payload: { x, y, z } }
 *   { cmd: 'boot', payload: null }     — Worker запустился
 *   { cmd: 'ready', payload: null }    — user-код выполнен (top-level)
 *
 * Защита:
 *   - watchdog 1 секунда: если Worker не отвечает на ping → terminate
 *   - terminate при остановке гарантированно убивает таймеры/циклы
 */

import { getWorkerSourceUrl } from './ScriptSandboxWorker';

let _workerUrl = null;

export class ScriptSandbox {
    constructor(code, target = null) {
        this.code = code || '';
        this.target = target;          // {kind, ref} — объект-носитель скрипта
        this.worker = null;
        this._onCommand = null;
        this._isReady = false;
        this._isStopped = false;
        this._pendingEvents = [];      // события до ready
    }

    setOnCommand(cb) { this._onCommand = cb; }

    start() {
        if (this.worker) return;
        if (!_workerUrl) _workerUrl = getWorkerSourceUrl();
        // eslint-disable-next-line no-console
        console.log('[ScriptSandbox] creating Worker, code length:', this.code.length);
        this.worker = new Worker(_workerUrl);
        this.worker.onmessage = (e) => this._handleMessage(e);
        this.worker.onerror = (err) => {
            // eslint-disable-next-line no-console
            console.error('[ScriptSandbox] Worker error', err);
            this._emit('log', {
                level: 'error',
                text: `Ошибка Worker'а: ${err.message || err}`,
            });
        };
        // Передаём код + target (если есть). Worker внутри сам выполнит его в new Function(game, code).
        // initialScene — первичный snapshot сцены, чтобы findOne() работал
        // в синхронном теле скрипта на старте (до прихода sceneSnapshot в rAF).
        this.worker.postMessage({
            cmd: 'init',
            payload: {
                code: this.code,
                target: this.target,
                selfPosition: this._initialSelfPosition || null,
                modules: this._modules || {},
                initialScene: this._initialScene || null,
            },
        });
    }

    /** Установить начальную позицию self до start (для первого render Tick). */
    setInitialSelfPosition(p) {
        this._initialSelfPosition = p;
    }

    /** Первичный snapshot сцены (до start) — чтобы findOne работал на старте. */
    setInitialScene(snap) {
        this._initialScene = snap;
    }

    /** Установить код модулей для game.require — { 'имя': 'код' }. Вызывать до start(). */
    setModules(modules) {
        this._modules = modules || {};
    }

    _handleMessage(e) {
        if (this._isStopped) return;
        const { cmd, payload } = e.data || {};
        if (cmd === 'boot') return;
        if (cmd === 'ready') {
            this._isReady = true;
            // Доставим pending snapshot'ы (приходили до ready)
            if (this._pendingSceneSnapshot) {
                try { this.worker.postMessage({ cmd: 'sceneSnapshot', payload: this._pendingSceneSnapshot }); } catch (e) {}
                this._pendingSceneSnapshot = null;
            }
            if (this._pendingGuiSnapshot) {
                try { this.worker.postMessage({ cmd: 'guiSnapshot', payload: this._pendingGuiSnapshot }); } catch (e) {}
                this._pendingGuiSnapshot = null;
            }
            if (this._pendingTerrainHM) {
                try { this.worker.postMessage({ cmd: 'terrainHeightmap', payload: this._pendingTerrainHM }); } catch (e) {}
                this._pendingTerrainHM = null;
            }
            if (this._pendingDataSnapshot) {
                try { this.worker.postMessage({ cmd: 'dataSnapshot', payload: this._pendingDataSnapshot }); } catch (e) {}
                this._pendingDataSnapshot = null;
            }
            if (this._pendingSkinsSnapshot) {
                try { this.worker.postMessage({ cmd: 'skinsSnapshot', payload: this._pendingSkinsSnapshot }); } catch (e) {}
                this._pendingSkinsSnapshot = null;
            }
            // Доставим события которые пришли до готовности
            if (this._pendingEvents.length > 0) {
                for (const ev of this._pendingEvents) {
                    const isGlobal = ev.__global;
                    if (isGlobal) {
                        const { __global, ...payload } = ev;
                        try { this.worker.postMessage({ cmd: 'globalEvent', payload }); } catch (e) {}
                    } else {
                        try { this.worker.postMessage({ cmd: 'event', payload: ev }); } catch (e) {}
                    }
                }
                this._pendingEvents = [];
            }
            return;
        }
        this._emit(cmd, payload);
    }

    /**
     * Доставить событие в Worker (например click/touch объекта).
     * Если Worker ещё не ready — буферим.
     */
    sendEvent(payload) {
        if (!this.worker) return;
        if (!this._isReady) {
            this._pendingEvents.push(payload);
            return;
        }
        try { this.worker.postMessage({ cmd: 'event', payload }); } catch (e) {}
    }

    /**
     * Глобальное событие (key/click/playerTouch) — доставляется всем sandbox'ам.
     * Отличается от sendEvent тем что Worker роутит его в _global*Handlers.
     */
    sendGlobalEvent(payload) {
        if (!this.worker) return;
        if (!this._isReady) {
            this._pendingEvents.push({ __global: true, ...payload });
            return;
        }
        try { this.worker.postMessage({ cmd: 'globalEvent', payload }); } catch (e) {}
    }

    /**
     * Snapshot сцены — отправляется всем sandbox'ам, чтобы game.scene.find/all/getPosition
     * могли работать синхронно.
     */
    sendSceneSnapshot(snapshot) {
        if (!this.worker) return;
        if (!this._isReady) {
            // Запоминаем последний snapshot — отдадим в _handleMessage когда придёт ready
            this._pendingSceneSnapshot = snapshot;
            return;
        }
        try { this.worker.postMessage({ cmd: 'sceneSnapshot', payload: snapshot }); } catch (e) {}
    }

    /** Snapshot GUI-элементов — для game.gui.find/get/all и game.self.props (gui). */
    sendGuiSnapshot(snapshot) {
        if (!this.worker) return;
        if (!this._isReady) {
            this._pendingGuiSnapshot = snapshot;
            return;
        }
        try { this.worker.postMessage({ cmd: 'guiSnapshot', payload: snapshot }); } catch (e) {}
    }

    /** Snapshot атрибутов объектов — для синхронного game.scene.getData. */
    sendDataSnapshot(snapshot) {
        if (!this.worker) return;
        if (!this._isReady) {
            this._pendingDataSnapshot = snapshot;
            return;
        }
        try { this.worker.postMessage({ cmd: 'dataSnapshot', payload: snapshot }); } catch (e) {}
    }

    /** Задача 07: снапшот скинов — для game.player.getAvailableSkins/getAllSkins. */
    sendSkinsSnapshot(snapshot) {
        if (!this.worker) return;
        if (!this._isReady) {
            this._pendingSkinsSnapshot = snapshot;
            return;
        }
        try { this.worker.postMessage({ cmd: 'skinsSnapshot', payload: snapshot }); } catch (e) {}
    }

    /**
     * Карта высот гладкого ландшафта — для game.scene.surfaceY(x,z).
     * Шлётся один раз (террейн не меняется в Play). Формат:
     *   { origin:{x,z}, step, cols, rows, heights:Float32Array|number[] }
     */
    sendTerrainHeightmap(hm) {
        if (!this.worker) return;
        if (!this._isReady) {
            this._pendingTerrainHM = hm;
            return;
        }
        try { this.worker.postMessage({ cmd: 'terrainHeightmap', payload: hm }); } catch (e) {}
    }

    _emit(cmd, payload) {
        if (this._onCommand) {
            try { this._onCommand(cmd, payload); } catch (e) { /* ignore */ }
        }
    }

    /**
     * Вызвать onTick-хэндлеры пользователя.
     * sceneState — { player: { position: {x,y,z} } } для зеркалирования в worker.
     */
    tick(dt, sceneState) {
        if (!this.worker || !this._isReady) return;
        this.worker.postMessage({
            cmd: 'tick',
            payload: { dt, ...sceneState },
        });
    }

    /** Обновить state в worker без tick (на паузе). */
    syncState(sceneState) {
        if (!this.worker || !this._isReady) return;
        this.worker.postMessage({ cmd: 'state', payload: sceneState });
    }

    stop() {
        this._isStopped = true;
        this._isReady = false;
        if (this.worker) {
            try { this.worker.postMessage({ cmd: 'stop' }); } catch (e) { /* ignore */ }
            try { this.worker.terminate(); } catch (e) { /* ignore */ }
            this.worker = null;
        }
    }
}
