/* eslint-disable no-restricted-globals */
/**
 * ZombieAIWorker — Web Worker для AI зомби.
 *
 * Главный поток (ZombieManager) — рендер: отрисовка моделей, position.set,
 * rotation.set, _applyZombiePose. Никакого FSM, raycast, surface scan здесь.
 *
 * Worker — чистая JS-логика без Babylon: FSM (idle/wander/chase/attack),
 * выбор wanderTarget, обнаружение игрока по дистанции, surface height,
 * boundary clamp, гравитация, генерация событий (attack, sound).
 *
 * Протокол:
 *   main → worker:
 *     { type: 'init', surfaceMap, worldHalf }
 *       — запустить worker, дать слепок поверхности.
 *     { type: 'updateSurface', surfaceMap }
 *       — карта блоков изменилась (редко, например при загрузке).
 *     { type: 'addZombie', id, x, y, z, opts }
 *       — добавить зомби со стартовой позицией.
 *     { type: 'removeZombie', id }
 *       — удалить.
 *     { type: 'tick', playerX, playerY, playerZ, now, dt }
 *       — тикни AI, верни новые состояния. Главный поток зовёт это раз
 *       в ~50мс (не каждый кадр).
 *
 *   worker → main:
 *     { type: 'state', zombies: [{id, x, y, z, yaw, state, isMoving, walkPhase}],
 *       events: [{type:'attack', id}, ...] }
 *       — текущие позиции и события за период с прошлого тика.
 */

const HIDE_DIST = 80;
const ATTACK_RANGE_DEFAULT = 1.6;

// Состояние всех зомби в worker'е
const zombies = new Map();
let surfaceMap = null;     // Map<numericKey, topY>
let worldHalf = 40;
// Шаг разреженной части карты (гладкий ландшафт семплится с шагом 2м,
// блоки — с шагом 1). При промахе по точной ячейке ищем ближайшие
// заполненные клетки с этим шагом и интерполируем.
let surfaceStep = 1;

// Поверхность под (x, z) — берём из surfaceMap.
// Блоки лежат в каждой целой ячейке; гладкий ландшафт — разреженно
// (шаг surfaceStep). Если точной ячейки нет — ищем 4 ближайшие узла
// сетки шага surfaceStep и билинейно интерполируем. Это убирает
// провал зомби сквозь гладкий ландшафт (раньше fallback был 0).
function surfaceHeightAt(x, z) {
    if (!surfaceMap) return 0;
    const gx = Math.round(x);
    const gz = Math.round(z);
    // 1. Точная ячейка (блоки кладутся в каждую целую клетку)
    const exact = surfaceMap.get(gx * 65537 + gz);
    if (exact !== undefined) return exact;
    // 2. Разреженная сетка гладкого ландшафта — билинейная интерполяция
    const st = surfaceStep > 0 ? surfaceStep : 2;
    const c0 = Math.floor(x / st) * st;
    const r0 = Math.floor(z / st) * st;
    const h00 = surfaceMap.get(c0 * 65537 + r0);
    const h10 = surfaceMap.get((c0 + st) * 65537 + r0);
    const h01 = surfaceMap.get(c0 * 65537 + (r0 + st));
    const h11 = surfaceMap.get((c0 + st) * 65537 + (r0 + st));
    const vals = [];
    if (h00 !== undefined) vals.push(h00);
    if (h10 !== undefined) vals.push(h10);
    if (h01 !== undefined) vals.push(h01);
    if (h11 !== undefined) vals.push(h11);
    if (vals.length === 0) return 0;
    const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
    const v00 = h00 !== undefined ? h00 : avg;
    const v10 = h10 !== undefined ? h10 : avg;
    const v01 = h01 !== undefined ? h01 : avg;
    const v11 = h11 !== undefined ? h11 : avg;
    const tx = (x - c0) / st;
    const tz = (z - r0) / st;
    const a = v00 * (1 - tx) + v10 * tx;
    const b = v01 * (1 - tx) + v11 * tx;
    return a * (1 - tz) + b * tz;
}

function pickWanderTarget(cx, cz, radius) {
    const a = Math.random() * Math.PI * 2;
    const r = Math.random() * radius;
    let tx = cx + Math.cos(a) * r;
    let tz = cz + Math.sin(a) * r;
    const half = worldHalf - 1;
    if (tx >  half) tx =  half;
    if (tx < -half) tx = -half;
    if (tz >  half) tz =  half;
    if (tz < -half) tz = -half;
    return { x: tx, z: tz };
}

function lerpAngle(a, b, t) {
    let diff = b - a;
    while (diff > Math.PI) diff -= Math.PI * 2;
    while (diff < -Math.PI) diff += Math.PI * 2;
    return a + diff * Math.min(1, t);
}

/** Тик одного зомби. Возвращает массив событий (например, attack). */
function tickZombie(z, dt, px, py, pz, now) {
    const dx = px - z.x;
    const dz = pz - z.z;
    const dy = (py ?? 0) - (z.y ?? 0);
    const dist2d = Math.sqrt(dx * dx + dz * dz);
    // 3D-расстояние: важно для FSM. Если зомби на y=4, а игрок на y=10
    // (стоит на крыше блочного дома), XZ-расстояние может быть 0.5м, но
    // фактически он не дотягивается. Используем 3D для attack/chase.
    const dist3d = Math.sqrt(dx * dx + dy * dy + dz * dz);
    z.distToPlayer = dist3d;

    // Скрытие далёких зомби — в main thread (не тут).
    if (dist2d >= HIDE_DIST) {
        z.visible = false;
        return null;
    }
    z.visible = true;

    const opts = z.opts;
    const events = [];

    // FSM.
    // attack: либо 3D-расстояние в зоне поражения, ЛИБО игрок ПРЯМО НАД
    // зомби (запрыгнул на голову) — dist2d мал, dy умеренный. Без этого
    // игрок «топчет» зомби стоя на нём, выходит из attackRange по dist3d
    // и зомби бесполезно крутится на месте.
    const onHead = dist2d < opts.attackRange && Math.abs(dy) < 3.5;
    if (dist3d < opts.attackRange || onHead) {
        z.state = 'attack';
    } else if (dist3d < opts.detectionRadius) {
        z.state = 'chase';
    } else if (z.state !== 'wander') {
        z.state = 'wander';
        z.wanderTarget = pickWanderTarget(z.x, z.z, opts.wanderRadius);
        z.stateTime = 0;
        z.idlePauseUntil = null;
    }

    let moveDir = null;
    let speed = 0;

    if (z.state === 'attack') {
        // attack уже сработал по 3D-расстоянию в FSM (dist3d < attackRange),
        // дополнительная проверка не нужна.
        if (now - z.lastAttackTime > opts.attackCooldown) {
            z.lastAttackTime = now;
            events.push({ type: 'attack', id: z.id, damage: opts.attackDamage });
        }
        const targetYaw = Math.atan2(dx, dz) + Math.PI;
        z.yaw = lerpAngle(z.yaw, targetYaw, dt * 5);
    } else if (z.state === 'chase') {
        // Двигаемся в XZ к игроку (по горизонтали), 2D-нормализация направления.
        const len = Math.max(0.001, dist2d);
        moveDir = { x: dx / len, z: dz / len };
        speed = opts.speed;
    } else {
        // wander
        z.stateTime += dt;
        if (z.idlePauseUntil != null) {
            if (z.stateTime >= z.idlePauseUntil) {
                z.idlePauseUntil = null;
                z.stateTime = 0;
                z.wanderTarget = pickWanderTarget(z.x, z.z, opts.wanderRadius);
            }
        } else {
            const tx = z.wanderTarget.x - z.x;
            const tz = z.wanderTarget.z - z.z;
            const tdist = Math.sqrt(tx * tx + tz * tz);
            const reached = tdist < 0.7;
            const tooLong = z.stateTime > 15;
            if (reached || tooLong) {
                z.idlePauseUntil = 1.5 + Math.random() * 1.5;
                z.stateTime = 0;
            } else {
                moveDir = { x: tx / tdist, z: tz / tdist };
                speed = opts.wanderSpeed;
            }
        }
    }

    // Гравитация
    if (z.vy == null) z.vy = 0;
    z.vy += -18 * dt;
    if (z.vy < -25) z.vy = -25;
    let newY = z.y + z.vy * dt;

    const surfaceY = surfaceHeightAt(z.x, z.z);
    if (newY <= surfaceY) {
        newY = surfaceY;
        z.vy = 0;
        z.onGround = true;
    } else {
        z.onGround = false;
    }
    z.y = newY;

    // Clamp в границы мира
    const halfClamp = worldHalf - 0.5;
    if (z.x >  halfClamp) z.x =  halfClamp;
    if (z.x < -halfClamp) z.x = -halfClamp;
    if (z.z >  halfClamp) z.z =  halfClamp;
    if (z.z < -halfClamp) z.z = -halfClamp;

    let stepped = false;
    if (moveDir) {
        const stepX = moveDir.x * speed * dt;
        const stepZ = moveDir.z * speed * dt;
        const half = worldHalf - 1.0;
        const tryX = z.x + stepX;
        const tryZ = z.z + stepZ;
        const outX = tryX > half || tryX < -half;
        const outZ = tryZ > half || tryZ < -half;
        const targetSurface = surfaceHeightAt(tryX, tryZ);
        const heightDiff = targetSurface - z.y;

        if (heightDiff <= 1.05 && !outX && !outZ) {
            z.x = tryX;
            z.z = tryZ;
            stepped = true;
            if (heightDiff > 0.05 && z.onGround) z.vy = 6;
        } else {
            const onlyXok = !outX
                && (surfaceHeightAt(z.x + stepX, z.z) - z.y) <= 1.05;
            const onlyZok = !outZ
                && (surfaceHeightAt(z.x, z.z + stepZ) - z.y) <= 1.05;
            if (onlyXok) { z.x += stepX; stepped = true; }
            else if (onlyZok) { z.z += stepZ; stepped = true; }
        }

        if (!stepped && z.state === 'wander') {
            z.idlePauseUntil = 0.8 + Math.random() * 1.2;
            z.stateTime = 0;
            z.wanderTarget = pickWanderTarget(z.x, z.z, opts.wanderRadius);
            moveDir = null;
        }
    }

    z.isMoving = !!moveDir;
    if (moveDir) {
        const targetYaw = Math.atan2(moveDir.x, moveDir.z) + Math.PI;
        z.yaw = lerpAngle(z.yaw, targetYaw, dt * 5);
    }

    if (z.walkPhase == null) z.walkPhase = 0;
    if (z.isMoving) {
        const stepFreq = (z.state === 'chase') ? 8 : 5;
        z.walkPhase += dt * stepFreq;
    }

    return events.length ? events : null;
}

// === Сообщения main → worker ===
self.onmessage = (e) => {
    const msg = e.data;
    switch (msg.type) {
        case 'init':
        case 'updateSurface': {
            // surfaceMap приходит как plain object {key: y}, конвертим в Map
            surfaceMap = new Map();
            for (const k in msg.surfaceMap) {
                surfaceMap.set(parseInt(k, 10), msg.surfaceMap[k]);
            }
            if (typeof msg.worldHalf === 'number') worldHalf = msg.worldHalf;
            if (typeof msg.surfaceStep === 'number' && msg.surfaceStep > 0) {
                surfaceStep = msg.surfaceStep;
            }
            break;
        }
        case 'addZombie': {
            const opts = msg.opts || {};
            zombies.set(msg.id, {
                id: msg.id,
                x: msg.x, y: msg.y, z: msg.z,
                yaw: 0,
                vy: 0,
                state: 'wander',
                stateTime: 0,
                idlePauseUntil: null,
                wanderTarget: pickWanderTarget(msg.x, msg.z, opts.wanderRadius || 8),
                lastAttackTime: 0,
                onGround: false,
                visible: true,
                isMoving: false,
                walkPhase: 0,
                opts: {
                    speed: opts.speed ?? 2.2,
                    wanderSpeed: opts.wanderSpeed ?? 1.2,
                    detectionRadius: opts.detectionRadius ?? 12,
                    attackRange: opts.attackRange ?? ATTACK_RANGE_DEFAULT,
                    attackCooldown: opts.attackCooldown ?? 1.5,
                    attackDamage: opts.attackDamage ?? 10,
                    wanderRadius: opts.wanderRadius ?? 8,
                },
            });
            break;
        }
        case 'removeZombie': {
            zombies.delete(msg.id);
            break;
        }
        case 'tick': {
            const { playerX, playerY, playerZ, now, dt } = msg;
            const out = [];
            const allEvents = [];
            for (const z of zombies.values()) {
                const events = tickZombie(z, dt, playerX, playerY, playerZ, now);
                if (events) {
                    for (const ev of events) {
                        allEvents.push({ ...ev, id: z.id });
                    }
                }
                out.push({
                    id: z.id,
                    x: z.x, y: z.y, z: z.z,
                    yaw: z.yaw,
                    state: z.state,
                    isMoving: z.isMoving,
                    walkPhase: z.walkPhase,
                    visible: z.visible,
                });
            }
            self.postMessage({ type: 'state', zombies: out, events: allEvents });
            break;
        }
        default:
            break;
    }
};
