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_grid_pos = new Float32Array(3);

/**
 * Splats a velocity impulse into a spherical region around a world-space position
 * each step. Useful for fans, jets, point explosions, and unit-testing the projection
 * step.
 *
 * Falloff is quadratic in the normalized distance — `(1 - r/R)²` — giving a smooth
 * brush profile with zero gradient at the boundary, which projects more cleanly than a
 * hard sphere.
 *
 * Everything is in WORLD units: {@link position} (point), {@link force} (direction
 * vector), {@link radius} (scalar length). The `world_to_grid` matrix supplied to
 * {@link apply} converts them at splat time. Works at any user-supplied radius
 * including sub-cell: the effective kernel radius is internally floored at 1 grid
 * cell so a small world-space radius still produces a roughly one-cell-wide splat
 * instead of silently missing every cell.
 *
 * For moving sources that should leave a trail, see {@link WakeFluidEffector}.
 */
export class ImpulseFluidEffector extends AbstractFluidEffector {

    /**
     * Position in world coordinates.
     * @type {[number, number, number]}
     */
    position = [0, 0, 0];

    /**
     * Acceleration vector in WORLD units per second squared. Per-step deposit is
     * `world_to_grid · force × dt × falloff_weight`. Same world-units convention as
     * {@link GlobalFluidEffector.force} — gravity stays gravity across fields with
     * different `cell_size`.
     * @type {[number, number, number]}
     */
    force = [0, 0, 0];

    /**
     * Influence radius in WORLD units. Cells past `radius` from the splat point are
     * unaffected by the falloff. Internally floored to 1 grid cell so sub-cell
     * world-radii still hit cells (the alternative is silently dropping the splat
     * at fractional positions — see class docstring).
     * @type {number}
     */
    radius = 1;

    /**
     * @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 (this.radius <= 0) {
            // Explicit no-op so callers can disable per-frame without zeroing force.
            return;
        }

        const wfx = this.force[0];
        const wfy = this.force[1];
        const wfz = this.force[2];

        if (wfx === 0 && wfy === 0 && wfz === 0) {
            return;
        }

        const m = world_to_grid;

        // Direction-transform the world force into grid units, then scale by dt.
        const fx = (m[0] * wfx + m[4] * wfy + m[8]  * wfz) * dt;
        const fy = (m[1] * wfx + m[5] * wfy + m[9]  * wfz) * dt;
        const fz = (m[2] * wfx + m[6] * wfy + m[10] * wfz) * dt;

        // Point-transform position into grid coords.
        v3_matrix4_multiply(scratch_grid_pos, 0, this.position, 0, world_to_grid);
        const px = scratch_grid_pos[0];
        const py = scratch_grid_pos[1];
        const pz = scratch_grid_pos[2];

        // World radius → grid radius via the uniform-scale factor (length of the
        // matrix's first basis vector). For non-uniform scale this assumes the user
        // is OK with the x-axis scale standing in for all three.
        const scale = Math.sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]);
        // Internal min-radius clamp: see class docstring.
        const r = Math.max(1, this.radius * scale);

        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;

        const x_min = Math.max(0, Math.floor(px - r));
        const x_max = Math.min(res_x - 1, Math.ceil(px + r));
        const y_min = Math.max(0, Math.floor(py - r));
        const y_max = Math.min(res_y - 1, Math.ceil(py + r));
        const z_min = Math.max(0, Math.floor(pz - r));
        const z_max = Math.min(res_z - 1, Math.ceil(pz + r));

        if (x_min > x_max || y_min > y_max || z_min > z_max) {
            return;
        }

        const r_inv = 1 / r;

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

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

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

                for (let x = x_min; x <= x_max; x++) {
                    const dx = x - px;

                    const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
                    const t = dist * r_inv;
                    if (t >= 1) {
                        continue;
                    }
                    const w = (1 - t) * (1 - t);

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

                    vx[c] += fx * w;
                    vy[c] += fy * w;
                    vz[c] += fz * w;
                }
            }
        }
    }

    /**
     * Pull world-space position off the entity Transform.
     * @param {Transform} transform
     */
    syncFromTransform(transform) {
        const p = transform.position;
        this.position[0] = p.x;
        this.position[1] = p.y;
        this.position[2] = p.z;
    }

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

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

    /**
     * World-space box around `position` expanded by `radius`. Uses the user-supplied
     * radius (in world units), not the internal min-radius clamp — broad-phase rejects
     * with the same precision the user expressed.
     *
     * @param {AABB3} out
     */
    getBoundingBox(out) {
        const r = this.radius;
        const px = this.position[0];
        const py = this.position[1];
        const pz = this.position[2];
        out.setBounds(
            px - r, py - r, pz - r,
            px + r, py + r, pz + r
        );
    }
}
