/**
 * BillboardUiManager — управление 3D-табличками с GUI (BillboardGui в Roblox).
 *
 * Каждая табличка — это plane-mesh с натянутой DynamicTexture, на которой
 * с помощью обычного Canvas 2D API рисуется содержимое: градиентный фон,
 * иконка, заголовок, подзаголовок, кнопка цены.
 *
 * Поддерживает 4 пресета (template):
 *   - 'shop-item'     — иконка слева, заголовок, "1 > 2", кнопка цены справа
 *   - 'shop-purchase' — иконка + название + цена в рубликах
 *   - 'banner'        — крупная плашка с одним текстом
 *   - 'sign'          — простой указатель с текстом
 *
 * Режимы ориентации:
 *   - 'camera' — всегда смотрит на камеру (BillboardMode.BILLBOARDMODE_ALL)
 *   - 'fixed'  — фиксированная ориентация (используется rotationY mesh-а)
 *
 * Клики:
 *   - на mesh-е ставим pickable=true
 *   - в _handlePick ловим точку пересечения, переводим в UV,
 *     ищем под этой точкой кнопку и эмитим событие
 *
 * State хранится в PrimitiveManager.instances[id].billboard:
 *   {
 *     template: 'shop-item',
 *     face: 'camera',
 *     content: { icon, title, sub, price, gradient: [from, to] }
 *     // или для custom-режима — elements: [...]
 *     onClickHandlers: { 'buy': fn }  // только в Play-режиме
 *   }
 */
import {
    DynamicTexture, StandardMaterial, Color3, Mesh, Texture,
} from '@babylonjs/core';

// Размер текстуры таблички (UV pixels). Чем больше — тем чётче, но больше VRAM.
// 512×320 даёт нормальное качество на расстоянии 2-10 метров.
const TEXTURE_W = 512;
const TEXTURE_H = 320;

// Координаты кнопки в shop-item пресете (для hit-теста кликов).
// Совпадают с тем что рисуется в _renderShopItem (cx, cy, cw, ch).
const SHOP_ITEM_BUTTON = { x: 332, y: 200, w: 160, h: 90 };

// Описания доступных иконок (key → emoji-аналог для Canvas).
// На фронте мы не имеем доступа к специализированным icon-fonts,
// поэтому используем простые символы рисуемые крупно. Можно расширить
// до полноценной библиотеки PNG-иконок позже.
const ICONS = {
    hammer: '🔨',
    saw: '🪚',
    drop: '💧',
    seed: '🌱',
    cube: '🧊',
    coin: '💰',
    home: '🏠',
    rocket: '🚀',
    sprinkler: '⛲',
    sparkle: '✨',
    star: '⭐',
    bag: '🎒',
    diamond: '💎',
    fire: '🔥',
    lightning: '⚡',
    heart: '❤',
    key: '🔑',
    shield: '🛡',
};

export class BillboardUiManager {
    constructor(scene) {
        this.scene = scene;
        // Колбэки от пользовательских скриптов: key=`${id}:${buttonId}` → fn
        this._clickHandlers = new Map();
    }

    /**
     * Применить к существующему mesh-у настройки билборда:
     * — натянуть DynamicTexture, нарисовать контент, выставить billboardMode.
     *
     * data — объект из PrimitiveManager.instances[id], содержит уже mesh, sx, sy, sz.
     * billboardOpts — { template, face, content, elements }
     */
    applyToMesh(data, billboardOpts = {}) {
        const mesh = data.mesh;
        if (!mesh) return;

        const template = billboardOpts.template || 'shop-item';
        const face = billboardOpts.face || 'camera';
        const content = billboardOpts.content || this._defaultContent(template);

        // Создаём DynamicTexture один раз и кешируем на mesh.
        let dyn = mesh.metadata?._billboardTexture;
        if (!dyn) {
            dyn = new DynamicTexture(
                `bb_tex_${data.id}`,
                { width: TEXTURE_W, height: TEXTURE_H },
                this.scene,
                false /* generateMipMaps */
            );
            dyn.hasAlpha = true;
            // Новый StandardMaterial с этой текстурой как diffuseTexture+emissiveTexture
            // (emissive чтобы светилось и без освещения, как UI).
            const mat = new StandardMaterial(`bb_mat_${data.id}`, this.scene);
            mat.diffuseTexture = dyn;
            mat.emissiveTexture = dyn;
            mat.emissiveColor = new Color3(1, 1, 1);
            mat.specularColor = new Color3(0, 0, 0);
            mat.useAlphaFromDiffuseTexture = true;
            mat.backFaceCulling = false;
            mesh.material = mat;
            if (!mesh.metadata) mesh.metadata = {};
            mesh.metadata._billboardTexture = dyn;
            mesh.metadata._billboardMaterial = mat;
        }

        // Ориентация. Babylon-quirk: CreatePlane FRONT в -Z (left-handed),
        // юзер ставит билборд так чтобы лицо смотрело в +Z мира → +π.
        mesh.billboardMode = Mesh.BILLBOARDMODE_NONE;
        if (mesh.metadata._billboardLookObs) {
            this.scene.onBeforeRenderObservable.remove(mesh.metadata._billboardLookObs);
            mesh.metadata._billboardLookObs = null;
        }
        if (face === 'camera') {
            // Ручной look-at — каждый кадр поворачиваем front к камере.
            const obs = this.scene.onBeforeRenderObservable.add(() => {
                if (mesh.isDisposed()) return;
                const cam = this.scene.activeCamera;
                if (!cam) return;
                const dx = cam.position.x - mesh.position.x;
                const dz = cam.position.z - mesh.position.z;
                mesh.rotation.y = Math.atan2(dx, dz) + Math.PI;
            });
            mesh.metadata._billboardLookObs = obs;
        } else {
            // Фиксированная ориентация: front в +Z + пользовательский rotationY.
            const userY = Number.isFinite(billboardOpts.rotationY) ? billboardOpts.rotationY : 0;
            mesh.rotation.y = Math.PI + userY;
            // Двусторонняя табличка: рамка стоит, но при взгляде сзади
            // флипаем UV таблички чтобы текст не был зеркальным.
            const mat = mesh.material;
            if (mat) {
                // Включаем рендер обеих сторон (back-face визуализируется).
                mat.backFaceCulling = false;
            }
            const obs = this.scene.onBeforeRenderObservable.add(() => {
                if (mesh.isDisposed()) return;
                const cam = this.scene.activeCamera;
                if (!cam) return;
                // Локальная нормаль FRONT plane = +Z. Поворот mesh.rotation.y
                // переводит её в world: normalWorld = (sin(ry), 0, cos(ry)).
                const ry = mesh.rotation.y;
                const nWx = Math.sin(ry);
                const nWz = Math.cos(ry);
                // Вектор от mesh к камере
                const vx = cam.position.x - mesh.position.x;
                const vz = cam.position.z - mesh.position.z;
                // Скалярное произведение: >0 — камера смотрит на FRONT,
                // <0 — на BACK (зеркальная UV). Для BACK инвертируем uScale.
                const dot = nWx * vx + nWz * vz;
                const dyn = mesh.metadata?._billboardTexture;
                if (dyn) {
                    // dot > 0 — камера со стороны FRONT-нормали → flip
                    // dot < 0 — камера сзади → нормально
                    if (dot > 0) {
                        if (dyn.uScale !== -1) { dyn.uScale = -1; dyn.uOffset = 1; }
                    } else {
                        if (dyn.uScale !== 1)  { dyn.uScale = 1;  dyn.uOffset = 0; }
                    }
                }
            });
            mesh.metadata._billboardLookObs = obs;
        }
        mesh.scaling.x = Math.abs(mesh.scaling.x || 1);
        mesh.metadata._billboardMirrorX = false;

        // Сохраняем state в data для сериализации и для hit-теста кликов.
        data.billboard = {
            template,
            face,
            content: { ...content },
            elements: billboardOpts.elements || null,
        };

        dyn._kubikonMirrorX = mesh.metadata._billboardMirrorX === true;
        dyn._kubikonOwnerMesh = mesh;
        this._render(dyn, template, content, billboardOpts.elements);
    }

    /**
     * Обновить контент билборда (без пересоздания текстуры).
     * Две формы:
     *   1) update(data, { sub: '2 > 3', price: '$20,000' }) — patch content
     *   2) update(data, 'buy', { text: '$15,000' }) — patch конкретного элемента
     *      по id (для elements-режима ИЛИ для known-id пресета: 'buy', 'title',
     *      'sub', 'price', 'icon', 'gradient' маппятся на поля content).
     */
    update(data, elementIdOrPatch, patchMaybe) {
        if (!data.billboard) return;
        // Форма 2: 3 аргумента (data, elementId, patch)
        if (typeof elementIdOrPatch === 'string' && typeof patchMaybe === 'object' && patchMaybe !== null) {
            const elId = elementIdOrPatch;
            const patch = patchMaybe;
            // Кастомные elements: ищем элемент по id и обновляем его поля.
            if (Array.isArray(data.billboard.elements)) {
                data.billboard.elements = data.billboard.elements.map(el =>
                    el && el.id === elId ? { ...el, ...patch } : el);
            } else {
                // Пресет: мапим известные elementId → ключ content.
                // 'buy' → content.price; 'title'/'sub'/'icon'/'gradient' → одноимённый ключ.
                const c = { ...(data.billboard.content || {}) };
                if (elId === 'buy' && 'text' in patch) {
                    c.price = patch.text;
                } else if (elId in c) {
                    // Если patch имеет text — кладём в content[elId], иначе мерджим поля.
                    if ('text' in patch) c[elId] = patch.text;
                    else Object.assign(c, patch);
                } else {
                    Object.assign(c, patch);
                }
                data.billboard.content = c;
            }
        } else if (typeof elementIdOrPatch === 'object' && elementIdOrPatch !== null) {
            // Форма 1: 2 аргумента (data, patchContent)
            data.billboard.content = { ...data.billboard.content, ...elementIdOrPatch };
        } else {
            return;
        }
        const dyn = data.mesh?.metadata?._billboardTexture;
        if (dyn) {
            this._render(dyn, data.billboard.template, data.billboard.content, data.billboard.elements);
        }
    }

    /** Подписаться на клик по кнопке билборда (только в Play-режиме). */
    onClick(data, buttonId, fn) {
        const key = `${data.id}:${buttonId}`;
        this._clickHandlers.set(key, fn);
    }

    /** Снять все подписки (вызывается при остановке Play). */
    clearHandlers() {
        this._clickHandlers.clear();
    }

    /**
     * Проверить клик по точке UV: вернуть buttonId или null.
     * UV точка — нормализованная (0..1).
     */
    pickButtonAt(data, uvX, uvY) {
        if (!data.billboard) return null;
        // Если текстура в данный момент отзеркалена (face=fixed, смотрим
        // на back-side), uScale=-1 — инвертим UV.x чтобы попасть в правильный
        // canvas-пиксель.
        const dyn = data.mesh?.metadata?._billboardTexture;
        const flipped = dyn && dyn.uScale === -1;
        const uX = flipped ? (1 - uvX) : uvX;
        const px = uX * TEXTURE_W;
        const py = (1 - uvY) * TEXTURE_H;
        // Кастомные elements имеют приоритет (если заданы)
        if (data.billboard.elements) {
            return this._hitTestElements(data.billboard.elements, px, py);
        }
        const tmpl = data.billboard.template;
        if (tmpl === 'shop-item' || tmpl === 'shop-purchase') {
            // Кнопка адаптивной ширины — пересчитываем её rect по тексту
            // именно ЭТОЙ таблички (тем же _computeBuyRect, что и при рисовании).
            const label = (data.billboard.content && data.billboard.content.price) || '$0';
            let b = SHOP_ITEM_BUTTON;
            try {
                const measCtx = (dyn && dyn.getContext && dyn.getContext()) || null;
                if (measCtx) b = this._computeBuyRect(measCtx, label, SHOP_ITEM_BUTTON);
            } catch (e) { /* fallback на базовый rect */ }
            if (px >= b.x && px <= b.x + b.w && py >= b.y && py <= b.y + b.h) {
                return 'buy';
            }
        }
        // banner и sign — кнопок нет, только текст.
        return null;
    }

    /** Вызвать обработчик клика, если он подписан. */
    fireClick(data, buttonId) {
        const key = `${data.id}:${buttonId}`;
        const fn = this._clickHandlers.get(key);
        if (fn) {
            try { fn(); } catch (e) { console.error('[Billboard onClick]', e); }
        }
        // Также пишем кнопку в "нажатом" виде на 100мс для UX-фидбека.
        this._flashButton(data, buttonId);
    }

    _flashButton(data, buttonId) {
        if (!data.billboard) return;
        const dyn = data.mesh?.metadata?._billboardTexture;
        if (!dyn) return;
        // Перерисовываем pressed=true. ВАЖНО: используем СВЕЖИЙ content в callback'е
        // (на момент 120мс content уже может быть обновлён через update — берём
        // актуальный, иначе откатим к старому).
        // Также гарантируем 1 flash на табличку — если предыдущий ещё крутится,
        // отменяем его таймер.
        if (data._flashTimer) {
            clearTimeout(data._flashTimer);
            data._flashTimer = null;
        }
        this._render(dyn, data.billboard.template, data.billboard.content,
            data.billboard.elements, /* pressed */ buttonId);
        data._flashTimer = setTimeout(() => {
            data._flashTimer = null;
            // Берём АКТУАЛЬНЫЕ data.billboard.content/elements — могли обновиться
            // через game.billboard.update() ВО ВРЕМЯ flash'а.
            if (data.mesh?.metadata?._billboardTexture === dyn && data.billboard) {
                this._render(dyn, data.billboard.template, data.billboard.content,
                    data.billboard.elements, null);
            }
        }, 120);
    }

    _defaultContent(template) {
        switch (template) {
            case 'shop-item':
                return { icon: 'hammer', title: 'Апгрейд', sub: '1 > 2',
                    price: '$100', gradient: ['#ff5a5a', '#ff8a3d'] };
            case 'shop-purchase':
                return { icon: 'seed', title: 'Набор семян', sub: 'x3',
                    price: '199 R', gradient: ['#3b82f6', '#0ea5e9'] };
            case 'banner':
                return { title: 'Удвоенный урожай в 17:00',
                    gradient: ['#7c3aed', '#a855f7'] };
            case 'sign':
                return { title: 'Сюда', gradient: ['#1f2937', '#374151'] };
            default:
                return { title: 'Табличка', gradient: ['#1f2937', '#374151'] };
        }
    }

    /** Главная функция рендера — рисует контент на canvas DynamicTexture. */
    _render(dyn, template, content, elements, pressedButtonId) {
        const ctx = dyn.getContext();
        ctx.save();
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.clearRect(0, 0, TEXTURE_W, TEXTURE_H);
        if (elements && Array.isArray(elements)) {
            this._renderElements(ctx, elements, pressedButtonId);
        } else {
            switch (template) {
                case 'shop-item':
                    this._renderShopItem(ctx, content, pressedButtonId);
                    break;
                case 'shop-purchase':
                    this._renderShopPurchase(ctx, content, pressedButtonId);
                    break;
                case 'banner':
                    this._renderBanner(ctx, content);
                    break;
                case 'sign':
                    this._renderSign(ctx, content);
                    break;
                default:
                    this._renderBanner(ctx, content);
            }
        }
        ctx.restore();
        dyn.update(true);
    }

    /** Скруглённый прямоугольник + заливка градиентом + обводка. */
    _roundedGradientRect(ctx, x, y, w, h, opts) {
        const r = opts.radius ?? 24;
        ctx.beginPath();
        ctx.moveTo(x + r, y);
        ctx.arcTo(x + w, y, x + w, y + h, r);
        ctx.arcTo(x + w, y + h, x, y + h, r);
        ctx.arcTo(x, y + h, x, y, r);
        ctx.arcTo(x, y, x + w, y, r);
        ctx.closePath();
        const grad = ctx.createLinearGradient(x, y, x, y + h);
        const [from, to] = opts.gradient || ['#333', '#111'];
        grad.addColorStop(0, from);
        grad.addColorStop(1, to);
        ctx.fillStyle = grad;
        ctx.fill();
        if (opts.stroke) {
            ctx.lineWidth = opts.stroke.width ?? 3;
            ctx.strokeStyle = opts.stroke.color || '#000';
            ctx.stroke();
        }
    }

    /** Рендер пресета shop-item: иконка слева | title + sub | кнопка цены. */
    _renderShopItem(ctx, content, pressedButtonId) {
        // Главная плашка
        this._roundedGradientRect(ctx, 8, 8, TEXTURE_W - 16, TEXTURE_H - 16, {
            gradient: content.gradient || ['#ff5a5a', '#ff8a3d'],
            radius: 28,
            stroke: { color: '#0008', width: 4 },
        });

        // Иконка-слот: круг чуть темнее в левой части
        ctx.beginPath();
        ctx.arc(110, 130, 70, 0, Math.PI * 2);
        ctx.fillStyle = 'rgba(0,0,0,0.18)';
        ctx.fill();

        // Сама иконка (emoji крупно)
        const iconChar = ICONS[content.icon] || ICONS.cube;
        ctx.font = 'bold 96px "Segoe UI Emoji", Arial';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillStyle = '#fff';
        ctx.fillText(iconChar, 110, 132);

        // Заголовок
        ctx.font = 'bold 36px Arial, sans-serif';
        ctx.textAlign = 'left';
        ctx.textBaseline = 'top';
        ctx.fillStyle = '#fff';
        // Лёгкая тень
        ctx.shadowColor = 'rgba(0,0,0,0.45)';
        ctx.shadowBlur = 4;
        ctx.shadowOffsetY = 2;
        ctx.fillText(this._truncate(content.title || '', 18), 200, 50);
        ctx.shadowColor = 'transparent';
        ctx.shadowBlur = 0;
        ctx.shadowOffsetY = 0;

        // Подзаголовок "1 > 2" — зелёный, поменьше
        if (content.sub) {
            ctx.font = 'bold 28px Arial, sans-serif';
            ctx.fillStyle = '#a7f3d0';
            ctx.fillText(content.sub, 200, 105);
        }

        // Кнопка цены — жёлтый прямоугольник внизу справа.
        // Ширина адаптивная: длинный текст («ПРИМЕРИТЬ»/«НАДЕТО») расширяет
        // кнопку ВЛЕВО (правый край прижат к краю таблички), шрифт ужимается
        // если даже расширенная кнопка узка. Фактический rect → _buyRect для hit-теста.
        const pressed = pressedButtonId === 'buy';
        const label = content.price || '$0';
        const rect = this._computeBuyRect(ctx, label, SHOP_ITEM_BUTTON);
        this._roundedGradientRect(ctx, rect.x, rect.y, rect.w, rect.h, {
            gradient: pressed
                ? ['#d97706', '#92400e']
                : ['#fbbf24', '#f59e0b'],
            radius: 16,
            stroke: { color: '#000', width: 3 },
        });
        ctx.font = `bold ${rect.fontSize}px Arial, sans-serif`;
        ctx.fillStyle = pressed ? '#fef3c7' : '#1c1917';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(label, rect.x + rect.w / 2, rect.y + rect.h / 2);
    }

    /**
     * Подобрать прямоугольник кнопки «buy» под текст: правый край прижат к
     * правому краю таблички (как в базовом SHOP_ITEM_BUTTON), ширина растёт
     * влево под длину текста, шрифт ужимается если упёрлись в макс-ширину.
     * Возвращает { x, y, w, h, fontSize }.
     */
    _computeBuyRect(ctx, label, base) {
        const PAD = 36;                 // отступы текста по бокам
        const MAX_W = 300;              // макс ширина кнопки (не залезать на title)
        const rightEdge = base.x + base.w;   // правый край держим на месте
        let fontSize = 36;
        ctx.font = `bold ${fontSize}px Arial, sans-serif`;
        let textW = ctx.measureText(label).width;
        let w = Math.max(base.w, textW + PAD * 2);
        if (w > MAX_W) {
            // Ужимаем шрифт чтобы текст влез в MAX_W.
            w = MAX_W;
            const inner = MAX_W - PAD * 2;
            while (fontSize > 20 && textW > inner) {
                fontSize -= 2;
                ctx.font = `bold ${fontSize}px Arial, sans-serif`;
                textW = ctx.measureText(label).width;
            }
        }
        return { x: rightEdge - w, y: base.y, w, h: base.h, fontSize };
    }

    /** Рендер пресета shop-purchase: похож на shop-item, но иконка крупнее и центрирована. */
    _renderShopPurchase(ctx, content, pressedButtonId) {
        this._roundedGradientRect(ctx, 8, 8, TEXTURE_W - 16, TEXTURE_H - 16, {
            gradient: content.gradient || ['#3b82f6', '#0ea5e9'],
            radius: 28,
            stroke: { color: '#0008', width: 4 },
        });

        const iconChar = ICONS[content.icon] || ICONS.bag;
        ctx.font = 'bold 110px "Segoe UI Emoji", Arial';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillStyle = '#fff';
        ctx.fillText(iconChar, 110, 140);

        ctx.font = 'bold 34px Arial, sans-serif';
        ctx.textAlign = 'left';
        ctx.textBaseline = 'top';
        ctx.fillStyle = '#fff';
        ctx.fillText(this._truncate(content.title || '', 16), 200, 50);

        if (content.sub) {
            ctx.font = 'bold 26px Arial, sans-serif';
            ctx.fillStyle = '#dbeafe';
            ctx.fillText(content.sub, 200, 100);
        }

        // Кнопка-цена (адаптивная ширина под длинный текст, см. _computeBuyRect).
        const pressed = pressedButtonId === 'buy';
        const label = content.price || '0 R';
        const rect = this._computeBuyRect(ctx, label, SHOP_ITEM_BUTTON);
        this._roundedGradientRect(ctx, rect.x, rect.y, rect.w, rect.h, {
            gradient: pressed
                ? ['#9333ea', '#6b21a8']
                : ['#a855f7', '#7c3aed'],
            radius: 16,
            stroke: { color: '#000', width: 3 },
        });
        ctx.font = `bold ${rect.fontSize}px Arial, sans-serif`;
        ctx.fillStyle = '#fff';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(label, rect.x + rect.w / 2, rect.y + rect.h / 2);
    }

    /** Рендер пресета banner: одна крупная фраза по центру. */
    _renderBanner(ctx, content) {
        this._roundedGradientRect(ctx, 8, 8, TEXTURE_W - 16, TEXTURE_H - 16, {
            gradient: content.gradient || ['#7c3aed', '#a855f7'],
            radius: 28,
            stroke: { color: '#0008', width: 4 },
        });

        ctx.font = 'bold 46px Arial, sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillStyle = '#fff';
        ctx.shadowColor = 'rgba(0,0,0,0.45)';
        ctx.shadowBlur = 8;
        ctx.shadowOffsetY = 3;

        // Перенос строк, чтобы длинная фраза влезла
        const lines = this._wrapText(ctx, content.title || '', TEXTURE_W - 80);
        const lh = 56;
        const startY = TEXTURE_H / 2 - (lines.length - 1) * lh / 2;
        for (let i = 0; i < lines.length; i++) {
            ctx.fillText(lines[i], TEXTURE_W / 2, startY + i * lh);
        }
        ctx.shadowColor = 'transparent';
        ctx.shadowBlur = 0;
        ctx.shadowOffsetY = 0;
    }

    /** Рендер пресета sign: компактный указатель. */
    _renderSign(ctx, content) {
        this._roundedGradientRect(ctx, 8, 8, TEXTURE_W - 16, TEXTURE_H - 16, {
            gradient: content.gradient || ['#1f2937', '#374151'],
            radius: 20,
            stroke: { color: '#fff', width: 4 },
        });

        // Заголовок крупно сверху
        ctx.font = 'bold 44px Arial, sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillStyle = '#ffd166';
        const title = content.title || '';
        const subText = content.sub || '';
        if (subText) {
            // Заголовок сверху, sub-строки списком ниже
            ctx.fillText(this._truncate(title, 18), TEXTURE_W / 2, 50);
            // Sub — многострочный, выравнивание по левому краю
            ctx.font = '20px Arial, sans-serif';
            ctx.fillStyle = '#fff';
            ctx.textAlign = 'left';
            ctx.textBaseline = 'top';
            const lines = String(subText).split('\n');
            const startY = 95;
            const lineH = 30;
            const leftX = 38;
            for (let i = 0; i < lines.length && i < 8; i++) {
                ctx.fillText(this._truncate(lines[i], 36), leftX, startY + i * lineH);
            }
        } else {
            ctx.font = 'bold 64px Arial, sans-serif';
            ctx.fillText(this._truncate(title, 14), TEXTURE_W / 2, TEXTURE_H / 2);
        }
    }

    /** Рендер кастомного списка элементов: фон + список text/image/button.
     *  Каждый элемент: { kind, x, y, w, h, ... }
     *    text:   { text, size, color, bold, align }
     *    image:  { src (icon-key), w, h }
     *    button: { id, text, background: {color|gradient, cornerRadius, stroke} }
     */
    _renderElements(ctx, elements, pressedButtonId) {
        // Фоновая плашка — первый элемент типа 'background' (опционально)
        const bg = elements.find(e => e.kind === 'background');
        if (bg) {
            this._roundedGradientRect(ctx, 8, 8, TEXTURE_W - 16, TEXTURE_H - 16, {
                gradient: bg.gradient || ['#1f2937', '#374151'],
                radius: bg.cornerRadius ?? 24,
                stroke: bg.stroke || { color: '#0008', width: 4 },
            });
        }
        // Остальные элементы — поверх фона
        for (const el of elements) {
            if (el.kind === 'background') continue;
            if (el.kind === 'text') {
                ctx.font = `${el.bold ? 'bold ' : ''}${el.size || 24}px Arial, sans-serif`;
                ctx.fillStyle = el.color || '#fff';
                ctx.textAlign = el.align || 'left';
                ctx.textBaseline = 'top';
                ctx.fillText(el.text || '', el.x || 0, el.y || 0);
            } else if (el.kind === 'image') {
                const iconChar = ICONS[el.src] || ICONS.cube;
                const size = Math.min(el.w || 64, el.h || 64);
                ctx.font = `${Math.round(size * 1.1)}px "Segoe UI Emoji", Arial`;
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillStyle = el.color || '#fff';
                ctx.fillText(iconChar, (el.x || 0) + (el.w || 64) / 2,
                    (el.y || 0) + (el.h || 64) / 2);
            } else if (el.kind === 'button') {
                const isPressed = pressedButtonId === el.id;
                const bgSpec = el.background || {};
                this._roundedGradientRect(ctx, el.x || 0, el.y || 0,
                    el.w || 100, el.h || 36, {
                        gradient: bgSpec.gradient ||
                            (bgSpec.color ? [bgSpec.color, bgSpec.color] : ['#fbbf24', '#f59e0b']),
                        radius: bgSpec.cornerRadius ?? 12,
                        stroke: bgSpec.stroke || { color: '#000', width: 2 },
                    });
                ctx.font = `bold ${el.textSize || 28}px Arial, sans-serif`;
                ctx.fillStyle = isPressed ? '#fef3c7' : (el.textColor || '#1c1917');
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText(el.text || '', (el.x || 0) + (el.w || 100) / 2,
                    (el.y || 0) + (el.h || 36) / 2);
            }
        }
    }

    /** Hit-тест для кастомных elements (используется в pickButtonAt). */
    _hitTestElements(elements, px, py) {
        for (const el of elements) {
            if (el.kind !== 'button') continue;
            const x = el.x || 0, y = el.y || 0;
            const w = el.w || 100, h = el.h || 36;
            if (px >= x && px <= x + w && py >= y && py <= y + h) {
                return el.id || null;
            }
        }
        return null;
    }

    _truncate(s, max) {
        if (!s) return '';
        return s.length > max ? s.slice(0, max - 1) + '…' : s;
    }

    _wrapText(ctx, text, maxWidth) {
        const words = (text || '').split(' ');
        const lines = [];
        let cur = '';
        for (const w of words) {
            const test = cur ? cur + ' ' + w : w;
            if (ctx.measureText(test).width <= maxWidth) {
                cur = test;
            } else {
                if (cur) lines.push(cur);
                cur = w;
            }
        }
        if (cur) lines.push(cur);
        return lines.slice(0, 3); // максимум 3 строки
    }

    /** Список доступных иконок (для UI редактора). */
    static getAvailableIcons() {
        return Object.keys(ICONS);
    }

    /** Список доступных пресетов (для UI редактора). */
    static getAvailableTemplates() {
        return [
            { id: 'shop-item', name: 'Магазин: апгрейд', hasButton: true,
              fields: ['icon', 'title', 'sub', 'price', 'gradient'] },
            { id: 'shop-purchase', name: 'Магазин: покупка', hasButton: true,
              fields: ['icon', 'title', 'sub', 'price', 'gradient'] },
            { id: 'banner', name: 'Баннер', hasButton: false,
              fields: ['title', 'gradient'] },
            { id: 'sign', name: 'Указатель', hasButton: false,
              fields: ['title', 'gradient'] },
        ];
    }
}
