import { assert } from "../../../assert.js";
import { computeHashFloatArray } from "../../../math/hash/computeHashFloatArray.js";
import Vector3 from "../../Vector3.js";
import { aabb3_build_corners } from "./aabb3_build_corners.js";
import { aabb3_compute_distance_above_plane_max } from "./aabb3_compute_distance_above_plane_max.js";
import { aabb3_compute_plane_side } from "./aabb3_compute_plane_side.js";
import { aabb3_compute_surface_area } from "./aabb3_compute_surface_area.js";
import { aabb3_intersects_clipping_volume_array } from "./aabb3_intersects_clipping_volume_array.js";
import { aabb3_intersects_frustum_degree } from "./aabb3_intersects_frustum_degree.js";
import { aabb3_intersects_line_segment } from "./aabb3_intersects_line_segment.js";
import { aabb3_intersects_ray } from "./aabb3_intersects_ray.js";
import { aabb3_matrix4_project } from "./aabb3_matrix4_project.js";
import { aabb3_signed_distance_sqr_to_point } from "./aabb3_signed_distance_sqr_to_point.js";
import { aabb3_signed_distance_to_aabb3 } from "./aabb3_signed_distance_to_aabb3.js";

/**
 * Axis-Aligned bounding box in 3D
 * NOTE: In cases where all you want is raw performance - prefer to use typed arrays instead along with `aabb3_` functions
 * @extends {ArrayLike.<number>}
 */
export class AABB3 {
    /**
     *
     * @param {number} [x0]
     * @param {number} [y0]
     * @param {number} [z0]
     * @param {number} [x1]
     * @param {number} [y1]
     * @param {number} [z1]
     * @constructor
     */
    constructor(
        x0 = 0,
        y0 = 0,
        z0 = 0,
        x1 = 0,
        y1 = 0,
        z1 = 0
    ) {
        this.setBounds(x0, y0, z0, x1, y1, z1);
    }

    * [Symbol.iterator]() {

        yield this.x0;
        yield this.y0;
        yield this.z0;

        yield this.x1;
        yield this.y1;
        yield this.z1;

    }

    /**
     *
     * @returns {number}
     */
    get 0() {
        return this.x0;
    }


    /**
     *
     * @returns {number}
     */
    get 1() {
        return this.y0;
    }

    /**
     *
     * @returns {number}
     */
    get 2() {
        return this.z0;
    }


    /**
     *
     * @returns {number}
     */
    get 3() {
        return this.x1;
    }

    /**
     *
     * @returns {number}
     */
    get 4() {
        return this.y1;
    }

    /**
     *
     * @returns {number}
     */
    get 5() {
        return this.z1;
    }

    /**
     *
     * @param {number} v
     */
    set 0(v) {
        this.x0 = v;
    }

    /**
     *
     * @param {number} v
     */
    set 1(v) {
        this.y0 = v;
    }

    /**
     *
     * @param {number} v
     */
    set 2(v) {
        this.z0 = v;
    }

    /**
     *
     * @param {number} v
     */
    set 3(v) {
        this.x1 = v;
    }

    /**
     *
     * @param {number} v
     */
    set 4(v) {
        this.y1 = v;
    }

    /**
     *
     * @param {number} v
     */
    set 5(v) {
        this.z1 = v;
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number} z
     * @param {number} tolerance
     * @returns {boolean}
     */
    containsPointWithTolerance(x, y, z, tolerance) {
        return !((x + tolerance) < this.x0 || (x - tolerance) > this.x1
            || (y + tolerance) < this.y0 || (y - tolerance) > this.y1
            || (z + tolerance) < this.z0 || (z - tolerance) > this.z1);
    }

    /**
     *
     * @returns {number}
     */
    computeSurfaceArea() {
        return aabb3_compute_surface_area(this.x0, this.y0, this.z0, this.x1, this.y1, this.z1);
    }

    /**
     *
     * @returns {number}
     */
    getSurfaceArea() {
        return this.computeSurfaceArea();
    }

    /**
     *
     * @returns {number}
     */
    computeVolume() {
        return this.getExtentsX() * this.getExtentsY() * this.getExtentsZ();
    }

    /**
     *
     * @param {AABB3} other
     */
    copy(other) {
        this.setBounds(other.x0, other.y0, other.z0, other.x1, other.y1, other.z1);
    }

    /**
     *
     * @param {Number} x0
     * @param {Number} y0
     * @param {Number} z0
     * @param {Number} x1
     * @param {Number} y1
     * @param {Number} z1
     */
    setBounds(x0, y0, z0, x1, y1, z1) {
        assert.notNaN(x0, 'x0');
        assert.notNaN(y0, 'y0');
        assert.notNaN(z0, 'z0');
        assert.notNaN(x1, 'x1');
        assert.notNaN(y1, 'y1');
        assert.notNaN(z1, 'z1');

        /**
         *
         * @type {number}
         */
        this.x0 = x0;
        /**
         *
         * @type {number}
         */
        this.y0 = y0;
        /**
         *
         * @type {number}
         */
        this.z0 = z0;
        /**
         *
         * @type {number}
         */
        this.x1 = x1;
        /**
         *
         * @type {number}
         */
        this.y1 = y1;
        /**
         *
         * @type {number}
         */
        this.z1 = z1;
    }

    /**
     *
     * @param {AABB3} other
     * @returns {boolean}
     */
    equals(other) {
        assert.notNull(other, 'other');
        assert.defined(other, 'other');
        assert.equal(other.isAABB3, true, 'other.isAABB3 !== true');

        return this._equals(other.x0, other.y0, other.z0, other.x1, other.y1, other.z1);
    }

    /**
     *
     * @param {Number} x0
     * @param {Number} y0
     * @param {Number} z0
     * @param {Number} x1
     * @param {Number} y1
     * @param {Number} z1
     * @returns {boolean}
     */
    _equals(x0, y0, z0, x1, y1, z1) {
        return this.x0 === x0
            && this.y0 === y0
            && this.z0 === z0
            && this.x1 === x1
            && this.y1 === y1
            && this.z1 === z1;
    }

    /**
     * @returns {number}
     */
    hash() {
        return computeHashFloatArray(this);
    }

    /**
     * Same as setBounds, but does not require component pairs to be ordered (e.g. x0 <= x1). Method will enforce the correct order and invoke setBounds internally
     * @param {Number} x0
     * @param {Number} y0
     * @param {Number} z0
     * @param {Number} x1
     * @param {Number} y1
     * @param {Number} z1
     */
    setBoundsUnordered(x0, y0, z0, x1, y1, z1) {

        // sort bound coordinates
        let _x0, _y0, _z0, _x1, _y1, _z1;
        if (x0 < x1) {
            _x0 = x0;
            _x1 = x1;
        } else {
            _x0 = x1;
            _x1 = x0;
        }
        if (y0 < y1) {
            _y0 = y0;
            _y1 = y1;
        } else {
            _y0 = y1;
            _y1 = y0;
        }
        if (z0 < z1) {
            _z0 = z0;
            _z1 = z1;
        } else {
            _z0 = z1;
            _z1 = z0;
        }

        // write sorted
        this.setBounds(_x0, _y0, _z0, _x1, _y1, _z1);

    }

    setNegativelyInfiniteBounds() {
        this.setBounds(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
    }

    setInfiniteBounds() {
        this.setBounds(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number} z
     */
    _translate(x, y, z) {
        this.setBounds(
            this.x0 + x, this.y0 + y, this.z0 + z,
            this.x1 + x, this.y1 + y, this.z1 + z
        )
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number} z
     * @returns {number} Squared distance to point, value is negative if the point is inside the box
     * @deprecated use {@link aabb3_signed_distance_sqr_to_point} directly
     */
    distanceToPoint2(x, y, z) {
        return aabb3_signed_distance_sqr_to_point(
            this.x0, this.y0, this.z0,
            this.x1, this.y1, this.z1,
            x, y, z
        );
    }

    /**
     *
     * @param {AABB3} box
     * @returns {number}
     */
    distanceToBox(box) {
        return this._distanceToBox(box.x0, box.y0, box.z0, box.x1, box.y1, box.z1);
    }

    /**
     * Computes separation distance between two boxes.
     * If poxes penetrate or one is inside another - the result will be negative.
     * @param {number} x0
     * @param {number} y0
     * @param {number} z0
     * @param {number} x1
     * @param {number} y1
     * @param {number} z1
     * @returns {number}
     */
    _distanceToBox(x0, y0, z0, x1, y1, z1) {
        const _x0 = this.x0;
        const _y0 = this.y0;
        const _z0 = this.z0;
        const _x1 = this.x1;
        const _y1 = this.y1;
        const _z1 = this.z1;

        return aabb3_signed_distance_to_aabb3(
            _x0, _y0, _z0, _x1, _y1, _z1,
            x0, y0, z0, x1, y1, z1
        );
    }

    /**
     *
     * @param {AABB3} other
     * @returns {number}
     * @deprecated
     */
    costForInclusion(other) {
        return this._costForInclusion(other.x0, other.y0, other.z0, other.x1, other.y1, other.z1);
    }

    /**
     * Surface area delta when including a given AABB
     * 0 means that including a given AABB would not cause any change to total surface area
     * @param {number} x0
     * @param {number} y0
     * @param {number} z0
     * @param {number} x1
     * @param {number} y1
     * @param {number} z1
     * @returns {number}
     * @deprecated
     */
    _costForInclusion(x0, y0, z0, x1, y1, z1) {
        let x = 0;
        let y = 0;
        let z = 0;
        //
        const _x0 = this.x0;
        const _y0 = this.y0;
        const _z0 = this.z0;
        const _x1 = this.x1;
        const _y1 = this.y1;
        const _z1 = this.z1;
        //
        if (_x0 > x0) {
            x += _x0 - x0;
        }
        if (_x1 < x1) {
            x += x1 - _x1;
        }
        if (_y0 > y0) {
            y += _y0 - y0;
        }
        if (_y1 < y1) {
            y += y1 - _y1;
        }
        if (_z0 > z0) {
            z += _z0 - z0;
        }
        if (_z1 < z1) {
            z += z1 - _z1;
        }

        const dx = _x1 - _x0;
        const dy = _y1 - _y0;
        const dz = _z1 - _z0;

        return (x * (dy + dz) + y * (dx + dz) + z * (dx + dy));
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number} z
     * @returns {boolean}
     */
    _expandToFitPoint(x, y, z) {
        let expanded = false;
        if (x < this.x0) {
            this.x0 = x;
            expanded = true;
        }
        if (y < this.y0) {
            this.y0 = y;
            expanded = true;
        }
        if (z < this.z0) {
            this.z0 = z;
            expanded = true;
        }
        if (x > this.x1) {
            this.x1 = x;
            expanded = true;
        }
        if (y > this.y1) {
            this.y1 = y;
            expanded = true;
        }
        if (z > this.z1) {
            this.z1 = z;
            expanded = true;
        }
        return expanded;
    }

    /**
     *
     * @param {AABB3} other
     */
    union(other) {
        this._expandToFit(other.x0, other.y0, other.z0, other.x1, other.y1, other.z1);
    }

    /**
     *
     * @param {AABB3} box
     * @returns {boolean}
     * @deprecated use {@link AABB3#union} instead
     */
    expandToFit(box) {
        return this._expandToFit(box.x0, box.y0, box.z0, box.x1, box.y1, box.z1);
    }

    /**
     *
     * @param {number} x0
     * @param {number} y0
     * @param {number} z0
     * @param {number} x1
     * @param {number} y1
     * @param {number} z1
     * @returns {boolean}
     */
    _expandToFit(x0, y0, z0, x1, y1, z1) {
        let expanded = false;
        if (x0 < this.x0) {
            this.x0 = x0;
            expanded = true;
        }
        if (y0 < this.y0) {
            this.y0 = y0;
            expanded = true;
        }
        if (z0 < this.z0) {
            this.z0 = z0;
            expanded = true;
        }
        if (x1 > this.x1) {
            this.x1 = x1;
            expanded = true;
        }
        if (y1 > this.y1) {
            this.y1 = y1;
            expanded = true;
        }
        if (z1 > this.z1) {
            this.z1 = z1;
            expanded = true;
        }
        return expanded;
    }

    /**
     *
     * @param {number} x0
     * @param {number} y0
     * @param {number} z0
     * @param {number} x1
     * @param {number} y1
     * @param {number} z1
     * @returns {boolean}
     */
    _containsBox(x0, y0, z0, x1, y1, z1) {
        return x0 >= this.x0 && y0 >= this.y0 && z0 >= this.z0 && x1 <= this.x1 && y1 <= this.y1 && z1 <= this.z1;
    }

    /**
     *
     * @param {AABB3} box
     * @returns {boolean}
     */
    containsBox(box) {
        return this._containsBox(box.x0, box.y0, box.z0, box.x1, box.y1, box.z1);
    }

    /**
     *
     * @returns {number}
     */
    getExtentsX() {
        return (this.x1 - this.x0);
    }

    get width() {
        return this.getExtentsX();
    }

    /**
     *
     * @returns {number}
     */
    getExtentsY() {
        return (this.y1 - this.y0);
    }

    get height() {
        return this.getExtentsY();
    }

    /**
     *
     * @returns {number}
     */
    getExtentsZ() {
        return (this.z1 - this.z0);
    }

    get depth() {
        return this.getExtentsZ();
    }

    /**
     * half-width in X axis
     * @returns {number}
     */
    getHalfExtentsX() {
        return this.getExtentsX() / 2;
    }

    /**
     * half-width in Y axis
     * @returns {number}
     */
    getHalfExtentsY() {
        return this.getExtentsY() / 2;
    }

    /**
     * half-width in Z axis
     * @returns {number}
     */
    getHalfExtentsZ() {
        return this.getExtentsZ() / 2;
    }

    /**
     *
     * @param {Vector3} [target]
     * @returns {Vector3}
     */
    getExtents(target = new Vector3()) {
        target.set(
            this.width,
            this.height,
            this.depth,
        );

        return target;
    }

    /**
     *
     * @returns {number}
     */
    getCenterX() {
        return (this.x0 + this.x1) * 0.5;
    }

    get centerX() {
        return this.getCenterX();
    }

    /**
     *
     * @returns {number}
     */
    getCenterY() {
        return (this.y0 + this.y1) * 0.5;
    }

    get centerY() {
        return this.getCenterY();
    }

    /**
     *
     * @returns {number}
     */
    getCenterZ() {
        return (this.z0 + this.z1) * 0.5;
    }

    get centerZ() {
        return this.getCenterZ();
    }

    /**
     * Get center position of the box
     * @param {Vector3} [target] where to write result
     */
    getCenter(target = new Vector3()) {
        const x = this.getCenterX();
        const y = this.getCenterY();
        const z = this.getCenterZ();

        target.set(
            x,
            y,
            z
        );

        return target;
    }


    /**
     * Accepts ray description, first set of coordinates is origin (oX,oY,oZ) and second is direction (dX,dY,dZ). Algorithm from GraphicsGems by Andrew Woo
     * @param oX
     * @param oY
     * @param oZ
     * @param dX
     * @param dY
     * @param dZ
     */
    intersectRay(oX, oY, oZ, dX, dY, dZ) {
        return aabb3_intersects_ray(this.x0, this.y0, this.z0, this.x1, this.y1, this.z1, oX, oY, oZ, dX, dY, dZ);
    }

    intersectSegment(startX, startY, startZ, endX, endY, endZ) {
        return aabb3_intersects_line_segment(this.x0, this.y0, this.z0, this.x1, this.y1, this.z1, startX, startY, startZ, endX, endY, endZ);
    }

    /**
     * @deprecated use {@link AABB3#_containsBox} instead
     * @param {THREE.Box} box
     * @returns {boolean}
     */
    threeContainsBox(box) {
        const min = box.min;
        const max = box.max;
        return this._containsBox(min.x, min.y, min.z, max.x, max.y, max.z);
    }

    /**
     *
     * @param {function(x:number, y:number, z:number)} callback
     * @param {*} [thisArg]
     */
    traverseCorners(callback, thisArg) {
        const _x0 = this.x0;
        const _y0 = this.y0;
        const _z0 = this.z0;
        const _x1 = this.x1;
        const _y1 = this.y1;
        const _z1 = this.z1;

        callback.call(thisArg, _x0, _y0, _z0);
        callback.call(thisArg, _x0, _y0, _z1);

        callback.call(thisArg, _x0, _y1, _z0);
        callback.call(thisArg, _x0, _y1, _z1);

        callback.call(thisArg, _x1, _y0, _z0);
        callback.call(thisArg, _x1, _y0, _z1);

        callback.call(thisArg, _x1, _y1, _z0);
        callback.call(thisArg, _x1, _y1, _z1);
    }

    /**
     *
     * @param {number[]|Float64Array|Float32Array} result
     */
    getCorners(result) {
        aabb3_build_corners(
            result,
            0,
            this.x0,
            this.y0,
            this.z0,
            this.x1,
            this.y1,
            this.z1
        );
    }

    /**
     *
     * @param {number[]|Float32Array|Float64Array} result
     * @param {number} offset
     */
    writeToArray(result = [], offset = 0) {
        assert.isNonNegativeInteger(offset, 'offset');

        result[offset] = this.x0;
        result[offset + 1] = this.y0;
        result[offset + 2] = this.z0;

        result[offset + 3] = this.x1;
        result[offset + 4] = this.y1;
        result[offset + 5] = this.z1;

        return result;
    }

    /**
     *
     * @param {number[]|Float32Array|Float64Array} source
     * @param {number} offset
     */
    readFromArray(source, offset = 0) {
        assert.isNonNegativeInteger(offset, 'offset');

        const _x0 = source[offset];
        const _y0 = source[offset + 1];
        const _z0 = source[offset + 2];

        const _x1 = source[offset + 3];
        const _y1 = source[offset + 4];
        const _z1 = source[offset + 5];

        this.setBounds(_x0, _y0, _z0, _x1, _y1, _z1);
    }

    /**
     * @param {THREE.Plane} plane
     * @returns {int} 2,0,or -2; 2: above, -2 : below, 0 : intersects plane
     */
    computePlaneSide(plane) {
        const normal = plane.normal;

        return aabb3_compute_plane_side(
            normal.x, normal.y, normal.z, plane.constant,
            this.x0, this.y0, this.z0,
            this.x1, this.y1, this.z1
        );
    }

    /**
     *
     * @param {number} normal_x
     * @param {number} normal_y
     * @param {number} normal_z
     * @param {number} offset
     * @returns {number}
     */
    computeDistanceAbovePlane(normal_x, normal_y, normal_z, offset) {
        return aabb3_compute_distance_above_plane_max(
            normal_x, normal_y, normal_z, offset,
            this.x0, this.y0, this.z0,
            this.x1, this.y1, this.z1
        );
    }

    /**
     *
     * @param {number} normal_x
     * @param {number} normal_y
     * @param {number} normal_z
     * @param {number} offset
     * @returns {boolean}
     */
    _isBelowPlane(normal_x, normal_y, normal_z, offset) {
        return this.computeDistanceAbovePlane(normal_x, normal_y, normal_z, offset) < 0;
    }

    /**
     *
     * @param {Plane} plane
     * @return {boolean}
     */
    isBelowPlane(plane) {
        const normal = plane.normal;

        return this._isBelowPlane(normal.x, normal.y, normal.z, plane.constant);
    }

    /**
     * @param {Plane[]} clippingPlanes
     * @returns {boolean}
     */
    intersectSpace(clippingPlanes) {
        let i = 0;
        const l = clippingPlanes.length;
        for (; i < l; i++) {

            const plane = clippingPlanes[i];
            if (this.isBelowPlane(plane)) {
                return false;
            }
        }
        return true;
    }

    /**
     *
     * @param {Frustum} frustum
     * @returns {number}
     */
    intersectFrustumDegree(frustum) {
        const planes = frustum.planes;

        let i = 0;
        let result = 2;
        for (; i < 6; i++) {

            const plane = planes[i];

            const planeSide = this.computePlaneSide(plane);

            if (planeSide < 0) {
                return 0;
            } else if (planeSide === 0) {
                result = 1;
            }
        }
        return result;
    }

    /**
     *
     * @param {number[]} frustum
     * @returns {number}
     */
    intersectFrustumDegree_array(frustum) {
        return aabb3_intersects_frustum_degree(
            this.x0, this.y0, this.z0, this.x1, this.y1, this.z1, frustum
        );
    }

    /**
     *
     * @param {{planes:Array}}frustum
     * @returns {boolean}
     */
    intersectFrustum(frustum) {
        const planes = frustum.planes;
        for (let i = 0; i < 6; i++) {

            const plane = planes[i];
            if (this.isBelowPlane(plane)) {
                return false;
            }
        }
        return true;
    }

    /**
     *
     * @param {ArrayLike<number>|number[]|Float32Array}frustum
     * @returns {boolean}
     */
    intersectFrustum_array(frustum) {
        const x0 = this.x0;
        const y0 = this.y0;
        const z0 = this.z0;

        const x1 = this.x1;
        const y1 = this.y1;
        const z1 = this.z1;

        return aabb3_intersects_clipping_volume_array(x0, y0, z0, x1, y1, z1, frustum, 0, 6);
    }

    /**
     *
     * @param {number[]|ArrayLike<number>|Float32Array} matrix
     */
    applyMatrix4(matrix) {
        const a = [];
        const b = [];

        this.writeToArray(a, 0);

        aabb3_matrix4_project(b, a, matrix);

        this.readFromArray(b, 0);
    }

    /**
     * Expands the box in all directions by the given amount
     * @param {number} extra
     */
    grow(extra) {
        assert.isNumber(extra, 'extra');
        assert.notNaN(extra, 'extra');
        assert.isFinite(extra, 'extra');

        this.x0 -= extra;
        this.y0 -= extra;
        this.z0 -= extra;

        this.x1 += extra;
        this.y1 += extra;
        this.z1 += extra;
    }

    /**
     *
     * @returns {AABB3}
     */
    clone() {
        const clone = new AABB3();

        clone.copy(this);

        return clone;
    }

    fromJSON({ x0, y0, z0, x1, y1, z1 }) {
        this.setBounds(x0, y0, z0, x1, y1, z1);
    }

    toJSON() {
        return {
            x0: this.x0,
            y0: this.y0,
            z0: this.z0,
            x1: this.x1,
            y1: this.y1,
            z1: this.z1
        };
    }

    toString() {
        return `AABB3{ min:[${this.x0},${this.y0},${this.z0}], max:[${this.x1},${this.y1},${this.z1}] }`
    }
}


/**
 * @readonly
 * @type {boolean}
 */
AABB3.prototype.isAABB3 = true;

AABB3.prototype.toArray = AABB3.prototype.writeToArray;
AABB3.prototype.fromArray = AABB3.prototype.readFromArray;

/**
 * Pretending to be an array
 * @readonly
 * @type {number}
 */
AABB3.prototype.length = 6
