import { aabb3_build_corners } from "./aabb3_build_corners.js";
import { aabb_build_frustum } from "./aabb3_build_frustum.js";
import { aabb3_corner_edge_mapping } from "./aabb3_corner_edge_mapping.js";
import { aabb3_edge_corner_mapping } from "./aabb3_edge_corner_mapping.js";
import { plane3_compute_convex_3_plane_intersection } from "../plane/plane3_compute_convex_3_plane_intersection.js";
import { is_point_within_planes } from "../plane/is_point_within_planes.js";
import { v3_distance_above_plane } from "../../vec3/v3_distance_above_plane.js";
import { EPSILON } from "../../../math/EPSILON.js";
import { plane3_compute_line_segment_intersection } from "../plane/plane3_compute_line_segment_intersection.js";

/**
 * Common piece of continuous memory for better cache coherence
 * @type {ArrayBuffer}
 * @private
 */
const __scratch_buffer = new ArrayBuffer(
    24 * 4
    + 24 * 4
    + 8 * 4
    + 3 * 4
);

/**
 *
 * @type {number[]}
 */
const scratch_aabb_corners = new Float32Array(__scratch_buffer, 0, 24);

/**
 *
 * @type {number[]}
 */
const scratch_aabb_planes = new Float32Array(__scratch_buffer, 96, 24);

/**
 *
 * @type {number[]}
 */
const scratch_corner_out_bitfields = new Uint32Array(__scratch_buffer, 192, 8);

/**
 *
 * @type {number[]}
 */
const scratch_v3_array = new Float32Array(__scratch_buffer, 224, 3);

/**
 * Figures out intersection volume between an AABB and a frustum for much tighter bounds
 *
 * Especially beneficial when trying to intersect a large AABB against a small frustum,
 * where where traditionally only AABB corners would be considered,
 * we're instead able to compute minimum bounds of the overlap of the two
 *
 * @param {number[]} destination
 * @param {number} destination_offset
 * @param {number} x0
 * @param {number} y0
 * @param {number} z0
 * @param {number} x1
 * @param {number} y1
 * @param {number} z1
 * @param {number[]} planes
 * @param {number} plane_mask
 * @param {number} plane_count
 * @returns {number}
 */
export function aabb3_detailed_volume_intersection(
    destination, destination_offset,
    x0, y0, z0, x1, y1, z1,
    plane_mask,
    planes,
    plane_count
) {

    aabb3_build_corners(scratch_aabb_corners, 0, x0, y0, z0, x1, y1, z1);
    aabb_build_frustum(scratch_aabb_planes, x0, y0, z0, x1, y1, z1);

    /**
     * Bitfield marking which corners are outside of the frustum
     * @type {number}
     */
    let outside_corner_mask = 0;

    /**
     * Bitfield marking edges that are attached to outside corners
     * @type {number}
     */
    let intersecting_edge_mask = 0;

    /**
     * Bitfield to outside of which planes a particular corner lies
     * @type {number}
     */
    let corner_plane_out_bitfield = 0;

    let destination_cursor = destination_offset;

    // find out which corners are outside the frustum
    for (let corner_index = 0; corner_index < 8; corner_index++) {
        const corner_offset = corner_index * 3;

        const x = scratch_aabb_corners[corner_offset];
        const y = scratch_aabb_corners[corner_offset + 1];
        const z = scratch_aabb_corners[corner_offset + 2];

        corner_plane_out_bitfield = 0;

        // check which points are outside of the frustum sides. Disregard near and far planes at this stage
        for (let plane_index = 0; plane_index < 4; plane_index++) {
            const plane_bit_mask = 1 << plane_index;

            if ((plane_mask & plane_bit_mask) === 0) {
                // non-intersecting plane, ignore
                continue;
            }

            const plane_address = plane_index * 4;

            const nx = planes[plane_address];
            const ny = planes[plane_address + 1];
            const nz = planes[plane_address + 2];
            const c = planes[plane_address + 3];

            const distance_to_plane = v3_distance_above_plane(
                x, y, z,
                nx, ny, nz, c
            );

            if (distance_to_plane < 0) {
                // corner is below the plane

                if (corner_plane_out_bitfield === 0) {
                    // first plane found
                    const corner_edge_bitmask = aabb3_corner_edge_mapping[corner_index];

                    intersecting_edge_mask |= corner_edge_bitmask;

                    outside_corner_mask |= 1 << corner_index;
                }

                corner_plane_out_bitfield |= plane_bit_mask;

            }

        }

        // set bitfield
        scratch_corner_out_bitfields[corner_index] = corner_plane_out_bitfield;

        if (corner_plane_out_bitfield === 0) {
            // corner is fully inside the frustum
            destination[destination_cursor++] = x;
            destination[destination_cursor++] = y;
            destination[destination_cursor++] = z;
        }
    }

    // traverse AABB edges
    for (let edge_index = 0; edge_index < 12; edge_index++) {
        const edge_bit_index = 1 << edge_index;

        if ((intersecting_edge_mask & edge_bit_index) === 0) {
            // non-intersecting edge
            continue;
        }

        const edge_corners_encoded = aabb3_edge_corner_mapping[edge_index];

        const corner_index_0 = edge_corners_encoded & 0b111;
        const corner_index_1 = (edge_corners_encoded >> 3) & 0b111;

        // check to see if both corners are outside
        const corner_0_out_planes_bitfield = scratch_corner_out_bitfields[corner_index_0];
        const corner_1_out_planes_bitfield = scratch_corner_out_bitfields[corner_index_1];

        if ((corner_0_out_planes_bitfield & corner_1_out_planes_bitfield) !== 0) {
            // edge is fully outside one or more of the clipping planes, the edge is definitely outside of the frustum
            continue;
        }

        const corner_0_address = corner_index_0 * 3;
        const corner_1_address = corner_index_1 * 3;

        const corner_0_x = scratch_aabb_corners[corner_0_address];
        const corner_0_y = scratch_aabb_corners[corner_0_address + 1];
        const corner_0_z = scratch_aabb_corners[corner_0_address + 2];

        const corner_1_x = scratch_aabb_corners[corner_1_address];
        const corner_1_y = scratch_aabb_corners[corner_1_address + 1];
        const corner_1_z = scratch_aabb_corners[corner_1_address + 2];

        // check which points are outside of the frustum
        for (let j = 0; j < plane_count; j++) {
            const plane_bit_mask = 1 << j;

            if ((plane_mask & plane_bit_mask) === 0) {
                // non-intersecting plane, ignore
                continue;
            }

            const plane_address = j * 4;

            const nx = planes[plane_address];
            const ny = planes[plane_address + 1];
            const nz = planes[plane_address + 2];
            const c = planes[plane_address + 3];

            const edge_intersects = plane3_compute_line_segment_intersection(
                scratch_v3_array, 0,
                corner_0_x, corner_0_y, corner_0_z,
                corner_1_x, corner_1_y, corner_1_z,
                nx, ny, nz, c
            );

            if (edge_intersects) {
                // a possible candidate, let's see if this point is within rest of the planes
                const inside_planes = is_point_within_planes(
                    scratch_v3_array[0], scratch_v3_array[1], scratch_v3_array[2],
                    (plane_mask ^ plane_bit_mask),
                    planes,
                    plane_count
                );

                if (inside_planes) {
                    destination[destination_cursor++] = scratch_v3_array[0];
                    destination[destination_cursor++] = scratch_v3_array[1];
                    destination[destination_cursor++] = scratch_v3_array[2];
                }
            }


        }
    }


    // traverse AABB planes
    for (let aabb_plane_index = 0; aabb_plane_index < 6; aabb_plane_index++) {
        const aabb_plane_address = aabb_plane_index * 4;

        const aabb_plane_nx = scratch_aabb_planes[aabb_plane_address];
        const aabb_plane_ny = scratch_aabb_planes[aabb_plane_address + 1];
        const aabb_plane_nz = scratch_aabb_planes[aabb_plane_address + 2];
        const aabb_plane_constant = scratch_aabb_planes[aabb_plane_address + 3];

        for (let frustum_plane_index = 0; frustum_plane_index < 4; frustum_plane_index++) {

            const plane_bit_mask = 1 << frustum_plane_index;

            if ((plane_mask & plane_bit_mask) === 0) {
                // non-intersecting plane, ignore
                continue;
            }

            const cp_0 = frustum_plane_index * 4;

            const next_orthogonal_plane_index = (frustum_plane_index + 2) % 4;
            const cp_1 = next_orthogonal_plane_index * 4;

            const intersection_exists = plane3_compute_convex_3_plane_intersection(
                scratch_v3_array, 0,
                aabb_plane_nx, aabb_plane_ny, aabb_plane_nz, aabb_plane_constant,
                planes[cp_0], planes[cp_0 + 1], planes[cp_0 + 2], planes[cp_0 + 3],
                planes[cp_1], planes[cp_1 + 1], planes[cp_1 + 2], planes[cp_1 + 3]
            );

            if (intersection_exists) {

                const intersection_point_x = scratch_v3_array[0];
                const intersection_point_y = scratch_v3_array[1];
                const intersection_point_z = scratch_v3_array[2];

                const point_within_box = !(
                    (intersection_point_x + EPSILON) < x0 || (intersection_point_x - EPSILON) > x1
                    || (intersection_point_y + EPSILON) < y0 || (intersection_point_y - EPSILON) > y1
                    || (intersection_point_z + EPSILON) < z0 || (intersection_point_z - EPSILON) > z1
                );

                if (point_within_box) {
                    // point is within the AABB bounds

                    // ThreeClippingPlaneComputingBVHVisitor.DEBUG_POINTS_2.send3(scratch_v3.x, scratch_v3.y, scratch_v3.z);

                    destination[destination_cursor++] = intersection_point_x;
                    destination[destination_cursor++] = intersection_point_y;
                    destination[destination_cursor++] = intersection_point_z;
                }
            }
        }
    }

    const address_change = destination_cursor - destination_offset;

    return address_change / 3;
}
