import { AABB3 } from "../../../../core/geom/3d/aabb/AABB3.js";
import { aabb3_intersects_aabb3 } from "../../../../core/geom/3d/aabb/aabb3_intersects_aabb3.js";
import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js";
import { System } from "../../../ecs/System.js";
import { Transform } from "../../../ecs/transform/Transform.js";
import { FluidComponent } from "./FluidComponent.js";
import { FluidEffectorsComponent } from "./FluidEffectorsComponent.js";
import { FluidSimulator } from "../FluidSimulator.js";

/**
 * ECS system that steps every {@link FluidComponent} on a fixed timestep.
 *
 * Effectors live on a separate {@link FluidEffectorsComponent}, decoupled from any
 * specific fluid field. Each step the system:
 *
 *   1. **Re-anchor moving fields.** For every (FluidComponent, Transform) tuple,
 *      compute the desired grid origin as
 *          `quantize(transform.position, cell_size) + offset`
 *      where the grid is then centred at that point. On the first sync this is just
 *      snapped (no shift), on subsequent syncs the integer cell delta is computed and
 *      the field is shifted to follow — fluid data moves with the world so the volume
 *      appears stationary while the grid hops between cells.
 *   2. Walks every (FluidEffectorsComponent, Transform) entity and calls
 *      {@link AbstractFluidEffector.syncFromTransform} on each effector so position-
 *      bearing effectors (wakes, splats) follow their owning entity.
 *   3. For each enabled {@link FluidComponent}:
 *      a. Builds the world-to-grid matrix from the component's `origin` + `cell_size`.
 *      b. Broad-phase filters effectors via {@link AbstractFluidEffector.getBoundingBox}
 *         vs {@link FluidComponent.getBoundingBox} — only effectors whose world-space
 *         AABB intersects the field's AABB are passed through.
 *      c. Calls the owned simulator's `step(field, dt, visible_effectors, world_to_grid)`.
 *
 * The broad-phase is cheap (AABB-AABB intersect, 6 comparisons) and skips the expensive
 * per-cell splat work for effectors that wouldn't have hit anything anyway. Gather +
 * filter happens per field; effectors that overlap multiple fields are evaluated
 * multiple times — fine for the typical "few fields" case, optimise to a cached
 * per-effector AABB pass if you find yourself with many fields.
 *
 * Uses `fixedUpdate` rather than `update` because fluid stability and the warm-started
 * pressure solve both depend on consistent dt. Variable timesteps cause visible
 * pulsing as projection convergence varies.
 */
export class FluidSystem extends System {

    dependencies = [FluidComponent];

    /**
     * Components the system traverses each step but does not own as the linking
     * tuple. Lets the engine validate registration order and schedule reads.
     *
     *   - `FluidEffectorsComponent` — read every step to gather effectors and to
     *     deliver Transform sync hooks.
     *   - `Transform` — read every step (paired with FluidEffectorsComponent) so
     *     position-bearing effectors can follow their entity.
     */
    components_used = [
        // FluidComponent is also in `dependencies`, but reanchor + step write to it
        // (origin, anchored, field buffers). Declared again with explicit Write.
        ResourceAccessSpecification.from(FluidComponent, ResourceAccessKind.Read | ResourceAccessKind.Write),
        ResourceAccessSpecification.from(FluidEffectorsComponent, ResourceAccessKind.Read),
        ResourceAccessSpecification.from(Transform, ResourceAccessKind.Read),
    ];

    /**
     * Single simulator drives every fluid field — it carries only transient scratch,
     * which {@link FluidSimulator.#ensure_scratch} grows to fit the largest field
     * encountered.
     * @type {FluidSimulator}
     */
    simulator = new FluidSimulator();

    /**
     * Per-field filtered effector list, rebuilt at the top of each field's step.
     * Held as an instance field so we don't allocate per frame.
     * @type {AbstractFluidEffector[]}
     */
    #visible_effectors = [];

    /**
     * Reusable column-major 4×4 buffer for the world-to-grid matrix; rewritten per
     * field per step.
     * @type {Float32Array}
     */
    #world_to_grid = new Float32Array(16);

    /**
     * Reusable AABB scratch for the current field being stepped.
     * @type {AABB3}
     */
    #field_aabb = new AABB3();

    /**
     * Reusable AABB scratch reused for every effector during broad-phase filtering.
     * @type {AABB3}
     */
    #effector_aabb = new AABB3();

    /**
     * @param {number} time_delta_seconds
     */
    fixedUpdate(time_delta_seconds) {
        const em = this.entityManager;
        if (em === null) {
            return;
        }
        const ecd = em.dataset;
        if (ecd === null) {
            return;
        }

        // 1. Re-anchor moving fields. Field origin tracks quantize(transform) + offset;
        //    when the quantization changes, the field is shifted to follow the world.
        ecd.traverseEntities([FluidComponent, Transform], FluidSystem.#reanchor_field);

        // 2. Sync every effector that wants to follow its entity's Transform.
        ecd.traverseEntities([FluidEffectorsComponent, Transform], FluidSystem.#sync_effectors_from_transform);

        // 2. Step each fluid field, with broad-phase effector filtering per field.
        const sim = this.simulator;
        const world_to_grid = this.#world_to_grid;
        const field_aabb = this.#field_aabb;
        const effector_aabb = this.#effector_aabb;
        const visible = this.#visible_effectors;

        ecd.traverseComponents(FluidComponent, function (component) {
            if (!component.enabled) {
                return;
            }

            component.getBoundingBox(field_aabb);
            FluidSystem.#build_world_to_grid(world_to_grid, component);

            visible.length = 0;
            ecd.traverseComponents(FluidEffectorsComponent, function (effectors_component) {
                const arr = effectors_component.effectors;
                for (let i = 0; i < arr.length; i++) {
                    const e = arr[i];
                    e.getBoundingBox(effector_aabb);
                    if (aabb3_intersects_aabb3(
                        effector_aabb.x0, effector_aabb.y0, effector_aabb.z0,
                        effector_aabb.x1, effector_aabb.y1, effector_aabb.z1,
                        field_aabb.x0, field_aabb.y0, field_aabb.z0,
                        field_aabb.x1, field_aabb.y1, field_aabb.z1
                    )) {
                        visible.push(e);
                    }
                }
            });

            sim.step(component.field, time_delta_seconds, visible, world_to_grid);
        });
    }

    /**
     * Visitor for the (effectors, transform) traversal — calls syncFromTransform on
     * every effector in the component.
     * @param {FluidEffectorsComponent} effectors_component
     * @param {Transform} transform
     */
    static #sync_effectors_from_transform(effectors_component, transform) {
        const effectors = effectors_component.effectors;
        for (let i = 0; i < effectors.length; i++) {
            effectors[i].syncFromTransform(transform);
        }
    }

    /**
     * Visitor for the (FluidComponent, Transform) traversal — keeps the field's
     * grid origin locked to a cell-aligned position near the transform.
     *
     * Centre formula: `q * cell_size + offset`, where `q` is the cell index along
     * each axis. Origin is then the centre minus half the grid extent so the centre
     * lands at the geometric middle of the grid.
     *
     * Hysteresis: instead of recomputing `q` from the raw position every frame
     * (which would re-shift on every boundary crossing for a Transform jittering on
     * a cell edge), we recover the current `q` from the existing origin and only
     * advance to a new cell when the raw position has drifted more than
     * `0.5 + hysteresis` cells from it. Crossing the threshold snaps to the new
     * nearest cell, so a single large move still produces a single discrete shift.
     *
     * First sync (`anchored = false`): snap origin to the desired value, no shift.
     * Subsequent syncs: per-axis hysteretic quantization, then `field.shift` if the
     * integer delta is non-zero.
     *
     * @param {FluidComponent} component
     * @param {Transform} transform
     */
    static #reanchor_field(component, transform) {
        const cs = component.cell_size;
        const p = transform.position;
        const offset = component.offset;
        const hyst = component.hysteresis;

        const res = component.field.getResolution();
        const half_extent_x = (res[0] - 1) * cs * 0.5;
        const half_extent_y = (res[1] - 1) * cs * 0.5;
        const half_extent_z = (res[2] - 1) * cs * 0.5;

        const origin = component.origin;

        if (!component.anchored) {
            // First sync — adopt the freshly-quantized origin without shifting.
            // Subsequent frames will see integer-cs differences and shift instead.
            const q_x = Math.round(p.x / cs);
            const q_y = Math.round(p.y / cs);
            const q_z = Math.round(p.z / cs);
            origin[0] = q_x * cs + offset[0] - half_extent_x;
            origin[1] = q_y * cs + offset[1] - half_extent_y;
            origin[2] = q_z * cs + offset[2] - half_extent_z;
            component.anchored = true;
            return;
        }

        // Recover the current quantized cell index per axis from the existing origin.
        // origin_axis = q_curr * cs + offset - half_extent  →
        //     q_curr = (origin_axis + half_extent - offset) / cs.
        // Round defends against floating drift; the value should already be integer.
        const q_curr_x = Math.round((origin[0] + half_extent_x - offset[0]) / cs);
        const q_curr_y = Math.round((origin[1] + half_extent_y - offset[1]) / cs);
        const q_curr_z = Math.round((origin[2] + half_extent_z - offset[2]) / cs);

        // Raw (fractional) cell coordinate the Transform sits at right now.
        const raw_x = p.x / cs;
        const raw_y = p.y / cs;
        const raw_z = p.z / cs;

        // Hysteretic re-quantize: only adopt a new cell when raw has drifted past
        // 0.5 + hyst from the current one. Snap to the new nearest, not the threshold.
        const threshold = 0.5 + hyst;
        const q_new_x = Math.abs(raw_x - q_curr_x) > threshold ? Math.round(raw_x) : q_curr_x;
        const q_new_y = Math.abs(raw_y - q_curr_y) > threshold ? Math.round(raw_y) : q_curr_y;
        const q_new_z = Math.abs(raw_z - q_curr_z) > threshold ? Math.round(raw_z) : q_curr_z;

        const shift_x = q_new_x - q_curr_x;
        const shift_y = q_new_y - q_curr_y;
        const shift_z = q_new_z - q_curr_z;

        if (shift_x === 0 && shift_y === 0 && shift_z === 0) {
            return;
        }

        component.field.shift(shift_x, shift_y, shift_z);
        origin[0] = q_new_x * cs + offset[0] - half_extent_x;
        origin[1] = q_new_y * cs + offset[1] - half_extent_y;
        origin[2] = q_new_z * cs + offset[2] - half_extent_z;
    }

    /**
     * Write the world-to-grid affine for a FluidComponent into `out`. Axis-aligned,
     * uniform-scale, so the matrix is sparse:
     *
     *     [ 1/cs   0     0    -ox/cs ]
     *     [ 0      1/cs  0    -oy/cs ]
     *     [ 0      0     1/cs -oz/cs ]
     *     [ 0      0     0     1     ]
     *
     * Stored column-major (matches the convention of {@link v3_matrix4_multiply}).
     *
     * @param {Float32Array} out  length-16
     * @param {FluidComponent} component
     */
    static #build_world_to_grid(out, component) {
        const inv_cs = 1 / component.cell_size;
        const ox = component.origin[0];
        const oy = component.origin[1];
        const oz = component.origin[2];

        out[0]  = inv_cs;  out[1]  = 0;       out[2]  = 0;       out[3]  = 0;
        out[4]  = 0;       out[5]  = inv_cs;  out[6]  = 0;       out[7]  = 0;
        out[8]  = 0;       out[9]  = 0;       out[10] = inv_cs;  out[11] = 0;
        out[12] = -ox * inv_cs;
        out[13] = -oy * inv_cs;
        out[14] = -oz * inv_cs;
        out[15] = 1;
    }
}
