import { assert } from "../../../../../core/assert.js";
import { BinaryUint32BVH } from "../../../../../core/bvh2/binary/2/BinaryUint32BVH.js";
import { Cache } from "../../../../../core/cache/Cache.js";
import { read_three_planes_to_array } from "../../../../../core/geom/3d/frustum/read_three_planes_to_array.js";
import { bvh32_from_indexed_geometry } from "../../bvh/buffered/bvh32_from_indexed_geometry.js";
import { bvh32_from_unindexed_geometry } from "../../bvh/buffered/bvh32_from_unindexed_geometry.js";
import { buffer_attribute_deinterleave } from "../buffer_attribute_deinterleave.js";
import { buffer_attribute_denormalize } from "../buffer_attribute_denormalize.js";
import { bvh32_geometry_overlap_clipping_volume } from "./bvh32_geometry_overlap_clipping_volume.js";
import { bvh32_geometry_raycast } from "./bvh32_geometry_raycast.js";

/**
 * 4-tuples
 * @type {number[]}
 */
const scratch_planes = []

export class GeometrySpatialQueryAccelerator {
    constructor(
        cache_size = 100 * 1024 * 1024
    ) {

        assert.isNonNegativeInteger(cache_size,'cache_size');
        assert.greaterThanOrEqual(cache_size, 0, 'cache_size must be greater than or equal to 0');

        /**
         * @private
         * @type {Cache<THREE.BufferGeometry, BinaryUint32BVH>}
         */
        this.cache = new Cache({
            maxWeight: cache_size,
            keyHashFunction(geometry) {
                return geometry.id;
            },
            keyEqualityFunction(a, b) {
                return a.id === b.id && a.uuid === b.uuid;
            },
            /**
             *
             * @param {BinaryUint32BVH} tree
             */
            valueWeigher(tree) {
                return tree.estimateByteSize();
            }
        });

    }

    /**
     *
     * @param {number} value in bytes
     */
    set cache_size(value) {
        this.cache.maxWeight = value;
    }

    /**
     *
     * @return {number} in bytes
     */
    get cache_size() {
        return this.cache.maxWeight;
    }

    /**
     *
     * @param {THREE.BufferGeometry} geometry
     * @param {Plane[]} planes
     * @returns {boolean} true means some overlap exists, false otherwise
     */
    queryContainmentViaClippingPlanes(geometry, planes) {
        assert.notNull(geometry, 'geometry');
        assert.defined(geometry, 'geometry');

        const bvh = this.acquire_bvh(geometry);

        read_three_planes_to_array(planes, scratch_planes);

        const geometryIndices = geometry.getIndex()?.array;
        const position_attribute = geometry.getAttribute('position');

        let position_data;
        let stride = 3;
        let position_data_offset = 0;

        if (position_attribute.isInterleavedBufferAttribute) {

            position_data = position_attribute.data.array;

            stride = position_attribute.data.stride;
            position_data_offset = position_attribute.offset;

        } else {

            position_data = position_attribute.array;

            stride = 3;
            position_data_offset = 0;

        }

        return bvh32_geometry_overlap_clipping_volume(
            bvh,
            position_data, position_data_offset, stride, position_attribute.normalized,
            geometryIndices,
            scratch_planes, 0, planes.length
        );
    }

    /**
     *
     * @param {SurfacePoint3} destination
     * @param {THREE.BufferGeometry} geometry
     * @param {number[]|ArrayLike<number>|Float32Array} ray 6 component vector, [origin_x, origin_y, origin_z, direction_x, direction_y, direction_z]
     * @return {boolean}
     */
    queryRaycastNearest_array(destination, geometry, ray) {
        return this.queryRaycastNearest_primitive(
            destination,
            geometry,
            ray[0], ray[1], ray[2],
            ray[3], ray[4], ray[5]
        );
    }

    /**
     *
     * @param {SurfacePoint3} destination
     * @param {THREE.BufferGeometry} geometry
     * @param {number} ray_origin_x
     * @param {number} ray_origin_y
     * @param {number} ray_origin_z
     * @param {number} ray_direction_x
     * @param {number} ray_direction_y
     * @param {number} ray_direction_z
     * @return {boolean}
     */
    queryRaycastNearest_primitive(
        destination,
        geometry,
        ray_origin_x, ray_origin_y, ray_origin_z,
        ray_direction_x, ray_direction_y, ray_direction_z
    ) {
        assert.notNull(geometry, 'geometry');
        assert.defined(geometry, 'geometry');


        let bvh;

        try {
            bvh = this.acquire_bvh(geometry);
        } catch (e) {
            console.error(e);
            return false;
        }

        const geometryIndices = geometry.getIndex()?.array;
        const position_attribute = geometry.getAttribute('position');

        let position_data;
        let stride = 3;
        let position_data_offset = 0;

        if (position_attribute.isInterleavedBufferAttribute) {

            position_data = position_attribute.data.array;

            stride = position_attribute.data.stride;
            position_data_offset = position_attribute.offset;

        } else {

            position_data = position_attribute.array;

            stride = 3;
            position_data_offset = 0;

        }

        return bvh32_geometry_raycast(
            destination,
            bvh,
            position_data,
            position_data_offset,
            stride,
            position_attribute.normalized,
            geometryIndices,
            ray_origin_x, ray_origin_y, ray_origin_z,
            ray_direction_x, ray_direction_y, ray_direction_z,
            Infinity
        );

    }

    /**
     * Get the nearest ray intersection
     * @param {SurfacePoint3} destination
     * @param {THREE.BufferGeometry} geometry
     * @param {Vector3} ray_origin
     * @param {Vector3} ray_direction
     * @returns {boolean} true if hit detected, false otherwise
     */
    queryRaycastNearest(destination, geometry, ray_origin, ray_direction) {

        return this.queryRaycastNearest_primitive(
            destination, geometry,
            ray_origin.x,
            ray_origin.y,
            ray_origin.z,
            ray_direction.x,
            ray_direction.y,
            ray_direction.z
        );

    }

    /**
     * Destroys cache for a given geometry, useful for when geometry changes and cache needs to be invalidated
     * @param {THREE.BufferGeometry} geometry
     * @returns {boolean}
     */
    cache_remove_geometry(geometry) {

        return this.cache.remove(geometry);

    }

    /**
     * NOTE: do not modify the BVH
     * @param {THREE.BufferGeometry} geometry
     * @returns {BinaryUint32BVH}
     */
    acquire_bvh(geometry) {
        assert.notNull(geometry, 'geometry');
        assert.defined(geometry, 'geometry');

        const existing = this.cache.get(geometry);

        if (existing !== null) {
            return existing;
        }

        const bvh = this.__build_bvh(geometry);

        this.cache.put(geometry, bvh);

        return bvh;
    }

    /**
     * @private
     * @param {THREE.BufferGeometry} geometry
     * @returns {BinaryUint32BVH}
     */
    __build_bvh(geometry) {
        const bvh = new BinaryUint32BVH();

        const position_attribute = geometry.getAttribute('position');
        const index_attribute = geometry.getIndex();

        const de_interleaved_position_attribute = buffer_attribute_deinterleave(position_attribute);
        const de_normalized_position_attribute = buffer_attribute_denormalize(de_interleaved_position_attribute);

        const vertices = de_normalized_position_attribute.array;

        if (index_attribute === undefined || index_attribute === null) {

            bvh32_from_unindexed_geometry(bvh, vertices);

        } else {

            const de_interleaved_index_attribute = buffer_attribute_deinterleave(index_attribute);
            const indices = de_interleaved_index_attribute.array;

            bvh32_from_indexed_geometry(bvh, vertices, indices);

        }

        return bvh;
    }

}

/**
 * @readonly
 * @type {GeometrySpatialQueryAccelerator}
 */
GeometrySpatialQueryAccelerator.INSTANCE = new GeometrySpatialQueryAccelerator();
