import { combine_hash } from "../../../../core/collection/array/combine_hash.js";
import { v3_matrix4_multiply } from "../../../../core/geom/vec3/v3_matrix4_multiply.js";
import { computeHashFloat } from "../../../../core/primitives/numbers/computeHashFloat.js";
import { AbstractFluidEffector } from "./AbstractFluidEffector.js";

const scratch_a = new Float32Array(3);
const scratch_b = new Float32Array(3);

/**
 * Imparts momentum into the fluid along the swept path of a moving source — the wake
 * left by an agent passing through air or water.
 *
 * Each call to {@link apply} treats the segment from {@link previous_position} to
 * {@link position} (both in WORLD coordinates) as a swept tube. The matrix
 * `world_to_grid` converts both endpoints into the target field's grid space, and the
 * effector splats `(position - previous_position) / dt * strength` into every fluid
 * cell within the kernel of that grid-space segment, weighted by a smooth
 * `(1 - d/R)²` falloff. After the splat the effector copies `position` into
 * `previous_position` so the next call automatically tracks the next segment.
 *
 * Works at any user-supplied {@link radius}, including sub-cell. The kernel is
 * internally floored at `1` grid cell — small user radii (bullets, arrows) still
 * produce a roughly 1-cell-wide tube instead of silently missing every cell. The
 * intent: users supply radii in world units without thinking about cell size; the
 * effector picks the smallest physically meaningful footprint when the converted
 * radius is below one cell.
 *
 * When the entity carrying a {@link FluidEffectorsComponent} also has a Transform,
 * {@link FluidSystem} calls {@link syncFromTransform} on this effector each step —
 * which pulls the latest world-space position off the transform. On the very first
 * sync `previous_position` is seeded equal to `position` to suppress the spurious
 * wake from `[0, 0, 0]`.
 *
 * For sources you position manually (no Transform on the entity, or no entity at
 * all), call {@link reset_trail} once after the initial placement and after any
 * teleport so the next apply doesn't splat a wake across the jump.
 */
export class WakeFluidEffector extends AbstractFluidEffector {

    /**
     * Current world-space position of the source. Updated each step from the entity
     * Transform (via {@link syncFromTransform}) when one is available, otherwise set
     * by user code before each step.
     * @type {[number, number, number]}
     */
    position = [0, 0, 0];

    /**
     * Position at the start of the swept segment, world space. {@link apply} copies
     * `position` into this at the end of every successful splat. Caller normally
     * doesn't touch it directly.
     * @type {[number, number, number]}
     */
    previous_position = [0, 0, 0];

    /**
     * Influence radius of the swept tube, in WORLD units. Converted to grid units via
     * the `world_to_grid` matrix's uniform scale, then internally floored to 1 grid
     * cell so sub-cell world-radii still hit cells (see class docstring).
     * @type {number}
     */
    radius = 1;

    /**
     * Multiplier on the imparted velocity. `1` means an on-segment cell takes the
     * source's full grid-space velocity (before falloff weighting). `< 1` softens the
     * wake; `> 1` exaggerates it.
     * @type {number}
     */
    strength = 1;

    /**
     * True once syncFromTransform has been called at least once. Used to seed
     * previous_position on the first sync, avoiding a spurious wake from the origin.
     * @type {boolean}
     */
    #initialized = false;

    /**
     * Snap {@link previous_position} to {@link position}. Call once after manual
     * initial placement, and after any teleport, so the next {@link apply} doesn't
     * splat a wake across the jump.
     */
    reset_trail() {
        this.previous_position[0] = this.position[0];
        this.previous_position[1] = this.position[1];
        this.previous_position[2] = this.position[2];
        this.#initialized = true;
    }

    /**
     * Pull world-space position off the entity Transform. On the first call, also
     * seeds `previous_position` to suppress a wake-from-origin.
     * @param {Transform} transform
     */
    syncFromTransform(transform) {
        const p = transform.position;
        this.position[0] = p.x;
        this.position[1] = p.y;
        this.position[2] = p.z;
        if (!this.#initialized) {
            this.previous_position[0] = p.x;
            this.previous_position[1] = p.y;
            this.previous_position[2] = p.z;
            this.#initialized = true;
        }
    }

    /**
     * @param {FluidField} field
     * @param {number} dt
     * @param {Float32Array|number[]} world_to_grid  Column-major 4×4 affine from world
     *                                               to the target field's grid space.
     */
    apply(field, dt, world_to_grid) {
        if (dt <= 0 || this.strength === 0 || this.radius <= 0) {
            return;
        }

        const m = world_to_grid;

        // World radius → grid radius via uniform scale (length of the matrix's
        // first basis vector). For axis-aligned uniform-scale matrices this is
        // exactly 1/cell_size. Effective kernel is then floored to 1 grid cell so
        // sub-cell radii still hit cells regardless of where the segment falls.
        // `radius <= 0` is handled separately as a no-op so users can explicitly
        // disable.
        const scale = Math.sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]);
        const r = Math.max(1, this.radius * scale);

        // Convert both segment endpoints into grid space.
        v3_matrix4_multiply(scratch_a, 0, this.previous_position, 0, world_to_grid);
        v3_matrix4_multiply(scratch_b, 0, this.position, 0, world_to_grid);

        const ax = scratch_a[0];
        const ay = scratch_a[1];
        const az = scratch_a[2];
        const bx = scratch_b[0];
        const by = scratch_b[1];
        const bz = scratch_b[2];

        const seg_dx = bx - ax;
        const seg_dy = by - ay;
        const seg_dz = bz - az;

        const seg_len_sq = seg_dx * seg_dx + seg_dy * seg_dy + seg_dz * seg_dz;
        if (seg_len_sq === 0) {
            // Source didn't move in grid space — no wake. previous_position is already
            // equal to position (or maps to the same grid cell), leave it alone.
            return;
        }

        // Source velocity * strength = impulse per step (added directly to velocity).
        const inv_dt = 1 / dt;
        const impulse_x = seg_dx * inv_dt * this.strength;
        const impulse_y = seg_dy * inv_dt * this.strength;
        const impulse_z = seg_dz * inv_dt * this.strength;

        const res = field.getResolution();
        const res_x = res[0];
        const res_y = res[1];
        const res_z = res[2];
        const slice_size = res_x * res_y;

        // AABB of the swept tube, clipped to grid.
        const x_min = Math.max(0, Math.floor(Math.min(ax, bx) - r));
        const x_max = Math.min(res_x - 1, Math.ceil(Math.max(ax, bx) + r));
        const y_min = Math.max(0, Math.floor(Math.min(ay, by) - r));
        const y_max = Math.min(res_y - 1, Math.ceil(Math.max(ay, by) + r));
        const z_min = Math.max(0, Math.floor(Math.min(az, bz) - r));
        const z_max = Math.min(res_z - 1, Math.ceil(Math.max(az, bz) + r));

        if (x_min > x_max || y_min > y_max || z_min > z_max) {
            // Swept tube doesn't intersect the field at all.
            this.#advance_trail();
            return;
        }

        const vx = field.velocity_x;
        const vy = field.velocity_y;
        const vz = field.velocity_z;
        const solid = field.solid;

        const r_inv = 1 / r;
        const inv_seg_len_sq = 1 / seg_len_sq;

        for (let z = z_min; z <= z_max; z++) {
            const z_off = z * slice_size;
            const cell_dz = z - az;

            for (let y = y_min; y <= y_max; y++) {
                const y_off = y * res_x;
                const cell_dy = y - ay;

                for (let x = x_min; x <= x_max; x++) {
                    const cell_dx = x - ax;

                    // Project (cell - a) onto segment direction; clamp to [0, 1].
                    let t = (cell_dx * seg_dx + cell_dy * seg_dy + cell_dz * seg_dz) * inv_seg_len_sq;
                    if (t < 0) t = 0;
                    else if (t > 1) t = 1;

                    // Perpendicular offset from the closest point on the segment.
                    const ndx = cell_dx - t * seg_dx;
                    const ndy = cell_dy - t * seg_dy;
                    const ndz = cell_dz - t * seg_dz;

                    const dist = Math.sqrt(ndx * ndx + ndy * ndy + ndz * ndz);
                    const tt = dist * r_inv;
                    if (tt >= 1) {
                        continue;
                    }

                    const w = (1 - tt) * (1 - tt);

                    const c = z_off + y_off + x;
                    if (solid[c] !== 0) {
                        continue;
                    }

                    vx[c] += impulse_x * w;
                    vy[c] += impulse_y * w;
                    vz[c] += impulse_z * w;
                }
            }
        }

        this.#advance_trail();
    }

    /**
     * Shift `previous_position` to `position` so the next apply() picks up where this
     * one left off.
     */
    #advance_trail() {
        this.previous_position[0] = this.position[0];
        this.previous_position[1] = this.position[1];
        this.previous_position[2] = this.position[2];
    }

    /**
     * @param {AbstractFluidEffector} other
     * @return {boolean}
     */
    equals(other) {
        if (other === this) {
            return true;
        }
        if (!(other instanceof WakeFluidEffector)) {
            return false;
        }
        return this.radius === other.radius
            && this.strength === other.strength
            && this.position[0] === other.position[0]
            && this.position[1] === other.position[1]
            && this.position[2] === other.position[2]
            && this.previous_position[0] === other.previous_position[0]
            && this.previous_position[1] === other.previous_position[1]
            && this.previous_position[2] === other.previous_position[2];
    }

    /**
     * @return {number}
     */
    hash() {
        return combine_hash(
            computeHashFloat(this.position[0]),
            computeHashFloat(this.position[1]),
            computeHashFloat(this.position[2]),
            computeHashFloat(this.previous_position[0]),
            computeHashFloat(this.previous_position[1]),
            computeHashFloat(this.previous_position[2]),
            computeHashFloat(this.radius),
            computeHashFloat(this.strength)
        );
    }

    /**
     * World-space AABB of the swept segment, grown by `radius` in each direction.
     * Uses the user-supplied radius (in world units), not the internal min-radius
     * clamp.
     *
     * @param {AABB3} out
     */
    getBoundingBox(out) {
        const r = this.radius;
        const px = this.position[0];
        const py = this.position[1];
        const pz = this.position[2];
        const ppx = this.previous_position[0];
        const ppy = this.previous_position[1];
        const ppz = this.previous_position[2];
        out.setBounds(
            Math.min(px, ppx) - r,
            Math.min(py, ppy) - r,
            Math.min(pz, ppz) - r,
            Math.max(px, ppx) + r,
            Math.max(py, ppy) + r,
            Math.max(pz, ppz) + r
        );
    }
}
