import { assert } from "../../../assert.js";
import { clamp } from "../../../math/clamp.js";
import { sign_not_zero } from "../../../math/sign_not_zero.js";
import { computeHashFloat } from "../../../primitives/numbers/computeHashFloat.js";
import { randomPointInCircle } from "../../random/randomPointInCircle.js";
import { AbstractShape3D } from "./AbstractShape3D.js";
import { compute_signed_distance_gradient_by_sampling } from "./util/compute_signed_distance_gradient_by_sampling.js";

const scratch_v3 = [0, 0, 0];

/**
 * A solid right circular cylinder aligned with the Y axis, centred at the local
 * origin. A disk of {@link radius} extruded along Y for {@link height}, so it
 * spans `y ∈ [-height/2, +height/2]` with **flat** circular caps (the capsule's
 * flat-cap sibling — same radius/height parametrisation, hemispherical caps
 * replaced by disks).
 *
 * Convex, so the physics narrowphase handles it through the GJK + EPA fallback
 * via {@link support} — there is no closed-form cylinder-vs-X contact path yet
 * (a future refinement; the curved side is the usual smooth-support EPA case).
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class CylinderShape3D extends AbstractShape3D {
    constructor() {
        super();

        /**
         * Radius of the circular cross-section.
         * @type {number}
         */
        this.radius = 0.5;

        /**
         * Full height along the Y axis (total extent — the cylinder spans
         * `[-height/2, +height/2]`).
         * @type {number}
         */
        this.height = 1;
    }

    /**
     * @param {number} radius
     * @param {number} height
     * @returns {CylinderShape3D}
     */
    static from(radius, height) {
        assert.isNumber(radius, 'radius');
        assert.isNumber(height, 'height');
        assert.greaterThanOrEqual(radius, 0, 'radius');
        assert.greaterThanOrEqual(height, 0, 'height');

        const r = new CylinderShape3D();
        r.radius = radius;
        r.height = height;

        return r;
    }

    get volume() {
        const r = this.radius;
        return Math.PI * r * r * this.height;
    }

    get surface_area() {
        const r = this.radius;
        // two circular caps + the lateral (side) surface
        return 2 * Math.PI * r * r + 2 * Math.PI * r * this.height;
    }

    compute_bounding_box(result) {
        const r = this.radius;
        const half_h = this.height / 2;

        result[0] = -r;
        result[1] = -half_h;
        result[2] = -r;
        result[3] = r;
        result[4] = half_h;
        result[5] = r;
    }

    support(result, result_offset, direction_x, direction_y, direction_z) {
        const r = this.radius;
        const half_h = this.height / 2;

        // Radial: farthest point on the cross-section disk along (dx, dz). For a
        // purely axial query (rl ≈ 0) any point on the cap face is extremal —
        // the cap centre (0,0) is on the (flat) surface, so it's a valid support.
        const rl = Math.sqrt(direction_x * direction_x + direction_z * direction_z);
        let sx = 0, sz = 0;
        if (rl > 0) {
            const inv = r / rl;
            sx = direction_x * inv;
            sz = direction_z * inv;
        }

        // Axial: the cap on the +direction.y side.
        const sy = sign_not_zero(direction_y) * half_h;

        result[result_offset] = sx;
        result[result_offset + 1] = sy;
        result[result_offset + 2] = sz;
    }

    signed_distance_at_point(point) {
        const px = point[0];
        const py = point[1];
        const pz = point[2];

        const half_h = this.height / 2;

        // Inigo Quilez capped-cylinder SDF, axis = Y. `d_radial` / `d_axial` are
        // the signed distances outside the infinite cylinder and the slab.
        const d_radial = Math.sqrt(px * px + pz * pz) - this.radius;
        const d_axial = Math.abs(py) - half_h;

        const ext_r = d_radial > 0 ? d_radial : 0;
        const ext_a = d_axial > 0 ? d_axial : 0;
        const outside = Math.sqrt(ext_r * ext_r + ext_a * ext_a);

        const m = d_radial > d_axial ? d_radial : d_axial;
        const inside = m < 0 ? m : 0;

        return outside + inside;
    }

    signed_distance_gradient_at_point(result, point) {
        return compute_signed_distance_gradient_by_sampling(result, this, point);
    }

    contains_point(point) {
        const px = point[0];
        const py = point[1];
        const pz = point[2];

        const half_h = this.height / 2;
        const r = this.radius;

        return (px * px + pz * pz) < r * r && (py < half_h) && (py > -half_h);
    }

    nearest_point_on_surface(result, reference) {
        const rx = reference[0];
        const ry = reference[1];
        const rz = reference[2];

        const r = this.radius;
        const half_h = this.height / 2;

        const rho = Math.sqrt(rx * rx + rz * rz);
        let dirx, dirz;
        if (rho > 0) {
            dirx = rx / rho;
            dirz = rz / rho;
        } else {
            // On the axis — radial direction is undefined; pick +X.
            dirx = 1;
            dirz = 0;
        }

        // Three candidate projections; the nearest to the reference is the
        // closest surface point (correct for points inside and outside).
        //   1. side    — radial clamped to r, y clamped to the band
        //   2. top cap — y = +half_h, radial clamped to r
        //   3. bot cap — y = -half_h, radial clamped to r
        const cy = clamp(ry, -half_h, half_h);
        const s1x = r * dirx, s1y = cy, s1z = r * dirz;

        const rc = rho < r ? rho : r;
        const s2x = rc * dirx, s2y = half_h, s2z = rc * dirz;
        const s3x = rc * dirx, s3y = -half_h, s3z = rc * dirz;

        const d1 = (s1x - rx) * (s1x - rx) + (s1y - ry) * (s1y - ry) + (s1z - rz) * (s1z - rz);
        const d2 = (s2x - rx) * (s2x - rx) + (s2y - ry) * (s2y - ry) + (s2z - rz) * (s2z - rz);
        const d3 = (s3x - rx) * (s3x - rx) + (s3y - ry) * (s3y - ry) + (s3z - rz) * (s3z - rz);

        if (d1 <= d2 && d1 <= d3) {
            result[0] = s1x; result[1] = s1y; result[2] = s1z;
        } else if (d2 <= d3) {
            result[0] = s2x; result[1] = s2y; result[2] = s2z;
        } else {
            result[0] = s3x; result[1] = s3y; result[2] = s3z;
        }
    }

    sample_random_point_in_volume(result, result_offset, random) {
        // Uniform in the disk (sqrt-weighted radius) × uniform in y.
        randomPointInCircle(random, scratch_v3, 0);

        result[result_offset] = scratch_v3[0] * this.radius;
        result[result_offset + 1] = (random() - 0.5) * this.height;
        result[result_offset + 2] = scratch_v3[1] * this.radius;
    }

    /**
     * @param {CylinderShape3D} other
     * @returns {boolean}
     */
    equals(other) {
        return super.equals(other)
            && this.radius === other.radius
            && this.height === other.height;
    }

    hash() {
        const a = computeHashFloat(this.radius);
        const b = computeHashFloat(this.height);

        return ((a << 5) - a + b) | 0;
    }
}

/**
 * Fast type-check marker, mirroring `isSphereShape3D` / `isBoxShape3D` /
 * `isCapsuleShape3D`. Reserved for a future closed-form cylinder narrowphase
 * dispatch; until then the cylinder routes through GJK + EPA like any other
 * convex shape (no marker check needed for that path).
 * @readonly
 * @type {boolean}
 */
CylinderShape3D.prototype.isCylinderShape3D = true;
