/**
 * Чистая симуляция частицы — без Pixi, тестируется в Vitest.
 * ParticleEmitter использует эти функции для интеграции и спавна.
 */

import type { Rng } from '../math/rng';
import { lerpColor } from '../anim/flash';
import type { EmitterOptions } from './Particles';

/** Состояние частицы без привязки к отображению. */
export interface ParticleState {
    x: number;
    y: number;
    vx: number;
    vy: number;
    age: number;
    /** Секунд до смерти. */
    lifetime: number;
    /** Угол (рад) и скорость вращения (рад/сек). */
    rot: number;
    spin: number;
    /** Базовый размер: px для квадратов, множитель для текстурных частиц. */
    scale: number;
    /** Цвет, выбранный при спавне (из color/colors). */
    tint: number;
    /** Персональное «зерно» 0..1: фаза wobble и прочая вариация. */
    k: number;
}

/** Опции симуляции — общие для всех частиц эмиттера. */
export interface ParticleSimOptions {
    acceleration?: { x?: number; y?: number };
    /** Затухание альфы: true — вся жизнь; число — хвост в секундах. */
    fadeOut?: boolean | number;
    /** Сек нарастания альфы в начале жизни. */
    fadeIn?: number;
    /** Торможение, 1/сек: v *= max(0, 1 - drag*dt). */
    drag?: number;
    /** Масштаб по жизни: явный диапазон или предустановка. */
    scaleOverLife?: [number, number] | 'shrink' | 'grow';
    /** Цвет по жизни: лерп 0xRRGGBB от начала к концу. */
    colorOverLife?: [number, number];
    /** Амплитуда синус-дрейфа по X, px/сек (мотыльки, планктон). */
    wobble?: number;
}

/** Видимая часть частицы на шаге (базовый размер уже учтён в scale). */
export interface ParticleVisual {
    alpha: number;
    scale: number;
    rotation: number;
    tint: number;
}

/**
 * Один шаг интеграции (полу-неявный Эйлер) + видимые кривые по жизни.
 * Мутирует p (позиция/скорость/угол/возраст), возвращает визуал.
 */
export function stepParticle(p: ParticleState, dt: number, opts: ParticleSimOptions): ParticleVisual {
    p.age += dt;
    const drag = Math.max(0, 1 - (opts.drag ?? 0) * dt);
    p.vx *= drag;
    p.vy *= drag;
    p.vx += (opts.acceleration?.x ?? 0) * dt;
    p.vy += (opts.acceleration?.y ?? 0) * dt;
    const wobble = opts.wobble ? Math.sin(p.age * 3 + p.k * Math.PI * 2) * opts.wobble : 0;
    p.x += (p.vx + wobble) * dt;
    p.y += p.vy * dt;
    p.rot += p.spin * dt;

    // Альфа: нарастание в начале × затухание в конце.
    const fadeIn = opts.fadeIn ? Math.min(1, p.age / opts.fadeIn) : 1;
    const fadeTail = opts.fadeOut === false ? 1
        : opts.fadeOut === true || opts.fadeOut === undefined
            ? 1 - p.age / p.lifetime
            : Math.min(1, (p.lifetime - p.age) / opts.fadeOut);
    const alpha = Math.max(0, Math.min(fadeIn, fadeTail));

    // Масштаб: кривая по жизни, умноженная на базовый размер частицы.
    const lifeK = Math.min(1, p.age / p.lifetime);
    const curve = opts.scaleOverLife === 'shrink'
        ? 1 - lifeK
        : opts.scaleOverLife === 'grow'
            ? lifeK
            : opts.scaleOverLife
                ? opts.scaleOverLife[0] + (opts.scaleOverLife[1] - opts.scaleOverLife[0]) * lifeK
                : 1;

    return {
        alpha,
        scale: Math.max(0, p.scale * curve),
        rotation: p.rot,
        tint: opts.colorOverLife ? lerpColor(opts.colorOverLife[0], opts.colorOverLife[1], lifeK) : p.tint
    };
}

/** Случайная начальная частица по опциям эмиттера (позиция относительно центра). */
export function sampleSpawn(options: EmitterOptions, rng: Rng): ParticleState {
    const area = options.spawnArea;
    const v = options.velocity;
    return {
        x: area ? rng.int(0, area.width) - area.width / 2 : 0,
        y: area ? rng.int(0, area.height) - area.height / 2 : 0,
        vx: v.x[0] + rng.next() * (v.x[1] - v.x[0]),
        vy: v.y[0] + rng.next() * (v.y[1] - v.y[0]),
        age: 0,
        lifetime:
            options.lifetime[0] + rng.next() * (options.lifetime[1] - options.lifetime[0]),
        rot: sampleRange(options.rotation, rng),
        spin: sampleRange(options.spin, rng),
        scale: options.scale ?? 1,
        tint: options.colors ? options.colors[rng.int(0, options.colors.length - 1)] : options.color ?? 0xffffff,
        k: rng.next()
    };
}

/**
 * Случайный «взрыв»: частицы разлетаются из точки по кругу.
 * speed — (min..max) пикселей/сек; spin/scale — опциональная вариация.
 */
export function sampleBurst(
    count: number,
    speed: [number, number],
    lifetime: [number, number],
    rng: Rng,
    opts: { spin?: [number, number]; scale?: number; colors?: number[]; color?: number } = {}
): ParticleState[] {
    const out: ParticleState[] = [];
    for (let i = 0; i < count; i++) {
        const angle = rng.next() * Math.PI * 2;
        const v = speed[0] + rng.next() * (speed[1] - speed[0]);
        out.push({
            x: 0,
            y: 0,
            vx: Math.cos(angle) * v,
            vy: Math.sin(angle) * v,
            age: 0,
            lifetime: lifetime[0] + rng.next() * (lifetime[1] - lifetime[0]),
            rot: sampleRange(opts.spin, rng) + angle,
            spin: 0,
            scale: opts.scale ?? 1,
            tint: opts.colors ? opts.colors[rng.int(0, opts.colors.length - 1)] : opts.color ?? 0xffffff,
            k: rng.next()
        });
    }
    return out;
}

/** Слияние базовых опций с переопределением burst-а — чистое, без мутации. */
export function mergeEmitterOptions(base: EmitterOptions, over?: Partial<EmitterOptions>): EmitterOptions {
    return over ? { ...base, ...over } : { ...base };
}

function sampleRange(range: [number, number] | undefined, rng: Rng): number {
    return range ? range[0] + rng.next() * (range[1] - range[0]) : 0;
}