/**
 * PhysicsWorld — обёртка над Rapier3D-compat (wasm физ-движок).
 *
 * Phase 6.5: настоящая динамическая физика для объектов, которым скрипт
 * установил `physics.bodyType = 'dynamic'`. Игрок и обычные блоки/модели
 * остаются на самописной DynamicsManager — Rapier для них НЕ включается.
 *
 * Архитектура:
 *   1. init() — асинхронная загрузка wasm (~400КБ). Возвращает promise.
 *      Пока wasm не загружен, addBody возвращает null (no-op).
 *   2. step(dt) — продвинуть симуляцию на dt секунд. Зовётся каждый кадр
 *      из GameRuntime.tick() ПОСЛЕ обычной физики.
 *   3. addBody(ref, opts) — зарегистрировать примитив как rigid body.
 *      opts: { bodyType, mass, friction, restitution, position, rotation, size, shape }
 *   4. removeBody(ref) — удалить из физ-мира.
 *   5. getBodyTransform(ref) — после step получить новое position/rotation
 *      для синхронизации с Babylon-mesh.
 *   6. raycast(origin, dir, maxDist) — настоящий raycast по геометрии.
 *   7. addJoint(refA, refB, opts) — hinge / spring / distance constraint.
 *
 * Безопасность:
 *   - При любой ошибке Rapier-инициализации движок переходит в "disabled" режим
 *     и все API становятся no-op. Игра не падает.
 *   - removeBody / removeJoint всегда безопасны (повторный вызов не падает).
 *   - dispose() обязателен при exitPlayMode, иначе wasm-память утечёт.
 */

import RAPIER from '@dimforge/rapier3d-compat';

// Глобальный singleton — Rapier wasm можно инициализировать только один раз
// за время жизни страницы. После dispose() оставляем wasm загруженным,
// просто пересоздаём World для нового Play.
let _wasmReady = false;
let _wasmInitPromise = null;

async function _ensureWasm() {
    if (_wasmReady) return true;
    if (_wasmInitPromise) return _wasmInitPromise;
    _wasmInitPromise = (async () => {
        try {
            await RAPIER.init();
            _wasmReady = true;
            return true;
        } catch (e) {
            console.error('[PhysicsWorld] Rapier wasm init failed:', e);
            _wasmReady = false;
            return false;
        }
    })();
    return _wasmInitPromise;
}

export class PhysicsWorld {
    constructor() {
        this.world = null;         // RAPIER.World
        this.bodies = new Map();   // ref → { body: RigidBody, collider: Collider, shape, lastPos, lastRot }
        this.joints = new Map();   // jointId → ImpulseJoint
        this._jointSeq = 0;
        this._ready = false;
        this._disposed = false;
    }

    async init(gravity = -22) {
        const ok = await _ensureWasm();
        if (!ok) return false;
        if (this._disposed) return false;
        // Гравитация по Y отрицательная (как в Babylon-мире).
        this.world = new RAPIER.World({ x: 0.0, y: gravity, z: 0.0 });
        this._ready = true;
        return true;
    }

    isReady() { return this._ready && !this._disposed; }

    /**
     * Шаг симуляции. dt в секундах. Rapier работает в собственном FixedDt,
     * но мы передаём фактический dt — он сам разобьёт на substep'ы при необходимости.
     */
    step(dt) {
        if (!this.isReady()) return;
        // Rapier ожидает что world.timestep = 1/60. Меняем на текущий dt чтобы
        // подстраивалось под FPS. Безопасно: clamp в [1/240, 1/30].
        const ts = Math.max(1 / 240, Math.min(1 / 30, dt));
        this.world.timestep = ts;
        this.world.step();
    }

    /**
     * Добавить rigid body для примитива.
     * @param ref строковый ref (primitive:N).
     * @param opts {
     *   bodyType: 'dynamic' | 'static' | 'kinematic',
     *   shape: 'box' | 'sphere' | 'cylinder' | 'capsule',
     *   size: {x, y, z} -- полу-размеры для box, или {radius} для sphere
     *   position: {x, y, z},
     *   rotation: {x, y, z, w} (кватернион) или {y: yaw_radians} как fallback,
     *   mass: число (default 1),
     *   friction: 0..1 (default 0.5),
     *   restitution: 0..1 (default 0.0) -- упругость
     * }
     * @returns true если добавили, false при ошибке.
     */
    addBody(ref, opts) {
        if (!this.isReady() || !ref || !opts) return false;
        if (this.bodies.has(ref)) return true;   // уже есть
        const W = this.world;
        const pos = opts.position || { x: 0, y: 0, z: 0 };
        const rot = opts.rotation || { x: 0, y: 0, z: 0, w: 1 };
        // Body descriptor по типу
        let bodyDesc;
        if (opts.bodyType === 'static') {
            bodyDesc = RAPIER.RigidBodyDesc.fixed();
        } else if (opts.bodyType === 'kinematic') {
            bodyDesc = RAPIER.RigidBodyDesc.kinematicPositionBased();
        } else {
            bodyDesc = RAPIER.RigidBodyDesc.dynamic();
        }
        bodyDesc.setTranslation(pos.x, pos.y, pos.z);
        if (typeof rot.w === 'number') {
            bodyDesc.setRotation(rot);
        } else if (typeof rot.y === 'number') {
            // Yaw -> quaternion
            const h = rot.y / 2;
            bodyDesc.setRotation({ x: 0, y: Math.sin(h), z: 0, w: Math.cos(h) });
        }
        const body = W.createRigidBody(bodyDesc);
        // Collider
        const shape = opts.shape || 'box';
        const size = opts.size || { x: 0.5, y: 0.5, z: 0.5 };
        let colDesc;
        if (shape === 'sphere') {
            const r = (size.radius != null) ? size.radius : Math.max(size.x || 0.5, size.y || 0.5, size.z || 0.5);
            colDesc = RAPIER.ColliderDesc.ball(r);
        } else if (shape === 'cylinder') {
            colDesc = RAPIER.ColliderDesc.cylinder(size.y || 0.5, size.x || 0.5);
        } else if (shape === 'capsule') {
            colDesc = RAPIER.ColliderDesc.capsule(size.y || 0.5, size.x || 0.5);
        } else {
            // box: полу-размеры
            colDesc = RAPIER.ColliderDesc.cuboid(size.x || 0.5, size.y || 0.5, size.z || 0.5);
        }
        colDesc.setDensity(opts.mass != null ? opts.mass : 1);
        colDesc.setFriction(opts.friction != null ? opts.friction : 0.5);
        colDesc.setRestitution(opts.restitution != null ? opts.restitution : 0.0);
        const collider = W.createCollider(colDesc, body);
        this.bodies.set(ref, {
            body, collider, shape, size,
            // Кеш последней позиции/поворота для дельты в getBodyTransform
            lastPos: { ...pos },
            lastRot: typeof rot.w === 'number' ? { ...rot } : { x: 0, y: 0, z: 0, w: 1 },
        });
        return true;
    }

    /** Удалить тело из физ-мира. Безопасно вызывать повторно. */
    removeBody(ref) {
        const rec = this.bodies.get(ref);
        if (!rec) return;
        try { this.world.removeRigidBody(rec.body); } catch (_) {}
        this.bodies.delete(ref);
    }

    /** Применить мгновенный импульс к dynamic body. */
    applyImpulse(ref, ix, iy, iz) {
        const rec = this.bodies.get(ref);
        if (!rec) return;
        try { rec.body.applyImpulse({ x: ix, y: iy, z: iz }, true); } catch (_) {}
    }

    /** Задать скорость dynamic body напрямую. */
    setVelocity(ref, vx, vy, vz) {
        const rec = this.bodies.get(ref);
        if (!rec) return;
        try { rec.body.setLinvel({ x: vx, y: vy, z: vz }, true); } catch (_) {}
    }

    /**
     * Получить текущую позицию и поворот тела после шага физики.
     * Возвращает { x, y, z, qx, qy, qz, qw } или null.
     */
    getBodyTransform(ref) {
        const rec = this.bodies.get(ref);
        if (!rec) return null;
        try {
            const t = rec.body.translation();
            const r = rec.body.rotation();
            return { x: t.x, y: t.y, z: t.z, qx: r.x, qy: r.y, qz: r.z, qw: r.w };
        } catch (_) { return null; }
    }

    /**
     * Raycast по физ-миру.
     * @returns { hit, point, ref, distance, normal } или null.
     */
    raycast(origin, dir, maxDist = 100) {
        if (!this.isReady()) return null;
        try {
            const ray = new RAPIER.Ray(
                { x: origin.x, y: origin.y, z: origin.z },
                { x: dir.x, y: dir.y, z: dir.z }
            );
            const hit = this.world.castRayAndGetNormal(ray, maxDist, true);
            if (!hit) return null;
            const t = hit.timeOfImpact ?? hit.toi;
            const point = {
                x: origin.x + dir.x * t,
                y: origin.y + dir.y * t,
                z: origin.z + dir.z * t,
            };
            // Найти ref по collider'у. Линейный поиск (O(N)) — для сотен тел ok.
            let foundRef = null;
            const colHandle = hit.collider?.handle;
            for (const [r, rec] of this.bodies) {
                if (rec.collider.handle === colHandle) { foundRef = r; break; }
            }
            return {
                hit: true,
                point,
                ref: foundRef,
                distance: t,
                normal: hit.normal ? { x: hit.normal.x, y: hit.normal.y, z: hit.normal.z } : null,
            };
        } catch (e) {
            return null;
        }
    }

    /**
     * Hinge-сустав: тело A может вращаться вокруг axis относительно тела B.
     * @returns jointId (число) или null.
     */
    addHinge(refA, refB, opts) {
        if (!this.isReady()) return null;
        const recA = this.bodies.get(refA);
        const recB = this.bodies.get(refB);
        if (!recA || !recB) return null;
        try {
            const anchorA = opts.anchorA || { x: 0, y: 0, z: 0 };
            const anchorB = opts.anchorB || { x: 0, y: 0, z: 0 };
            const axis = opts.axis || { x: 0, y: 1, z: 0 };
            const params = RAPIER.JointData.revolute(anchorA, anchorB, axis);
            const joint = this.world.createImpulseJoint(params, recA.body, recB.body, true);
            const id = ++this._jointSeq;
            this.joints.set(id, joint);
            return id;
        } catch (_) { return null; }
    }

    /** Distance-constraint (верёвка/жёсткая дистанция). */
    addDistance(refA, refB, opts) {
        if (!this.isReady()) return null;
        const recA = this.bodies.get(refA);
        const recB = this.bodies.get(refB);
        if (!recA || !recB) return null;
        try {
            const anchorA = opts.anchorA || { x: 0, y: 0, z: 0 };
            const anchorB = opts.anchorB || { x: 0, y: 0, z: 0 };
            // У Rapier 0.12 distance-joint называется RopeJointData в API, но
            // мы используем generic Joint через spherical c distance limit.
            const params = RAPIER.JointData.spherical(anchorA, anchorB);
            const joint = this.world.createImpulseJoint(params, recA.body, recB.body, true);
            const id = ++this._jointSeq;
            this.joints.set(id, joint);
            return id;
        } catch (_) { return null; }
    }

    /** Удалить сустав. */
    removeJoint(id) {
        const joint = this.joints.get(id);
        if (!joint) return;
        try { this.world.removeImpulseJoint(joint, true); } catch (_) {}
        this.joints.delete(id);
    }

    /**
     * Полная очистка физ-мира. Вызывать при exitPlayMode.
     */
    dispose() {
        if (this._disposed) return;
        this._disposed = true;
        this._ready = false;
        for (const [id] of this.joints) {
            try { this.world?.removeImpulseJoint(this.joints.get(id), true); } catch (_) {}
        }
        this.joints.clear();
        for (const [ref] of this.bodies) {
            try { this.world?.removeRigidBody(this.bodies.get(ref).body); } catch (_) {}
        }
        this.bodies.clear();
        try { this.world?.free(); } catch (_) {}
        this.world = null;
    }
}
