/**
 * ConstraintManager — связи между объектами (Фаза 5, Constraints).
 *
 * Кинематическая реализация (НЕ честный физ-решатель): связи применяются
 * как правила в каждом кадре поверх существующей физики. Этого достаточно
 * для дверей, батутов, подвижных платформ и склеек — то, ради чего
 * constraints нужны в детских играх. Честный решатель связей сломал бы
 * работающую AABB-физику паркур/GD-игр.
 *
 * Типы связей:
 *   weld   — жёсткая склейка: объект B движется вместе с A, сохраняя
 *            относительное смещение. Применяется каждый кадр.
 *   hinge  — петля: объект вращается вокруг оси Y через точку pivot.
 *            Скрипт задаёт угол (setAngle) — для дверей, рычагов, кранов.
 *   spring — пружина: объект упруго возвращается к точке покоя. Толчок
 *            (push) запускает затухающее колебание — для батутов.
 *
 * Связи живут только в Play-режиме. Управляются скриптом через
 * game.constraints.* — каждый вызов возвращает прокси с методами.
 */

let _constraintIdSeq = 1;

export class ConstraintManager {
    constructor(scene3d) {
        this.scene3d = scene3d;
        /** @type {Map<number, object>} id → constraint state */
        this.constraints = new Map();
        this._renderHook = null;
        this._lastTick = performance.now() / 1000;
    }

    start() {
        if (this._renderHook) return;
        this._renderHook = () => this._tick();
        this.scene3d.scene.registerBeforeRender(this._renderHook);
        this._lastTick = performance.now() / 1000;
    }

    stop() {
        if (this._renderHook) {
            try { this.scene3d.scene.unregisterBeforeRender(this._renderHook); } catch (e) {}
            this._renderHook = null;
        }
        this.constraints.clear();
    }

    /**
     * Жёсткая склейка: объект B следует за A, сохраняя текущее смещение.
     * refA, refB — ref-строки объектов. Возвращает id связи.
     */
    addWeld(refA, refB) {
        const a = this._resolve(refA);
        const b = this._resolve(refB);
        if (!a || !b) return null;
        const id = _constraintIdSeq++;
        // Смещение B относительно A в момент создания — оно сохраняется.
        this.constraints.set(id, {
            id, type: 'weld',
            refA, refB,
            offX: b.x - a.x, offY: b.y - a.y, offZ: b.z - a.z,
        });
        return id;
    }

    /**
     * Петля: объект вращается вокруг вертикальной оси через pivot.
     * ref — объект. opts: { pivotX, pivotZ — точка оси (по умолчанию
     * центр объекта), angle — стартовый угол в градусах }.
     */
    addHinge(ref, opts = {}) {
        const o = this._resolve(ref);
        if (!o) return null;
        const id = _constraintIdSeq++;
        // pivot по умолчанию — текущая позиция объекта.
        const pivotX = Number.isFinite(opts.pivotX) ? opts.pivotX : o.x;
        const pivotZ = Number.isFinite(opts.pivotZ) ? opts.pivotZ : o.z;
        // Радиус-вектор от pivot к объекту (фиксируется при создании).
        const armX = o.x - pivotX;
        const armZ = o.z - pivotZ;
        const armLen = Math.hypot(armX, armZ);
        const armAngle0 = Math.atan2(armX, armZ);   // базовый угол плеча
        const c = {
            id, type: 'hinge',
            ref, pivotX, pivotZ, armLen, armAngle0,
            baseRotY: o.rotationY || 0,
            // целевой и текущий угол петли (радианы)
            targetAngle: (Number(opts.angle) || 0) * Math.PI / 180,
            curAngle: (Number(opts.angle) || 0) * Math.PI / 180,
            speed: 3,   // рад/с — скорость поворота к цели
        };
        this.constraints.set(id, c);
        this._applyHinge(c);   // сразу ставим в стартовый угол
        return id;
    }

    /** Задать целевой угол петли (градусы) — объект плавно повернётся. */
    setHingeAngle(id, deg) {
        const c = this.constraints.get(Number(id));
        if (!c || c.type !== 'hinge') return;
        c.targetAngle = (Number(deg) || 0) * Math.PI / 180;
    }

    /**
     * Пружина: объект упруго держится в точке покоя (текущая позиция).
     * opts: { stiffness — жёсткость (по умолчанию 8), damping — затухание
     * (по умолчанию 3) }.
     */
    addSpring(ref, opts = {}) {
        const o = this._resolve(ref);
        if (!o) return null;
        const id = _constraintIdSeq++;
        this.constraints.set(id, {
            id, type: 'spring',
            ref,
            // Точка покоя — где объект «хочет» находиться.
            restX: o.x, restY: o.y, restZ: o.z,
            // Текущая скорость колебания.
            vx: 0, vy: 0, vz: 0,
            stiffness: Number.isFinite(opts.stiffness) ? opts.stiffness : 8,
            damping: Number.isFinite(opts.damping) ? opts.damping : 3,
        });
        return id;
    }

    /** Толкнуть пружинный объект — запускает колебание. */
    pushSpring(id, vx, vy, vz) {
        const c = this.constraints.get(Number(id));
        if (!c || c.type !== 'spring') return;
        c.vx += Number(vx) || 0;
        c.vy += Number(vy) || 0;
        c.vz += Number(vz) || 0;
    }

    /** Убрать связь по id. */
    remove(id) {
        this.constraints.delete(Number(id));
    }

    // ===== внутреннее =====

    _tick() {
        const now = performance.now() / 1000;
        const dt = Math.min(0.05, now - this._lastTick);
        this._lastTick = now;
        if (this.constraints.size === 0) return;
        for (const c of this.constraints.values()) {
            if (c.type === 'weld') this._applyWeld(c);
            else if (c.type === 'hinge') this._tickHinge(c, dt);
            else if (c.type === 'spring') this._tickSpring(c, dt);
        }
    }

    _applyWeld(c) {
        const a = this._resolve(c.refA);
        const b = this._resolve(c.refB);
        if (!a || !b) return;
        this._setPos(b, a.x + c.offX, a.y + c.offY, a.z + c.offZ);
    }

    _tickHinge(c, dt) {
        // Плавно подводим curAngle к targetAngle.
        const diff = c.targetAngle - c.curAngle;
        if (Math.abs(diff) > 0.001) {
            const step = Math.sign(diff) * Math.min(Math.abs(diff), c.speed * dt);
            c.curAngle += step;
        }
        this._applyHinge(c);
    }

    _applyHinge(c) {
        const o = this._resolve(c.ref);
        if (!o) return;
        // Позиция объекта — конец плеча, повёрнутого на curAngle.
        const ang = c.armAngle0 + c.curAngle;
        const nx = c.pivotX + Math.sin(ang) * c.armLen;
        const nz = c.pivotZ + Math.cos(ang) * c.armLen;
        this._setPos(o, nx, o.y, nz);
        // Сам объект тоже доворачивается вокруг своей оси.
        this._setRotY(o, c.baseRotY + c.curAngle);
    }

    _tickSpring(c, dt) {
        const o = this._resolve(c.ref);
        if (!o) return;
        // Сила пружины: F = -k * смещение. Затухание: -damping * скорость.
        const dx = o.x - c.restX, dy = o.y - c.restY, dz = o.z - c.restZ;
        c.vx += (-c.stiffness * dx - c.damping * c.vx) * dt;
        c.vy += (-c.stiffness * dy - c.damping * c.vy) * dt;
        c.vz += (-c.stiffness * dz - c.damping * c.vz) * dt;
        this._setPos(o, o.x + c.vx * dt, o.y + c.vy * dt, o.z + c.vz * dt);
    }

    /**
     * Резолв ref в объект сцены с x/y/z/rotationY и mesh.
     * Возвращает data-объект менеджера или null.
     */
    _resolve(ref) {
        if (typeof ref !== 'string') return null;
        // Локальный ref из scene.spawn → реальный через GameRuntime.
        const rt = this.scene3d.gameRuntime;
        let r = ref;
        if (rt && rt._localToReal && rt._localToReal.has(r)) {
            r = rt._localToReal.get(r);
        }
        const colon = r.indexOf(':');
        if (colon < 0) return null;
        const kind = r.slice(0, colon);
        const rest = r.slice(colon + 1);
        const getFrom = (mgr) => {
            if (!mgr || !mgr.instances) return null;
            let d = mgr.instances.get(rest);
            if (!d) {
                const n = Number(rest);
                if (Number.isFinite(n)) d = mgr.instances.get(n);
            }
            return d || null;
        };
        if (kind === 'primitive') return getFrom(this.scene3d.primitiveManager);
        if (kind === 'model') return getFrom(this.scene3d.modelManager);
        return null;
    }

    /** Записать позицию в data-объект и его меш. */
    _setPos(d, x, y, z) {
        d.x = x; d.y = y; d.z = z;
        const mesh = d.mesh || d.rootMesh || d.rootNode;
        if (mesh && mesh.position) {
            if (mesh._isWorldMatrixFrozen) {
                try { mesh.unfreezeWorldMatrix(); } catch (e) {}
            }
            mesh.position.set(x, y, z);
        }
    }

    /** Записать поворот вокруг Y в data-объект и его меш. */
    _setRotY(d, ry) {
        d.rotationY = ry;
        const mesh = d.mesh || d.rootMesh || d.rootNode;
        if (mesh && mesh.rotation) {
            mesh.rotation.y = ry;
        }
    }
}
