import { assert } from "../../assert.js";
import { UNIT_DIMENSION_COUNT } from "./UnitDimension.js";

/**
 * Represents a physical unit as a vector of exponents over the 7 SI base dimensions.
 *
 * Composition rules:
 *  - multiplying two units adds their exponents component-wise (e.g. m * s = m·s, m * m = m^2)
 *  - dividing two units subtracts their exponents (e.g. m / s = m/s, m / m = dimensionless)
 *  - raising a unit to a power scales every exponent by that power (e.g. m^3 = volume)
 *
 * Storage uses a fixed-size {@link Float64Array} so fractional exponents (e.g. sqrt of a unit)
 * are representable, and so that composition can be performed without per-call allocation.
 */
export class UnitMatrix {
    constructor() {
        /**
         * Exponent for each SI base dimension, indexed by {@link UnitDimension}.
         * @type {Float64Array}
         */
        this.exponents = new Float64Array(UNIT_DIMENSION_COUNT);
    }

    /**
     * Set the exponent of a single base dimension.
     * @param {number} dimension Index from {@link UnitDimension}.
     * @param {number} value
     */
    set_exponent(dimension, value) {
        assert.isNonNegativeInteger(dimension, 'dimension');
        assert.lessThan(dimension, UNIT_DIMENSION_COUNT, 'dimension overflow');
        assert.isNumber(value, 'value');

        this.exponents[dimension] = value;
    }

    /**
     * Read the exponent of a single base dimension.
     * @param {number} dimension Index from {@link UnitDimension}.
     * @returns {number}
     */
    get_exponent(dimension) {
        assert.isNonNegativeInteger(dimension, 'dimension');
        assert.lessThan(dimension, UNIT_DIMENSION_COUNT, 'dimension overflow');

        return this.exponents[dimension];
    }

    /**
     * Bulk-set every dimension exponent in a single call.
     * @param {number} length
     * @param {number} mass
     * @param {number} time
     * @param {number} electric_current
     * @param {number} temperature
     * @param {number} amount_of_substance
     * @param {number} luminous_intensity
     * @returns {this}
     */
    set(length, mass, time, electric_current, temperature, amount_of_substance, luminous_intensity) {
        const e = this.exponents;

        e[0] = length;
        e[1] = mass;
        e[2] = time;
        e[3] = electric_current;
        e[4] = temperature;
        e[5] = amount_of_substance;
        e[6] = luminous_intensity;

        return this;
    }

    /**
     * Reset to dimensionless (every exponent is 0).
     */
    set_zero() {
        this.exponents.fill(0);
    }

    /**
     * Copy values from another unit matrix.
     * @param {UnitMatrix} other
     */
    copy(other) {
        this.exponents.set(other.exponents);
    }

    /**
     * Allocate a new unit matrix with the same exponents.
     * @returns {UnitMatrix}
     */
    clone() {
        const r = new UnitMatrix();

        r.copy(this);

        return r;
    }

    /**
     * Strict structural equality between two unit matrices.
     * Two matrices are equal when every exponent matches exactly.
     * @param {UnitMatrix} other
     * @returns {boolean}
     */
    equals(other) {
        const a = this.exponents;
        const b = other.exponents;

        for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
            if (a[i] !== b[i]) {
                return false;
            }
        }

        return true;
    }

    /**
     * Whether this matrix represents a dimensionless quantity (all exponents are 0).
     * @returns {boolean}
     */
    is_dimensionless() {
        const e = this.exponents;

        for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
            if (e[i] !== 0) {
                return false;
            }
        }

        return true;
    }

    /**
     * Multiply this matrix by another in-place (this *= other).
     * Adds exponents component-wise.
     * @param {UnitMatrix} other
     */
    multiply(other) {
        const e = this.exponents;
        const o = other.exponents;

        for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
            e[i] += o[i];
        }
    }

    /**
     * Divide this matrix by another in-place (this /= other).
     * Subtracts exponents component-wise.
     * @param {UnitMatrix} other
     */
    divide(other) {
        const e = this.exponents;
        const o = other.exponents;

        for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
            e[i] -= o[i];
        }
    }

    /**
     * Store the product a * b into this matrix (this = a * b).
     * Safe to call with this === a or this === b.
     * @param {UnitMatrix} a
     * @param {UnitMatrix} b
     */
    multiply_matrices(a, b) {
        const out = this.exponents;
        const ax = a.exponents;
        const bx = b.exponents;

        for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
            out[i] = ax[i] + bx[i];
        }
    }

    /**
     * Store the quotient a / b into this matrix (this = a / b).
     * Safe to call with this === a or this === b.
     * @param {UnitMatrix} a
     * @param {UnitMatrix} b
     */
    divide_matrices(a, b) {
        const out = this.exponents;
        const ax = a.exponents;
        const bx = b.exponents;

        for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
            out[i] = ax[i] - bx[i];
        }
    }

    /**
     * Raise this matrix to a power in-place (this **= n).
     * @param {number} n
     */
    power(n) {
        assert.isNumber(n, 'n');

        const e = this.exponents;

        for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
            e[i] *= n;
        }
    }

    /**
     * Store a ** n into this matrix (this = a ** n).
     * Safe to call with this === a.
     * @param {UnitMatrix} a
     * @param {number} n
     */
    power_of_matrix(a, n) {
        assert.isNumber(n, 'n');

        const out = this.exponents;
        const ax = a.exponents;

        for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
            out[i] = ax[i] * n;
        }
    }

    /**
     * Invert in-place: every exponent is negated, so this becomes 1 / this.
     */
    invert() {
        const e = this.exponents;

        for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
            e[i] = -e[i];
        }
    }

    /**
     * Store 1 / a into this matrix.
     * Safe to call with this === a.
     * @param {UnitMatrix} a
     */
    invert_matrix(a) {
        const out = this.exponents;
        const ax = a.exponents;

        for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
            out[i] = -ax[i];
        }
    }
}
