import { Vector3, TransformNode } from '@babylonjs/core';

/**
 * VehicleManager — система транспорта (задача 14, фаза V1 аркадная + V2 параметры).
 *
 * Каждая машина = chassisNode (TransformNode) + GLB-кузов (modelManager-инстанс) +
 * 4 колеса-визуала (передние доворачивают при руле). Физика АРКАДНАЯ:
 *   speed (скаляр вдоль yaw) += throttle*power*dt; трение; поворот по steer
 *   (масштаб от скорости — нет вращения на месте); коллизия с миром через
 *   physics.moveAABB (тот же солвер что у игрока). Колёса друг с другом и с
 *   другими машинами НЕ сталкиваются (V1) — только chassis с миром.
 *
 * Фича-парность: идентичный модуль в rublox-player/src/engine/.
 */

const DEFAULT_PARAMS = {
    mass: 1200,
    enginePower: 14,     // ускорение (м/с²) — аркадно, не реальные л.с.
    maxSpeed: 14,        // м/с (~50 км/ч) — для маленьких миров
    turnSpeed: 1.8,      // рад/с при полной скорости
    brake: 26,           // замедление при тормозе/реверсе
    drive: 'rwd',
};

export class VehicleManager {
    constructor(scene3d) {
        this.s = scene3d;
        this.scene = scene3d.scene;
        this.vehicles = new Map();   // id → veh
        this._seq = 0;
    }

    get _physics() { return this.s.physics; }
    get _models() { return this.s.modelManager; }

    /**
     * Создать машину. opts: { model:'car-taxi', color, name, params, x,y,z, rotationY }.
     * Возвращает Promise<id>.
     */
    async spawn(opts) {
        opts = opts || {};
        const x0 = Number(opts.x) || 0, z0 = Number(opts.z) || 0;
        // Идемпотентность: если машина с такой позицией уже есть — не плодим
        // (защита от двойного выполнения скрипта спавна → дубли машин).
        for (const v of this.vehicles.values()) {
            if (Math.abs(v.spawnX - x0) < 0.5 && Math.abs(v.spawnZ - z0) < 0.5) return v.id;
        }
        const id = ++this._seq;
        const x = Number(opts.x) || 0, y = Number(opts.y) || 0.4, z = Number(opts.z) || 0;
        const yaw = Number(opts.rotationY) || 0;
        const params = { ...DEFAULT_PARAMS, ...(opts.params || {}) };
        const modelType = opts.model || 'car-sedan';

        // chassis-узел — родитель кузова и колёс.
        const chassisNode = new TransformNode(`vehicle_${id}`, this.scene);
        chassisNode.position = new Vector3(x, y, z);
        chassisNode.rotation = new Vector3(0, yaw, 0);

        const veh = {
            id, name: opts.name || 'Машина', params,
            spawnX: x, spawnZ: z,   // для дедупа повторного спавна
            chassisNode, bodyInstanceId: null, wheels: [],
            pos: new Vector3(x, y, z), yaw, vy: 0,
            speed: 0, steerAngle: 0,
            half: { w: 1.0, h: 0.6, d: 2.0 },   // уточним по bbox кузова
            throttle: 0, steer: 0, handbrake: false,
            driver: null,
            handlers: { onEnter: [], onExit: [], onCollide: [], onSpeedChange: [] },
            ref: opts.ref || null,
        };
        this.vehicles.set(id, veh);

        // Кузов (GLB Kenney car-kit).
        try {
            const bodyId = await this._models.addInstance(modelType, x, y, z, yaw);
            veh.bodyInstanceId = bodyId;
            const inst = this._models.instances.get(bodyId);
            if (inst && inst.rootMesh) {
                // Габариты AABB + вертикальный offset кузова СЧИТАЕМ ДО парентинга
                // (в мировых координатах, кузов ещё в (x,y,z)).
                try {
                    const bb = inst.rootMesh.getHierarchyBoundingVectors(true);
                    veh.half = {
                        w: Math.max(0.6, (bb.max.x - bb.min.x) / 2),
                        h: Math.max(0.4, (bb.max.y - bb.min.y) / 2),
                        d: Math.max(1.0, (bb.max.z - bb.min.z) / 2),
                    };
                    // Насколько низ кузова ниже точки спавна y — чтобы посадить
                    // кузов так, чтобы его НИЗ совпал с низом AABB (машина на земле,
                    // не парит). bodyYOffset применяется к локальной Y кузова.
                    veh.bodyYOffset = -(bb.min.y - y) - veh.half.h;
                } catch (e) { veh.bodyYOffset = -veh.half.h; }
                inst.rootMesh.setParent(chassisNode);
                inst.rootMesh.position = new Vector3(0, veh.bodyYOffset || 0, 0);
                inst.rootMesh.rotation = Vector3.Zero();
                // Цвет кузова (tint поверх GLB-текстуры).
                if (opts.color) { try { this._models.setInstanceProps?.(bodyId, { tint: opts.color }); } catch (e) {} }
            }
        } catch (e) { console.warn('[VehicleManager] body load failed', e); }

        // Колёса НЕ спавним отдельно — GLB-модели Kenney car-kit уже содержат
        // колёса в кузове. Отдельные колёса дублировали/отрывались (баг V1).
        // Визуальный доворот передних колёс — фаза V3 (там кузов+колёса раздельно).

        // «Оседание»: уроним машину на землю СРАЗУ (до посадки игрока), иначе она
        // висит/утоплена на стартовой y, пока никто не за рулём (нет tick).
        this._settle(veh);
        // Повторное оседание на следующих кадрах: физический грид статики может
        // ещё не проиндексироваться к моменту спавна (await addInstance), тогда
        // первый _settle не находит пол и машина зависает в воздухе (баг седана).
        for (const d of [120, 350, 800]) {
            setTimeout(() => { try { if (!veh.driver) this._settle(veh); } catch (e) {} }, d);
        }

        return id;
    }

    /**
     * Опустить машину на поверхность гравитацией. Стартуем ВЫШЕ текущей точки и
     * роняем большим запасом (много шагов), чтобы гарантированно найти пол даже
     * если стартовая y оказалась чуть ниже/выше или физика поздно готова.
     */
    _settle(veh) {
        try {
            veh.pos.y += 0.5;
            let landed = false;
            for (let i = 0; i < 80; i++) {
                const r = this._physics.moveAABB(veh.pos, veh.half.w, veh.half.h, veh.half.d, 0, -0.25, 0);
                veh.pos.set(r.x, r.y, r.z);
                if (r.hitY) { landed = true; break; }
            }
            if (landed) {
                for (let i = 0; i < 4; i++) {
                    const r = this._physics.moveAABB(veh.pos, veh.half.w, veh.half.h, veh.half.d, 0, -0.04, 0);
                    veh.pos.set(r.x, r.y, r.z);
                    if (r.hitY) break;
                }
            }
            veh.vy = 0;
            veh.chassisNode.position.copyFrom(veh.pos);
            veh.chassisNode.rotation.y = veh.yaw;
        } catch (e) { /* ignore */ }
    }

    getById(id) { return this.vehicles.get(id) || null; }

    /** Установить ввод водителя (из PlayerController). */
    setInput(veh, throttle, steer, handbrake) {
        if (!veh) return;
        veh.throttle = Math.max(-1, Math.min(1, throttle || 0));
        veh.steer = Math.max(-1, Math.min(1, steer || 0));
        veh.handbrake = !!handbrake;
    }

    /** Физический шаг машины (вызывается каждый кадр пока есть водитель). */
    tickVehicle(veh, dt) {
        if (!veh) return;
        dt = Math.min(dt, 1 / 30);
        const p = veh.params;
        const prevSpeed = veh.speed;

        // Ускорение / торможение / реверс.
        if (veh.throttle > 0) {
            veh.speed += veh.throttle * p.enginePower * dt;
        } else if (veh.throttle < 0) {
            // S: сначала тормоз, потом задний ход (ограничен).
            if (veh.speed > 0.2) veh.speed -= p.brake * dt;
            else veh.speed += veh.throttle * p.enginePower * 0.5 * dt;
        }
        // Накат-трение.
        veh.speed *= (1 - 1.2 * dt);
        if (veh.handbrake) veh.speed *= (1 - 6 * dt);
        // Клампы.
        const maxFwd = p.maxSpeed, maxRev = p.maxSpeed * 0.4;
        if (veh.speed > maxFwd) veh.speed = maxFwd;
        if (veh.speed < -maxRev) veh.speed = -maxRev;
        if (Math.abs(veh.speed) < 0.05) veh.speed = 0;

        // Поворот (зависит от скорости — нельзя крутиться на месте).
        const speedFrac = veh.speed / maxFwd;
        veh.yaw += veh.steer * p.turnSpeed * speedFrac * dt;
        // Угол доворота передних колёс (визуал) — плавный lerp.
        const targetSteer = veh.steer * 0.5;
        veh.steerAngle += (targetSteer - veh.steerAngle) * Math.min(1, dt * 8);

        // Направление и перемещение.
        const dir = new Vector3(Math.sin(veh.yaw), 0, Math.cos(veh.yaw));
        const moveX = dir.x * veh.speed * dt;
        const moveZ = dir.z * veh.speed * dt;
        // Гравитация (машина сидит на полу/дороге).
        veh.vy += -22 * dt;

        // Коллизия с миром через тот же солвер что у игрока.
        let res;
        try {
            res = this._physics.moveAABB(veh.pos, veh.half.w, veh.half.h, veh.half.d, moveX, veh.vy * dt, moveZ);
        } catch (e) {
            res = { x: veh.pos.x + moveX, y: veh.pos.y, z: veh.pos.z + moveZ, hitX: false, hitY: false, hitZ: false };
        }
        veh.pos.set(res.x, res.y, res.z);
        if (res.hitY) veh.vy = 0;
        // Удар об стену — гасим ход.
        if (res.hitX || res.hitZ) {
            const force = Math.abs(veh.speed);
            veh.speed *= 0.3;
            for (const fn of veh.handlers.onCollide) { try { fn(force); } catch (e) {} }
        }

        // Применить к узлам.
        veh.chassisNode.position.copyFrom(veh.pos);
        veh.chassisNode.rotation.y = veh.yaw;
        // Колёса: передние доворачивают, все катятся.
        const roll = (veh.speed * dt) / 0.4;
        for (const w of veh.wheels) {
            if (w.isFront) w.node.rotation.y = veh.steerAngle;
            w.node.rotation.x = (w.node.rotation.x + roll) % (Math.PI * 2);
        }

        if (Math.abs(veh.speed - prevSpeed) > 0.01) {
            for (const fn of veh.handlers.onSpeedChange) { try { fn(Math.abs(veh.speed)); } catch (e) {} }
        }
        // Падение в бездну — сигнал PlayerController высадить + респавн.
        if (veh.pos.y < -25) return { fellOut: true };
        return null;
    }

    /** Текущая скорость машины в м/с (для спидометра). */
    speedOf(veh) { return veh ? Math.abs(veh.speed) : 0; }

    applyImpulse(veh, v) {
        if (!veh || !v) return;
        // Простой импульс: вертикальная составляющая в vy, горизонтальная в speed по направлению.
        if (Number.isFinite(v.y)) veh.vy += Number(v.y);
        const dir = new Vector3(Math.sin(veh.yaw), 0, Math.cos(veh.yaw));
        const horiz = (Number(v.x) || 0) * dir.x + (Number(v.z) || 0) * dir.z;
        veh.speed += horiz;
    }

    dispose() {
        for (const veh of this.vehicles.values()) {
            try {
                if (veh.bodyInstanceId != null) this._models.removeInstance?.(veh.bodyInstanceId);
                for (const w of veh.wheels) this._models.removeInstance?.(w.instanceId);
                veh.chassisNode?.dispose?.();
            } catch (e) { /* ignore */ }
        }
        this.vehicles.clear();
    }
}
