/**
 * LabelManager — billboard-плашки (текст-надписи) над 3D-объектами.
 *
 * game.scene.setLabel(ref, text, opts) — имена/HP/таймеры/счётчики над
 * персонажами, врагами, предметами. По умолчанию плашка повёрнута лицом к
 * камере (billboardMode=7), всегда поверх геометрии (renderingGroupId=1).
 *
 * Задача 10 — расширенные стили: фон/обводка/скругление (пресеты gameui/
 * warning/reward/boss-hp/plain), обводка текста, richText (<color>/<b>/<size>),
 * faceMode billboard|fixed, attachPoint, maxDistance.
 *
 * Плашка привязывается к мешу объекта (parent) и висит над ним.
 */
import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture';
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
import { Color3 } from '@babylonjs/core/Maths/math.color';
import { Mesh } from '@babylonjs/core/Meshes/mesh';

// === Пресеты стилей плашки (фон/обводка/текст) ===
// gameui — жёлтая обводка + тёмно-синий фон + белый текст (как в Roblox-UI).
export const LABEL_PRESETS = {
    plain: {
        background: null, borderColor: null, borderWidth: 0, cornerRadius: 0,
        color: '#ffffff', textStroke: { color: '#000', width: 8 },
    },
    gameui: {
        background: '#1a3a8a', borderColor: '#f5c020', borderWidth: 10, cornerRadius: 28,
        color: '#ffffff', textStroke: { color: '#0a1430', width: 6 },
    },
    warning: {
        background: '#b01e1e', borderColor: '#ffce3a', borderWidth: 10, cornerRadius: 28,
        color: '#ffffff', textStroke: { color: '#000', width: 6 },
    },
    reward: {
        background: '#caa018', borderColor: '#fff0a0', borderWidth: 10, cornerRadius: 28,
        color: '#fff8e0', textStroke: { color: '#6b4e00', width: 6 },
        gradient: ['#f7d34a', '#c98a18'],   // золотой градиент фона
    },
    'boss-hp': {
        background: '#3a0a0a', borderColor: '#ff4040', borderWidth: 8, cornerRadius: 20,
        color: '#ffd0d0', textStroke: { color: '#000', width: 6 },
        gradient: ['#8a1414', '#3a0a0a'],
    },
};

export class LabelManager {
    constructor(scene) {
        this.scene = scene;
        // ref-строка объекта → { plane, tex, mat, lastKey, opts }
        this.labels = new Map();
        this._playerMesh = null;   // для maxDistance — задаётся из BabylonScene
    }

    /** Дать ссылку на меш игрока (для maxDistance-скрытия). */
    setPlayerMesh(mesh) { this._playerMesh = mesh; }

    /**
     * Установить/обновить плашку над объектом.
     *   ref        — ref-строка объекта.
     *   anchorMesh — Babylon-меш объекта (плашка крепится к нему).
     *   text       — текст (может содержать richText-теги если opts.richText).
     *   opts       — см. LABEL_PRESETS + { color, height, size, background,
     *                borderColor, borderWidth, cornerRadius, padding, textStroke,
     *                fontWeight, faceMode, rotationY, attachPoint, preset,
     *                richText, maxDistance }
     */
    setLabel(ref, anchorMesh, text, opts = {}) {
        if (!anchorMesh) return;
        text = String(text == null ? '' : text);

        // Пресет → база, поверх — явные opts.
        const preset = opts.preset && LABEL_PRESETS[opts.preset] ? LABEL_PRESETS[opts.preset] : null;
        const st = { ...(preset || {}), ...opts };
        const color = st.color || '#ffffff';
        const heightAbove = Number.isFinite(opts.height) ? opts.height : 2.5;
        const sizeMul = Number.isFinite(opts.size) ? opts.size : 1;
        const richText = !!opts.richText;

        // Диф-чек: если ничего не поменялось — не пересоздаём (важно для bindLabel).
        const styleKey = JSON.stringify({ text, color, preset: opts.preset, bg: st.background,
            bc: st.borderColor, bw: st.borderWidth, cr: st.cornerRadius, rich: richText,
            fw: st.fontWeight, h: heightAbove, sz: sizeMul, fm: st.faceMode, ry: st.rotationY,
            af: st.attachFace, tl: st.tilt, ap: typeof st.attachPoint === 'string' ? st.attachPoint : null });
        const existing = this.labels.get(ref);
        if (existing && existing.lastKey === styleKey && existing.plane.parent === anchorMesh) {
            return; // ничего не изменилось
        }

        // Меняется только текст (тот же стиль/размер) → перерисуем canvas без
        // пересоздания меша (дешевле). Иначе — полное пересоздание.
        const sameStruct = existing && existing.styleStruct === this._structKey(st, richText, heightAbove, sizeMul);
        if (sameStruct) {
            this._drawCanvas(existing.tex, text, color, st, richText);
            existing.tex.update(true);
            existing.lastKey = styleKey;
            existing.lastText = text;
            return;
        }

        this.clearLabel(ref);

        // Размер текстуры: чем больше текста — тем шире, чтобы не растягивать.
        const fontPx = 120;
        const W = 1024, H = 256;
        const tex = new DynamicTexture(`lblTex_${ref}_${this._uid()}`,
            { width: W, height: H }, this.scene, true);
        tex.updateSamplingMode?.(3);   // TRILINEAR
        tex.anisotropicFilteringLevel = 8;
        tex.hasAlpha = true;
        this._drawCanvas(tex, text, color, st, richText);
        tex.update(true);

        // Соотношение плашки — 4:1 (1024×256). Крупная и читаемая (как в Roblox).
        // ОДНОСТОРОННЯЯ плоскость (FRONTSIDE): лицо = нормаль +Z. Мы всегда
        // разворачиваем плашку лицом к наблюдателю снаружи грани, поэтому текст
        // читается прямо. DOUBLESIDE НЕ используем — задняя грань зеркалит UV
        // (баг: «Ключ от тайника» наоборот). backFaceCulling выключен только
        // чтобы плашка не пропадала при взгляде сзади (без отражённого текста).
        const plane = MeshBuilder.CreatePlane(`lbl_${ref}`,
            { width: 3.4 * sizeMul, height: 0.85 * sizeMul,
              sideOrientation: Mesh.FRONTSIDE }, this.scene);
        const mat = new StandardMaterial(`lblMat_${ref}`, this.scene);
        mat.diffuseTexture = tex;
        mat.diffuseTexture.hasAlpha = true;
        mat.emissiveColor = new Color3(1, 1, 1);
        mat.diffuseColor = new Color3(0, 0, 0);
        mat.disableLighting = true;
        // Геометрия двусторонняя (DOUBLESIDE) с прямыми backUVs — culling можно
        // включить, дублей нет; текст читается с обеих сторон без зеркала.
        mat.backFaceCulling = false;
        mat.disableDepthWrite = true;
        mat.useAlphaFromDiffuseTexture = true;
        plane.material = mat;
        plane.renderingGroupId = 1;
        plane.isPickable = false;
        plane.parent = anchorMesh;

        // Полуразмеры объекта по каждой оси (для крепления над верхом ИЛИ на
        // грань). Берём bounding box в ЛОКАЛЬНЫХ координатах меша (min/max), чтобы
        // позиция плашки-ребёнка была верной при любом масштабе/вращении родителя.
        let halfX = 0.5, halfY = 0.5, halfZ = 0.5;
        try {
            const bb = anchorMesh.getBoundingInfo?.().boundingBox;
            if (bb && bb.minimum && bb.maximum) {
                halfX = (bb.maximum.x - bb.minimum.x) / 2;
                halfY = (bb.maximum.y - bb.minimum.y) / 2;
                halfZ = (bb.maximum.z - bb.minimum.z) / 2;
            } else if (anchorMesh.scaling) {
                halfX = Math.abs(anchorMesh.scaling.x) / 2;
                halfY = Math.abs(anchorMesh.scaling.y) / 2;
                halfZ = Math.abs(anchorMesh.scaling.z) / 2;
            }
        } catch (e) { /* ignore */ }
        const halfH = halfY;
        const halfPlane = 0.45 * sizeMul;   // полувысота самой плашки (3.4×0.85)

        // attachFace — ПРИКРЕПИТЬ плашку НА ГРАНЬ объекта (как табличка на
        // стене/полу): плашка лежит В ПЛОСКОСТИ грани, фиксированной ориентации,
        // и перемещается/вращается ВМЕСТЕ с объектом (она его ребёнок). Это
        // Roblox-style «надпись = часть постройки» (в отличие от billboard над
        // верхом). Значения: 'top'|'bottom'|'front'|'back'|'left'|'right'
        // (или сырые '+y'/'-y'/'+z'/'-z'/'+x'/'-x').
        const FACE = { top: '+y', bottom: '-y', front: '+z', back: '-z',
            right: '+x', left: '-x' };
        let face = st.attachFace;
        if (face && FACE[face]) face = FACE[face];

        if (face) {
            // На грань — всегда фиксированная ориентация (не billboard), иначе
            // «связки с примитивом» не будет (плашка крутилась бы к камере).
            plane.billboardMode = 0;
            const gap = Number.isFinite(opts.height) ? opts.height : 0.05;
            // ВАЖНО: Babylon CreatePlane (FRONTSIDE) лицевой стороной (где UV/текст
            // не зеркалятся) смотрит в −Z. Поэтому чтобы ЛИЦО таблички смотрело
            // НАРУЖУ выбранной грани, поворачиваем плоскость так, чтобы её −Z
            // совпал с внешней нормалью грани. tiltSign — знак наклона tilt с
            // учётом того, что для грани +z плоскость развёрнута на π.
            let tiltSign = 1;
            if (face === '+z')      { plane.position.set(0, 0,  halfZ + gap); plane.rotation.set(0, Math.PI, 0); tiltSign = -1; }
            else if (face === '-z') { plane.position.set(0, 0, -(halfZ + gap)); plane.rotation.set(0, 0, 0); }
            else if (face === '+x') { plane.position.set( halfX + gap, 0, 0); plane.rotation.set(0, -Math.PI / 2, 0); }
            else if (face === '-x') { plane.position.set(-(halfX + gap), 0, 0); plane.rotation.set(0,  Math.PI / 2, 0); }
            else if (face === '+y') { plane.position.set(0,  halfY + gap, 0); plane.rotation.set( Math.PI / 2, 0, 0); }
            else if (face === '-y') { plane.position.set(0, -(halfY + gap), 0); plane.rotation.set(-Math.PI / 2, 0, 0); }
            if (Number.isFinite(st.rotationY)) plane.rotation.y += st.rotationY;
            // tilt — наклон таблички вокруг локальной X (ценник под ~45°, как на
            // витрине Roblox). Знак нормализуем (tiltSign), чтобы «верх назад» был
            // одинаковым для всех граней. Отрицательный tilt = верх отклоняется
            // назад (от наблюдателя), как пюпитр.
            if (Number.isFinite(st.tilt)) plane.rotation.x += st.tilt * tiltSign;
        } else {
            // faceMode: 'fixed' — фиксированная ориентация (вращается с объектом),
            // но позиционируется как обычная плашка (над верхом/центром/низом).
            if (st.faceMode === 'fixed') {
                plane.billboardMode = 0;
                if (Number.isFinite(st.rotationY)) plane.rotation.y = st.rotationY;
            } else {
                plane.billboardMode = 7;     // всегда лицом к камере
            }
            // attachPoint: 'top'(default) — над верхом + небольшой зазор (height);
            // 'center' — по центру; 'bottom' — у низа; {x,y,z} — явно.
            const gap = Number.isFinite(opts.height) ? opts.height : 0.6;
            let py = halfH + gap + halfPlane;   // верх объекта + зазор + полувысота плашки
            if (st.attachPoint === 'center') py = 0;
            else if (st.attachPoint === 'bottom') py = -(halfH + gap);
            else if (st.attachPoint && typeof st.attachPoint === 'object') {
                plane.position.set(st.attachPoint.x || 0, st.attachPoint.y || 0, st.attachPoint.z || 0);
                py = null;
            }
            if (py !== null) plane.position.set(0, py, 0);
        }

        this.labels.set(ref, {
            plane, tex, mat,
            lastKey: styleKey,
            lastText: text,
            styleStruct: this._structKey(st, richText, heightAbove, sizeMul),
            maxDistance: Number.isFinite(opts.maxDistance) ? opts.maxDistance : null,
        });
    }

    /** Ключ «структуры» (всё кроме текста) — для решения перерисовать ли canvas. */
    _structKey(st, richText, h, sz) {
        return JSON.stringify({ p: st.preset, bg: st.background, bc: st.borderColor,
            bw: st.borderWidth, cr: st.cornerRadius, rich: richText, fw: st.fontWeight,
            grad: st.gradient, ts: st.textStroke, h, sz, fm: st.faceMode,
            af: st.attachFace, tl: st.tilt, ap: typeof st.attachPoint === 'string' ? st.attachPoint : null });
    }

    _uid() { this._seq = (this._seq || 0) + 1; return this._seq; }

    /**
     * Нарисовать плашку на canvas DynamicTexture.
     * Фон (roundRect + gradient/fill) → обводка border → текст (с обводкой).
     */
    _drawCanvas(tex, text, color, st, richText) {
        const W = 1024, H = 256;
        const ctx = tex.getContext();
        ctx.clearRect(0, 0, W, H);

        const hasBg = !!st.background || (Array.isArray(st.gradient) && st.gradient.length === 2);
        const pad = Number.isFinite(st.padding) ? st.padding : 28;
        const cr = Number.isFinite(st.cornerRadius) ? st.cornerRadius : 0;
        const bw = Number.isFinite(st.borderWidth) ? st.borderWidth : 0;

        const weight = st.fontWeight || 700;
        const innerPad = (hasBg ? (bw + pad) : 24);   // отступ от краёв (под рамку)
        const maxTextW = W - innerPad * 2;
        // Auto-fit: подбираем размер шрифта, чтобы текст влез по ширине (не обрезался).
        let fontPx = 120;
        if (!richText) {
            ctx.font = `${weight} ${fontPx}px "Roboto Condensed", "Segoe UI", sans-serif`;
            const tw = ctx.measureText(text).width;
            if (tw > maxTextW) fontPx = Math.max(40, Math.floor(fontPx * maxTextW / tw));
        }
        ctx.font = `${weight} ${fontPx}px "Roboto Condensed", "Segoe UI", sans-serif`;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';

        // === Фон-плашка ===
        if (hasBg) {
            const m = bw / 2 + 4;          // отступ рамки от края текстуры
            const x = m, y = m, w = W - m * 2, h = H - m * 2;
            this._roundRectPath(ctx, x, y, w, h, cr);
            if (Array.isArray(st.gradient) && st.gradient.length === 2) {
                const g = ctx.createLinearGradient(0, y, 0, y + h);
                g.addColorStop(0, st.gradient[0]);
                g.addColorStop(1, st.gradient[1]);
                ctx.fillStyle = g;
            } else {
                ctx.fillStyle = st.background;
            }
            ctx.globalAlpha = Number.isFinite(st.backgroundOpacity) ? st.backgroundOpacity : 0.92;
            ctx.fill();
            ctx.globalAlpha = 1;
            if (bw > 0 && st.borderColor) {
                ctx.lineWidth = bw;
                ctx.strokeStyle = st.borderColor;
                ctx.stroke();
            }
        }

        // === Текст ===
        const ts = st.textStroke || { color: '#000', width: hasBg ? 6 : 10 };
        if (richText) {
            this._drawRichText(ctx, text, color, ts, W, H);
        } else {
            if (ts && ts.width > 0) {
                ctx.lineWidth = ts.width;
                ctx.lineJoin = 'round';
                ctx.strokeStyle = ts.color || '#000';
                ctx.strokeText(text, W / 2, H / 2 + 4);
            }
            ctx.fillStyle = color;
            ctx.fillText(text, W / 2, H / 2 + 4);
        }
    }

    /** Путь скруглённого прямоугольника (roundRect не везде есть). */
    _roundRectPath(ctx, x, y, w, h, r) {
        r = Math.min(r, w / 2, h / 2);
        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();
    }

    /**
     * RichText: парсим теги <color=#hex>...</color>, <b>...</b>, <size=N>...</size>.
     * Рисуем сегментами по горизонтали, центрируя всю строку. Вложенность не
     * поддерживается (на MVP) — берём последний открытый тег каждого типа.
     */
    _drawRichText(ctx, text, baseColor, ts, W, H) {
        const segs = this._parseRich(text, baseColor);
        const fontPx = 120;
        // Замер ширины каждого сегмента в его размере.
        let total = 0;
        for (const s of segs) {
            ctx.font = `${s.bold ? 800 : 700} ${Math.round(fontPx * s.sizeMul)}px "Roboto Condensed","Segoe UI",sans-serif`;
            s.w = ctx.measureText(s.text).width;
            total += s.w;
        }
        let x = (W - total) / 2;
        for (const s of segs) {
            ctx.font = `${s.bold ? 800 : 700} ${Math.round(fontPx * s.sizeMul)}px "Roboto Condensed","Segoe UI",sans-serif`;
            ctx.textAlign = 'left';
            if (ts && ts.width > 0) {
                ctx.lineWidth = ts.width;
                ctx.lineJoin = 'round';
                ctx.strokeStyle = ts.color || '#000';
                ctx.strokeText(s.text, x, H / 2 + 4);
            }
            ctx.fillStyle = s.color;
            ctx.fillText(s.text, x, H / 2 + 4);
            x += s.w;
        }
        ctx.textAlign = 'center';
    }

    /** Простой парсер richText → [{text, color, bold, sizeMul}]. */
    _parseRich(text, baseColor) {
        const segs = [];
        let color = baseColor, bold = false, sizeMul = 1;
        // Разбиваем по тегам (открывающим/закрывающим).
        const re = /<(\/?)(?:(color)=([#0-9a-fA-F]+)|(b)|(i)|(size)=(\d+))>|([^<]+)/g;
        let m;
        while ((m = re.exec(text)) !== null) {
            const closing = m[1] === '/';
            if (m[8] != null) {
                // текстовый кусок
                if (m[8]) segs.push({ text: m[8], color, bold, sizeMul });
            } else if (m[2]) {            // <color=...>
                color = closing ? baseColor : m[3];
            } else if (m[4]) {            // <b>
                bold = !closing;
            } else if (m[6]) {            // <size=N>
                sizeMul = closing ? 1 : Math.max(0.4, Math.min(2, Number(m[7]) / 100));
            }
            // <i> игнорим визуально (italic в canvas через font-style — опускаем на MVP)
        }
        if (segs.length === 0) segs.push({ text: '', color: baseColor, bold: false, sizeMul: 1 });
        return segs;
    }

    /** Каждый кадр: maxDistance-скрытие плашек дальше порога от игрока. */
    update() {
        if (!this._playerMesh) return;
        const pp = this._playerMesh.position;
        for (const rec of this.labels.values()) {
            if (rec.maxDistance == null) continue;
            const ap = rec.plane.getAbsolutePosition();
            const dx = ap.x - pp.x, dy = ap.y - pp.y, dz = ap.z - pp.z;
            const far = (dx * dx + dy * dy + dz * dz) > rec.maxDistance * rec.maxDistance;
            rec.plane.setEnabled(!far);
        }
    }

    /** Убрать плашку с объекта. */
    clearLabel(ref) {
        const rec = this.labels.get(ref);
        if (!rec) return;
        try { rec.plane.dispose(); } catch (e) { /* ignore */ }
        try { rec.tex.dispose(); } catch (e) { /* ignore */ }
        try { rec.mat.dispose(); } catch (e) { /* ignore */ }
        this.labels.delete(ref);
    }

    /** Удалить все плашки (при выходе из Play). */
    clearAll() {
        for (const ref of [...this.labels.keys()]) this.clearLabel(ref);
    }
}
