import { assert } from "../assert.js";
import { combine_hash } from "../collection/array/combine_hash.js";
import { EPSILON } from "../math/EPSILON.js";
import { epsilonEquals } from "../math/epsilonEquals.js";
import { PI2 } from "../math/PI2.js";
import { computeHashFloat } from "../primitives/numbers/computeHashFloat.js";
import Vector3 from "./Vector3.js";


/**
 * Represents a direction vector+angle
 * Mainly used inside particle systems for pick a motion direction at spawn
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class ConicRay {

    /**
     * Must be normalized
     * @readonly
     * @type {Vector3}
     */
    direction = new Vector3(0, 1, 0);

    /**
     * Half-angle of the cone that is the angle between the center axis and the edge of the cone
     *
     * @type {number} In radians
     */
    angle = 0;

    toJSON() {
        return {
            direction: this.direction.toJSON(),
            angle: this.angle
        };
    }

    fromJSON(json) {
        this.direction.fromJSON(json.direction);
        this.angle = json.angle;
    }

    /**
     *
     * @param {BinaryBuffer} buffer
     */
    toBinaryBuffer(buffer) {
        buffer.writeFloat64(this.angle);
        this.direction.toBinaryBufferFloat32(buffer);
    }

    /**
     *
     * @param {BinaryBuffer} buffer
     */
    fromBinaryBuffer(buffer) {
        this.angle = buffer.readFloat64();
        this.direction.fromBinaryBufferFloat32(buffer);

        //normalize direction
        this.direction.normalize();
    }

    /**
     *
     * @param {ConicRay} other
     * @returns {boolean}
     */
    equals(other) {
        return this.angle === other.angle
            && this.direction.equals(other.direction)
            ;
    }

    /**
     *
     * @param {ConicRay} other
     * @param {number} [tolerance]
     * @returns {boolean}
     */
    roughlyEquals(other, tolerance = EPSILON) {
        return epsilonEquals(this.angle, other.angle, tolerance)
            && this.direction.roughlyEquals(other.direction, tolerance);
    }

    /**
     *
     * @return {number}
     */
    hash() {
        return combine_hash(
            computeHashFloat(this.angle),
            this.direction.hash()
        );
    }

    /**
     *
     * @param {ConicRay} other
     */
    copy(other) {
        this.direction.copy(other.direction);
        this.angle = other.angle;
    }

    /**
     * Includes boundary matches
     * @param {Vector3} v3
     * @returns {boolean}
     */
    containsDirectionVector(v3) {
        const angular_distance = this.direction.angleTo(v3);

        return angular_distance <= this.angle;
    }

    /**
     * NOTE: Heavily based on a stackoverflow answer
     * @see https://stackoverflow.com/questions/38997302/create-random-unit-vector-inside-a-defined-conical-region/39003745#39003745
     * @param {function} random
     * @param {Vector3} result
     */
    sampleRandomDirection(random, result) {
        const coneAngle = this.angle;

        if (coneAngle === 0) {
            //fast path, just copy ray direction
            result.copy(this.direction);
            return;
        }

        // Generate points on the spherical cap around the north pole [1].
        // [1] See https://math.stackexchange.com/a/205589/81266
        const angleCosine = Math.cos(coneAngle);

        const z = random() * (1 - angleCosine) + angleCosine;

        const phi = random() * PI2;

        const zSqr = z * z;

        const zSqrtNeg = Math.sqrt(1 - zSqr);

        const x = zSqrtNeg * Math.cos(phi);
        const y = zSqrtNeg * Math.sin(phi);

        // If the spherical cap is centered around the north pole, we're done.
        const direction = this.direction;
        if (direction._equals(0, 0, 1)) {
            result.set(x, y, z);
            return;
        }

        assert.ok(direction.isNormalized(), 'direction vector must be normalized');

        // Find the rotation axis `u` and rotation angle `rot` [1]

        const dX = direction.x;
        const dY = direction.y;
        const dZ = direction.z;

        //compute u
        //compute rotation angle
        const rot = Math.acos(dZ);


        // Convert rotation axis and angle to 3x3 rotation matrix [2]
        // [2] See https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle

        //write matrix
        const c = dZ;
        const s = Math.sin(rot);
        const t = 1 - c;
        const tx = -t * dY;
        const ty = t * dX;

        //build relevant terms of the matrix
        const n11 = c - tx * dY;
        const n12 = tx * dX;
        const n22 = ty * dX + c;
        const n13 = s * dX;
        const n32 = -s * dY;


        // Rotate [x; y; z] from north pole to `coneDir`.
        const _x = n11 * x + n12 * y + n13 * z;
        const _y = n12 * x + n22 * y + -n32 * z;
        const _z = -n13 * x + n32 * y + c * z;

        result.set(_x, _y, _z);
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number} z
     * @param {number} half_angle
     * @returns {ConicRay}
     */
    static fromScalars(
        x,
        y,
        z,
        half_angle
    ) {
        assert.isNumber(half_angle, 'angle');
        assert.greaterThanOrEqual(half_angle, 0, 'half_angle must be non-negative');

        const r = new ConicRay();

        r.angle = half_angle;
        r.direction.set(x, y, z);
        r.direction.normalize();

        return r;
    }
}

/**
 * @readonly
 * @type {boolean}
 */
ConicRay.prototype.isConicRay = true;

/**
 * @readonly
 * @type {string}
 */
ConicRay.typeName = "ConicRay";