import { assert } from "../../../../../../core/assert.js";
import { UINT32_MAX } from "../../../../../../core/binary/UINT32_MAX.js";
import { array_copy } from "../../../../../../core/collection/array/array_copy.js";
import {
    decode_octahedron_to_unit
} from "../../../../../../core/geom/3d/normal/octahedron/decode_octahedron_to_unit.js";
import {
    encode_unit_to_octahedron
} from "../../../../../../core/geom/3d/normal/octahedron/encode_unit_to_octahedron.js";
import { Ray3 } from "../../../../../../core/geom/3d/ray/Ray3.js";
import { sphere_fibonacci_point } from "../../../../../../core/geom/3d/sphere/sphere_fibonacci_point.js";
import { v3_distance } from "../../../../../../core/geom/vec3/v3_distance.js";
import { v3_dot_array_array } from "../../../../../../core/geom/vec3/v3_dot_array_array.js";
import { max2 } from "../../../../../../core/math/max2.js";

/**
 * Weights of rays that are aligned with the probe direction get exponentially weighted by this exponent
 * @type {number}
 */
const DEPTH_SHARPNESS = 3;

const scratch_ray = new Ray3();

/**
 *
 * @type {number[]}
 */
const ray_hit = [];

/**
 *
 * @param {number[]} result
 * @param {number} result_offset
 * @param {PathTracedScene} scene
 * @param {number[]|Vector3} position
 * @param {number} position_offset
 * @param {number} resolution
 * @param {number} max_depth
 * @param {number} [density]
 */
export function bake_octahedral_depth_map_fibonacci(
    result, result_offset,
    scene,
    position, position_offset,
    resolution,
    max_depth, density = 4
) {

    const pixel_count = resolution * resolution;

    assert.lessThan(result_offset + pixel_count, UINT32_MAX, 'Result overflow');

    scratch_ray.tMax = max_depth;
    array_copy(position, position_offset, scratch_ray, 0, 3);

    const NORMAL_BIAS = 1e-6;

    const ray_direction = scratch_ray.direction;

    const probe_direction = new Float32Array(3);

    const direction_sample_count = pixel_count * density;

    const octahedral_uv = new Float32Array(2);

    const weights = new Float32Array(pixel_count);


    // clear result data
    for (let i = 0; i < pixel_count; i++) {

        const address = result_offset + i * 2;

        result[address] = 0;
        result[address + 1] = 0;
    }

    for (let sample_index = 0; sample_index < direction_sample_count; sample_index++) {
        sphere_fibonacci_point(ray_direction, 0, sample_index, direction_sample_count);

        encode_unit_to_octahedron(octahedral_uv, 0, ray_direction[0], ray_direction[1], ray_direction[2]);

        const sample_u = octahedral_uv[0] * 0.5 + 0.5;
        const sample_v = octahedral_uv[1] * 0.5 + 0.5;

        const oct_x = (sample_u * resolution) >>> 0;
        const oct_y = (sample_v * resolution) >>> 0;

        // offset position by half a pixel, as we are storing values for pixel centers
        const u = (oct_x + 0.5) / (resolution);
        const v = (oct_y + 0.5) / (resolution);

        decode_octahedron_to_unit(
            probe_direction, 0,
            u * 2 - 1, v * 2 - 1
        );


        let distance = scene.trace(ray_hit, scratch_ray);

        if (distance < 0) {
            // no hit
            distance = max_depth;
        } else {
            const surface_normal_x = ray_hit[3];
            const surface_normal_y = ray_hit[4];
            const surface_normal_z = ray_hit[5];

            const surface_position_x = ray_hit[0];
            const surface_position_y = ray_hit[1];
            const surface_position_z = ray_hit[2];

            const hit_normal_bias = NORMAL_BIAS;

            // sink the contact into the surface along the hit normal
            const biased_hit_x = surface_position_x + surface_normal_x * hit_normal_bias;
            const biased_hit_y = surface_position_y + surface_normal_y * hit_normal_bias;
            const biased_hit_z = surface_position_z + surface_normal_z * hit_normal_bias;

            distance = v3_distance(
                scratch_ray[0], scratch_ray[1], scratch_ray[2],
                biased_hit_x, biased_hit_y, biased_hit_z
            );

        }

        const direction_dot_product = v3_dot_array_array(
            ray_direction, 0,
            probe_direction, 0
        );

        const weight = Math.pow(
            max2(
                0,
                direction_dot_product
            ),
            DEPTH_SHARPNESS
        );

        const mean = distance * weight;
        const mean2 = distance * distance * weight;

        const pixel_index = oct_y * resolution + oct_x;

        const address = result_offset + pixel_index * 2;

        weights[pixel_index] += weight;

        result[address] += mean;
        result[address + 1] += mean2;
    }

    for (let i = 0; i < pixel_count; i++) {

        const weight_sum = weights[i];

        const address = result_offset + i * 2;

        result[address] /= weight_sum;
        result[address + 1] /= weight_sum;
    }

}