import { assert } from "../../../assert.js";
import { clamp } from "../../../math/clamp.js";
import { max2 } from "../../../math/max2.js";
import { min2 } from "../../../math/min2.js";
import Vector2 from "../../Vector2.js";
import {
    line_segment_compute_line_segment_intersection_2d
} from "../line/line_segment_compute_line_segment_intersection_2d.js";
import { aabb2_compute_area } from "./aabb2_compute_area.js";
import { aabb2_compute_overlap } from "./aabb2_compute_overlap.js";
import { aabb2_overlap_exists } from "./aabb2_overlap_exists.js";

/**
 *
 * Axis-Aligned Bounding Box in 2D
 */
class AABB2 {
    /**
     * @param {number} [x0=0]
     * @param {number} [y0=0]
     * @param {number} [x1=0]
     * @param {number} [y1=0]
     */
    constructor(
        x0 = 0,
        y0 = 0,
        x1 = 0,
        y1 = 0
    ) {
        assert.isNumber(x0, `x0`);
        assert.isNumber(y0, `y0`);
        assert.isNumber(x1, `x1`);
        assert.isNumber(y1, `y1`);

        assert.notNaN(x0, `x0`);
        assert.notNaN(y0, `y0`);
        assert.notNaN(x1, `x1`);
        assert.notNaN(y1, `y1`);

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

    * [Symbol.iterator]() {

        yield this.x0;
        yield this.y0;

        yield this.x1;
        yield this.y1;

    }

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

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

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

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


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

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

    /**
     * @param {number} value
     */
    set 2(value) {
        this.x1 = value;
    }

    /**
     * @param {number} value
     */
    set 3(value) {
        this.y1 = value;
    }

    /**
     *
     * @param {number} size
     */
    growWidth(size) {
        assert.isNumber(size, 'size');
        assert.notNaN(size, 'size');

        this.x0 -= size;
        this.x1 += size;
    }

    /**
     *
     * @param {number} size
     */
    growHeight(size) {
        assert.isNumber(size, 'size');
        assert.notNaN(size, 'size');

        this.y0 -= size;
        this.y1 += size;
    }

    /**
     *  Expands box in every direction by a given amount
     * @param {number} size
     */
    grow(size) {

        this.growWidth(size);
        this.growHeight(size);
    }

    /**
     * Shrinks the box in every direction by a given amount
     * @param {number} size
     */
    shrink(size) {
        this.grow(-size);
    }

    /**
     *
     * @param {number[]} m
     */
    applyMatrix3(m) {

        const m0 = m[0];
        const m1 = m[1];
        const m3 = m[3];
        const m4 = m[4];
        const m6 = m[6];
        const m7 = m[7];

        const x0 = this.x0;
        const y0 = this.y0;
        const x1 = this.x1;
        const y1 = this.y1;


        const _xa = m0 * x0 + m3 * y0 + m6;
        const _ya = m1 * x0 + m4 * y0 + m7;

        const _xb = m0 * x1 + m3 * y1 + m6;
        const _yb = m1 * x1 + m4 * y1 + m7;

        let _x0 = min2(_xa, _xb);
        let _x1 = max2(_xa, _xb);

        let _y0 = min2(_ya, _yb);
        let _y1 = max2(_ya, _yb);

        this.set(_x0, _y0, _x1, _y1);
    }

    /**
     *
     * @param {number} value
     */
    multiplyScalar(value) {
        this.set(
            this.x0 * value,
            this.y0 * value,
            this.x1 * value,
            this.y1 * value
        );
    }

    /**
     *
     * @param {AABB2} other
     * @param {AABB2} result Overlapping region will be written here
     * @returns {boolean} true if there is overlap, result is also written. false otherwise
     */
    computeOverlap(other, result) {
        const ax0 = this.x0;
        const ay0 = this.y0;
        const ax1 = this.x1;
        const ay1 = this.y1;

        const bx0 = other.x0;
        const by0 = other.y0;
        const bx1 = other.x1;
        const by1 = other.y1;

        return aabb2_compute_overlap(ax0, ay0, ax1, ay1, bx0, by0, bx1, by1, result);
    }

    /**
     *
     * @param {AABB2} other
     * @returns {boolean}
     */
    overlapExists(other) {
        const ax0 = this.x0;
        const ay0 = this.y0;
        const ax1 = this.x1;
        const ay1 = this.y1;

        const bx0 = other.x0;
        const by0 = other.y0;
        const bx1 = other.x1;
        const by1 = other.y1;

        return aabb2_overlap_exists(ax0, ay0, ax1, ay1, bx0, by0, bx1, by1);
    }

    /**
     *
     * @param {Number} x0
     * @param {Number} y0
     * @param {Number} x1
     * @param {Number} y1
     * @public
     */
    _expandToFit(x0, y0, x1, y1) {
        this.x0 = min2(this.x0, x0);
        this.y0 = min2(this.y0, y0);

        this.x1 = max2(this.x1, x1);
        this.y1 = max2(this.y1, y1);
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     */
    _expandToFitPoint(x, y) {
        // shortcut, use a 0-size box
        this._expandToFit(x, y, x, y);
    }

    /**
     * NOTE: only 1 intersection point is produced
     * @param {Vector2} p0
     * @param {Vector2} p1
     * @param {Vector2} result
     * @returns {boolean}
     */
    lineIntersectionPoint(p0, p1, result) {
        const x0 = this.x0;
        const y0 = this.y0;
        const x1 = this.x1;
        const y1 = this.y1;

        if (line_segment_compute_line_segment_intersection_2d(p0.x, p0.y, p1.x, p1.y, x0, y0, x1, y0, result)) {
            //top
            return true;
        }
        if (line_segment_compute_line_segment_intersection_2d(p0.x, p0.y, p1.x, p1.y, x0, y1, x1, y1, result)) {
            //bottom
            return true;
        }
        if (line_segment_compute_line_segment_intersection_2d(p0.x, p0.y, p1.x, p1.y, x0, y0, x0, y1, result)) {
            //left
            return true;
        }
        if (line_segment_compute_line_segment_intersection_2d(p0.x, p0.y, p1.x, p1.y, x1, y0, x1, y1, result)) {
            //right
            return true;
        }

        return false;
    }

    /**
     *
     * @param {Vector2} point
     * @param {Vector2} result
     */
    computeNearestPointToPoint(point, result) {
        let x, y;


        const x0 = this.x0;
        const y0 = this.y0;
        const x1 = this.x1;
        const y1 = this.y1;

        const pX = point.x;
        const pY = point.y;


        x = clamp(pX, x0, x1);
        y = clamp(pY, y0, y1);

        result.set(x, y);
    }

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

    /**
     *
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     * @returns {number}
     */
    _costForInclusion(x0, y0, x1, y1) {
        let x = 0;
        let y = 0;
        //
        const _x0 = this.x0;
        const _y0 = this.y0;
        const _x1 = this.x1;
        const _y1 = this.y1;
        //
        if (_x0 > x0) {
            x += _x0 - x0;
        }
        if (_x1 < x1) {
            x += x1 - _x1;
        }
        if (_y0 > y0) {
            y += _y0 - y0;
        }
        if (_y1 < y1) {
            y += y1 - _y1;
        }

        const dx = _x1 - _x0;
        const dy = _y1 - _y0;

        return x * dy + y * dx;
    }

    /**
     *
     * @returns {number}
     */
    computeArea() {
        const x0 = this.x0;
        const y0 = this.y0;
        const x1 = this.x1;
        const y1 = this.y1;

        return aabb2_compute_area(x0, y0, x1, y1);
    }

    /**
     *
     * @return {number}
     */
    computeSurfaceArea() {
        const x0 = this.x0;
        const y0 = this.y0;
        const x1 = this.x1;
        const y1 = this.y1;

        const dx = x1 - x0;
        const dy = y1 - y0;

        return 2 * (dx + dy);
    }

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

    /**
     *
     * @param {AABB2} other
     */
    expandToFit(other) {
        this._expandToFit(other.x0, other.y0, other.x1, other.y1);
    }

    /**
     *
     * @param {Vector2} [result]
     */
    getCenter(result = new Vector2()) {

        result.set(this.centerX, this.centerY);

        return result;
    }

    /**
     * retrieve midpoint of AABB along X axis
     * @deprecated use {@link centerX} instead
     * @returns {number}
     */
    midX() {
        throw new Error('deprecated, use .centerX instead');
    }

    /**
     * retrieve midpoint of AABB along Y axis
     * @deprecated use {@link centerY} instead
     * @returns {number}
     */
    midY() {
        throw new Error('deprecated, use .centerY instead');
    }

    /**
     * midpoint along X axis
     * @return {number}
     */
    get centerX() {
        return (this.x0 + this.x1) * 0.5;
    }

    /**
     * midpoint along Y axis
     * @return {number}
     */
    get centerY() {
        return (this.y0 + this.y1) * 0.5
    }

    /**
     * @deprecated prefer to use {@link width} property instead
     * @returns {number}
     */
    getWidth() {
        return this.width;
    }

    /**
     *
     * @return {number}
     */
    get width() {
        return this.x1 - this.x0;
    }

    /**
     *
     * @deprecated prefer to use {@link height} property instead
     * @returns {number}
     */
    getHeight() {
        return this.height;
    }

    /**
     *
     * @return {number}
     */
    get height() {
        return this.y1 - this.y0;
    }

    /**
     *
     * @param {Number} x0
     * @param {Number} y0
     * @param {Number} x1
     * @param {Number} y1
     * @returns {this}
     *
     * @see setBoundsUnordered
     */
    set(x0, y0, x1, y1) {
        assert.isNumber(x0, `x0`);
        assert.isNumber(y0, `y0`);
        assert.isNumber(x1, `x1`);
        assert.isNumber(y1, `y1`);

        assert.notNaN(x0, `x0`);
        assert.notNaN(y0, `y0`);
        assert.notNaN(x1, `x1`);
        assert.notNaN(y1, `y1`);

        this.x0 = x0;
        this.y0 = y0;
        this.x1 = x1;
        this.y1 = y1;

        return this;
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     */
    setPosition(x, y) {
        const width = this.width;
        const height = this.height;

        this.set(x, y, x + width, y + height);
    }

    /**
     * Relative displacement of the AABB by given vector described by {@link deltaX} and {@link deltaY}
     * @param {number} deltaX displacement in X
     * @param {number} deltaY displacement in Y
     * @returns {this}
     */
    move(deltaX, deltaY) {
        return this.set(
            this.x0 + deltaX, this.y0 + deltaY,
            this.x1 + deltaX, this.y1 + deltaY
        );
    }

    /**
     *
     * @returns {AABB2}
     */
    clone() {
        return new AABB2(this.x0, this.y0, this.x1, this.y1);
    }

    /**
     *
     * @param {AABB2} other
     * @returns {AABB2} this
     */
    copy(other) {
        return this.set(other.x0, other.y0, other.x1, other.y1);
    }

    /**
     *
     * @param {AABB2} other
     * @returns {boolean}
     */
    equals(other) {
        return this.x0 === other.x0
            && this.y0 === other.y0
            && this.x1 === other.x1
            && this.y1 === other.y1;
    }

    /**
     *
     * @return {number}
     */
    hash() {
        let h = 0; // Initial hash value

        // Prime numbers for mixing. These are often chosen to be large and coprime.
        const p1 = 73856093;
        const p2 = 19349663;
        const p3 = 83492791;
        const p4 = 536870912; // This is 2^29, can also use another large prime

        // Mix in x0
        h = (h ^ (this.x0 * p1)) >>> 0; // XOR and unsigned right shift
        h = (h << 13 | h >>> 19) >>> 0; // Rotate left (or use a combination of shifts)

        // Mix in y0
        h = (h ^ (this.y0 * p2)) >>> 0;
        h = (h << 17 | h >>> 15) >>> 0;

        // Mix in x1
        h = (h ^ (this.x1 * p3)) >>> 0;
        h = (h << 23 | h >>> 9) >>> 0;

        // Mix in y1
        h = (h ^ (this.y1 * p4)) >>> 0; // Using p4 here
        h = (h << 29 | h >>> 3) >>> 0;

        return h;
    }

    /**
     * Clamps AABB to specified region
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     */
    clamp(x0, y0, x1, y1) {
        this.x0 = clamp(this.x0, x0, x1);
        this.y0 = clamp(this.y0, y0, y1);

        this.x1 = clamp(this.x1, x0, x1);
        this.y1 = clamp(this.y1, y0, y1);
    }

    /**
     * Set bounds without having to maintain constraint that x0 >= x1 and y0 >= y1
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     */
    setBoundsUnordered(x0, y0, x1, y1) {
        let _x0, _y0, _x1, _y1;

        if (x0 < x1) {
            _x0 = x0;
            _x1 = x1;
        } else {
            _x0 = x1;
            _x1 = x0;
        }

        if (y0 < y1) {
            _y0 = y0;
            _y1 = y1;
        } else {
            _y0 = y1;
            _y1 = y0;
        }

        this.set(_x0, _y0, _x1, _y1);
    }

    /**
     * @returns {this}
     */
    setNegativelyInfiniteBounds() {
        return this.set(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
    }

    toString() {
        return `AABB2{x0:${this.x0}, y0:${this.y0}, x1:${this.x1}, y1:${this.y1}}`;
    }

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

    fromJSON(json) {
        this.set(json.x0, json.y0, json.x1, json.y1);
    }

    /**
     * @param {number[]|Float32Array} target
     * @param {number} offset
     * @returns {number[]|Float32Array}
     */
    toArray(target = [], offset = 0) {

        target[offset] = this.x0;
        target[offset + 1] = this.y0;
        target[offset + 2] = this.x1;
        target[offset + 3] = this.y1;

        return target;
    }
}


/**
 *
 * @param {AABB2} b0
 * @param {AABB2} b1
 * @param {Vector2} p0 resulting line segment point
 * @param {Vector2} p1 resulting line segment point
 * @returns {boolean} false if no intersection line found
 */
function computeLineBetweenTwoBoxes(b0, b1, p0, p1) {
    //compute box centers
    const c0 = new Vector2(b0.centerX, b0.centerY);
    const c1 = new Vector2(b1.centerX, b1.centerY);

    const i0 = b0.lineIntersectionPoint(c0, c1, p0);

    if (!i0) {
        // console.error("No intersection point: ", b0, c0, c1);
        return false;
    }

    const i1 = b1.lineIntersectionPoint(c0, c1, p1);

    if (!i1) {
        // console.error("No intersection point: ", b1, c0, c1);
        return false;
    }

    return true;
}

/**
 * @readonly
 * @type {boolean}
 */
AABB2.prototype.isAABB2 = true;

/**
 * Alias
 */
AABB2.computeLineBetweenTwoBoxes = computeLineBetweenTwoBoxes;

/**
 * Useful singleton with bounds 0,0,0,0.
 * @readonly
 * @type {AABB2}
 * @example
 *  const box = new AABB();
 *  // ...
 *  box.copy(AABB.zero); // zero out
 */
AABB2.zero = Object.freeze(new AABB2(0, 0, 0, 0));

/**
 * @readonly
 * @type {AABB2}
 */
AABB2.unit = Object.freeze(new AABB2(0, 0, 1, 1));

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

export default AABB2;
