import { mat4 } from "gl-matrix";
import { array_copy } from "../../../../core/collection/array/array_copy.js";
import { AABB3 } from "../../../../core/geom/3d/aabb/AABB3.js";
import { aabb3_from_v3_array_transformed } from "../../../../core/geom/3d/aabb/aabb3_from_v3_array_transformed.js";
import { allocate_m4 } from "../../../../core/geom/3d/mat4/allocate_m4.js";
import { decompose_matrix_4_array } from "../../../../core/geom/3d/mat4/decompose_matrix_4_array.js";
import { Ray3 } from "../../../../core/geom/3d/ray/Ray3.js";
import Quaternion from "../../../../core/geom/Quaternion.js";
import { v3_distance } from "../../../../core/geom/vec3/v3_distance.js";
import Vector3 from "../../../../core/geom/Vector3.js";
import { ray_hit_apply_transform } from "./ray_hit_apply_transform.js";

const local_ray = new Ray3();
let mesh_id_counter = 0;

export class PathTracedMesh {
    /**
     *
     * @type {number}
     */
    id = mesh_id_counter++;

    /**
     *
     * @type {AABB3}
     */
    aabb = new AABB3();

    /**
     *
     * @type {BufferedGeometryBVH|null}
     */
    bvh = null;


    /**
     *
     * @type {THREE.BufferGeometry|null}
     */
    geometry = null;

    /**
     *
     * @type {StandardMaterial|null}
     */
    material = null;

    #transform = allocate_m4();
    #transform_inverse = allocate_m4();

    get transform_inverse() {
        return this.#transform_inverse;
    }

    build_tight_bounds() {
        const position = new Vector3();
        const rotation = new Quaternion();
        const scale = new Vector3();

        decompose_matrix_4_array(this.#transform, position, rotation, scale);

        if (rotation.roughlyEquals(Quaternion.identity)) {
            // no rotation component, can safely scale/translate geometry bounds to get tight fit
            this.update_bounds();

        } else {

            const position_attribute = this.geometry.getAttribute('position');
            const position_array = position_attribute.array;
            aabb3_from_v3_array_transformed(
                this.aabb,
                position_array, position_array.length,
                this.#transform
            );
        }
    }

    update_bounds() {
        this.bvh.getBounds(this.aabb);
        this.aabb.applyMatrix4(this.#transform);
    }

    set transform(m) {
        array_copy(m, 0, this.#transform, 0, 16);

        mat4.invert(this.#transform_inverse, m);

        if (this.bvh !== null) {
            this.update_bounds();
        }
    }

    get transform() {
        return this.#transform;
    }

    /**
     *
     * @param {Ray3} ray
     * @returns {boolean}
     */
    occluded(ray) {

        const m4 = this.#transform_inverse;

        local_ray.copy(ray);
        local_ray.applyMatrix4(m4);

        return this.bvh.occluded(local_ray);

    }

    /**
     *
     * @param {number[]} out
     * @param {number[]|Ray3} ray
     * @param {number} ray_limit overrides Ray.tMax
     * @returns {number} distance along the ray to contact
     */
    hit(out, ray, ray_limit) {
        //transform ray
        const m4 = this.#transform_inverse;

        local_ray.copy(ray);
        local_ray.tMax = ray_limit;

        local_ray.applyMatrix4(m4);

        let distance_to_hit = this.bvh.raycast2(out, local_ray);

        if (distance_to_hit >= 0) {
            // transform output
            ray_hit_apply_transform(out, out, this.#transform);

            out[10] = this.id;

            distance_to_hit = v3_distance(
                ray[0], ray[1], ray[2],
                out[0], out[1], out[2]
            );

        }


        return distance_to_hit;
    }
}
