import { assert } from "../../../assert.js";
import { array_copy } from "../../../collection/array/array_copy.js";
import { binarySearchHighIndex } from "../../../collection/array/binarySearchHighIndex.js";
import { isArrayEqual } from "../../../collection/array/isArrayEqual.js";
import { max2 } from "../../../math/max2.js";
import { min2 } from "../../../math/min2.js";
import { seededRandom } from "../../../math/random/seededRandom.js";
import { number_compare_ascending } from "../../../primitives/numbers/number_compare_ascending.js";
import { v3_dot } from "../../vec3/v3_dot.js";
import { AbstractShape3D } from "./AbstractShape3D.js";
import { compute_signed_distance_gradient_by_sampling } from "./util/compute_signed_distance_gradient_by_sampling.js";

/**
 * To avoid severe performance overhead, we limit number of possible rejections
 * @type {number}
 */
const SAMPLE_REJECTION_ATTEMPT_MAX = 4;

/**
 * Number of samples taken to estimate overlap level
 * @type {number}
 */
const VOLUME_OVERLAP_ESTIMATION_TAPS = 1024;
const VOLUME_OVERLAP_ESTIMATION_SUB_TAPS = 4;

const scratch_array = [];

export class UnionShape3D extends AbstractShape3D {
    constructor() {
        super();

        /**
         *
         * @type {AbstractShape3D[]}
         */
        this.children = [];

        /**
         * Estimator of how much volume is in overlap,
         * expressed as fraction of volume occupied by overlapping region of 2 shapes over the sum of volume occupied by each child
         * @type {number}
         * @private
         */
        this.__volume_overlap_fraction = -1;

        this.__volume_sum = -1;
        this.__volume_sums = [];
        this.__child_volumes = [];
    }

    compute_bounding_box(result) {
        let x0 = Infinity, y0 = Infinity, z0 = Infinity,
            x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;

        const children = this.children;
        const n = children.length;

        const tmp_aabb3 = new Float32Array(6);

        for (let i = 0; i < n; i++) {
            const child = children[i];

            child.compute_bounding_box(tmp_aabb3);

            x0 = min2(x0, tmp_aabb3[0]);
            y0 = min2(y0, tmp_aabb3[1]);
            z0 = min2(z0, tmp_aabb3[2]);

            x1 = max2(x1, tmp_aabb3[3]);
            y1 = max2(y1, tmp_aabb3[4]);
            z1 = max2(z1, tmp_aabb3[5]);

        }


        result[0] = x0;
        result[1] = y0;
        result[2] = z0;

        result[3] = x1;
        result[4] = y1;
        result[5] = z1;
    }

    /**
     *
     * @private
     */
    __compute_volume_variables() {
        this.__volume_sum = 0;

        const children = this.children;
        const child_count = children.length;

        const child_volumes = this.__child_volumes = new Float32Array(child_count);
        const child_volume_sums = this.__volume_sums = new Float32Array(child_count);

        for (let i = 0; i < child_count; i++) {
            const child_volume = children[i].volume;

            child_volumes[i] = child_volume;
            this.__volume_sum += child_volume;

            child_volume_sums[i] = this.__volume_sum;
        }

    }

    /**
     * Monte-carlo-style volume overlap estimation
     * @private
     */
    __estimate_volume_overlap_fraction() {

        if (this.__volume_sum === -1) {
            this.__compute_volume_variables();
        }

        const children = this.children;
        const child_count = children.length;

        const random = seededRandom(1);

        const child_volume_sums = this.__volume_sums;
        const child_volume_sum = this.__volume_sum;

        let i = 0, j = 0, k = 0, tap_overlaps = 0;

        for (i = 0; i < VOLUME_OVERLAP_ESTIMATION_TAPS; i++) {

            const target_weight = random() * child_volume_sum;

            // weighted search for a sample candidate, preferring larger volumes, resulting in more uniform sample distribution
            const target_child_index = binarySearchHighIndex(child_volume_sums, target_weight, number_compare_ascending, 0, child_count - 1);

            const child_0 = children[target_child_index];

            for (j = 0; j < VOLUME_OVERLAP_ESTIMATION_SUB_TAPS; j++, i++) {

                child_0.sample_random_point_in_volume(scratch_array, 0, random);

                for (k = 0; k < child_count; k++) {
                    if (target_child_index === k) {
                        // skip self
                        continue;
                    }

                    const child_1 = children[k];

                    if (child_1.contains_point(scratch_array)) {
                        tap_overlaps++;
                    }
                }

            }
        }

        this.__volume_overlap_fraction = tap_overlaps / (VOLUME_OVERLAP_ESTIMATION_TAPS);
    }

    /**
     *
     * @param {AbstractShape3D[]} children
     * @returns {UnionShape3D}
     */
    static from(children) {
        assert.isArray(children, 'children');

        const r = new UnionShape3D();

        r.children = children;

        r.__compute_volume_variables();

        return r;
    }

    get volume() {
        if (this.__volume_overlap_fraction < 0) {
            this.__estimate_volume_overlap_fraction();
        }

        let volume_sum = 0;

        const children = this.children;
        const child_count = children.length;

        for (let i = 0; i < child_count; i++) {
            const child = children[i];

            volume_sum += child.volume;
        }

        /**
         * Unbias the volume by taking overlap into account
         * @type {number}
         */
        const unbiased_volume = volume_sum / (this.__volume_overlap_fraction + 1);

        return unbiased_volume;
    }

    get surface_area() {
        let r = 0;

        const children = this.children;
        const child_count = children.length;
        for (let i = 0; i < child_count; i++) {
            const child = children[i];

            // TODO this is incorrect math
            r += child.surface_area;
        }

        return r;
    }

    signed_distance_gradient_at_point(result, point) {
        return compute_signed_distance_gradient_by_sampling(result, this, point);
    }

    signed_distance_at_point(point) {
        let r = Infinity;

        const children = this.children;
        const child_count = children.length;

        for (let i = 0; i < child_count; i++) {
            const child = children[i];

            r = min2(child.signed_distance_at_point(point), r);
        }

        return r;
    }

    contains_point(point) {
        const children = this.children;
        const n = children.length;
        for (let i = 0; i < n; i++) {
            const child = children[i];

            if (child.contains_point(point)) {
                return true;
            }
        }

        return false;
    }

    sample_random_point_in_volume(result, result_offset, random) {
        const children = this.children;
        const child_count = children.length;


        const target_weight = random() * this.__volume_sum;

        // weighted search for a sample candidate, preferring larger volumes, resulting in more uniform sample distribution
        const target_child_index = binarySearchHighIndex(this.__volume_sums, target_weight, number_compare_ascending, 0, child_count - 1);

        const child = children[target_child_index];

        let collision_count, i, j;

        for (i = 0; i < SAMPLE_REJECTION_ATTEMPT_MAX; i++) {

            // sample shape
            child.sample_random_point_in_volume(result, result_offset, random);

            /*
            because underlying shapes can overlap and we wish to offer a union, just returning the sample would result in over-sampling in areas where shapes overlap
            to avoid this oversampling, we reject samples in such areas with probability proportional to number of overlapping shapes at the sampled point
             */
            collision_count = 0;

            for (j = 0; j < child_count; j++) {
                const child_1 = children[j];

                if (child_1 === child) {
                    // exclude sampled child itself
                    continue;
                }

                if (child_1.contains_point(result)) {
                    collision_count++;
                }
            }

            if (collision_count > 0) {
                const rejection_chance = collision_count / (collision_count + 1);

                const rejection_roll = random();

                if (rejection_roll < rejection_chance) {
                    //reject
                    continue;
                }
            }

            // no collisions, or survived rejection
            break;
        }
    }

    support(result, result_offset, direction_x, direction_y, direction_z) {
        let best_distance = -Infinity;

        const children = this.children;
        const child_count = children.length;

        for (let i = 0; i < child_count; i++) {
            const child = children[i];

            child.support(scratch_array, 0, direction_x, direction_y, direction_z);

            const d = v3_dot(
                scratch_array[0], scratch_array[1], scratch_array[2],
                direction_x, direction_y, direction_z
            );

            if (d > best_distance) {
                // best candidate so far, write to result
                array_copy(scratch_array, 0, result, result_offset, 3);

                best_distance = d;
            }

        }

    }

    /**
     *
     * @param {UnionShape3D} other
     * @returns {boolean}
     */
    equals(other) {
        return super.equals(other)
            && isArrayEqual(this.children, other.children);
    }

    hash() {
        const children = this.children;
        const child_count = children.length;

        let hash = child_count;

        if (child_count > 0) {
            hash ^= children[0].hash();
        }

        return hash;
    }
}

/**
 * The set-union of convex shapes is convex only in the trivial cases (one
 * child, or fully overlapping children). The general case is non-convex,
 * so the narrowphase must decompose the union into its children and run
 * per-child GJK against the other body, then merge the resulting contact
 * manifolds. Treating the union as a single convex blob via GJK on a
 * combined support would conflate distinct children into a fake hull.
 *
 * @readonly
 * @type {boolean}
 */
UnionShape3D.prototype.is_convex = false;
