import { UnitDimension, UNIT_DIMENSION_COUNT } from "./UnitDimension.js";
import { UNIT_DIMENSION_MAPPING } from "./UNIT_DIMENSION_MAPPING.js";

/**
 * Order in which dimensions are visited when assembling a base-unit string. Chosen to match the
 * conventional way SI compound units are written (mass appears before length in "kg·m/s^2",
 * "kg·m^2/s^2", etc.).
 *
 * @type {number[]}
 */
const RENDER_ORDER = [
    UnitDimension.Mass,
    UnitDimension.Length,
    UnitDimension.Time,
    UnitDimension.ElectricCurrent,
    UnitDimension.Temperature,
    UnitDimension.AmountOfSubstance,
    UnitDimension.LuminousIntensity
];

/**
 * Format an exponent as either an empty suffix (for ^1) or a `^n` suffix.
 * @param {number} exponent
 * @returns {string}
 */
function format_exponent_suffix(exponent) {
    if (exponent === 1) {
        return '';
    }

    return `^${exponent}`;
}

/**
 * Append a `symbol[^k]` term to either the numerator or denominator buffer.
 *
 * @param {string} numerator
 * @param {string} denominator
 * @param {string} symbol
 * @param {number} power Non-zero power.
 * @returns {[string, string]} New (numerator, denominator) pair.
 */
function append_term(numerator, denominator, symbol, power) {
    if (power > 0) {
        const term = symbol + format_exponent_suffix(power);
        return [numerator === '' ? term : numerator + '·' + term, denominator];
    }

    const term = symbol + format_exponent_suffix(-power);
    return [numerator, denominator === '' ? term : denominator + '·' + term];
}

/**
 * Count how many components of an exponent vector are non-zero — used as the "term cardinality"
 * objective during decomposition.
 *
 * @param {Float64Array} exponents
 * @returns {number}
 */
function count_nonzero(exponents) {
    let count = 0;

    for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
        if (exponents[i] !== 0) {
            count++;
        }
    }

    return count;
}

/**
 * Count how many components of `(remaining - k * unit)` are non-zero. Hot path during
 * decomposition; written without allocating a temporary vector.
 *
 * @param {Float64Array} remaining
 * @param {Float64Array} unit
 * @param {number} k
 * @returns {number}
 */
function count_nonzero_after_subtract(remaining, unit, k) {
    let count = 0;

    for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
        if (remaining[i] - k * unit[i] !== 0) {
            count++;
        }
    }

    return count;
}

/**
 * Subtract `k * unit` from `remaining` in place.
 *
 * @param {Float64Array} remaining
 * @param {Float64Array} unit
 * @param {number} k
 */
function subtract_scaled(remaining, unit, k) {
    for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
        remaining[i] -= k * unit[i];
    }
}

/**
 * Maximum |k| considered when looking for the integer power of a named unit that best reduces
 * the remaining matrix. Chosen large enough to handle squares / cubes of common units while
 * keeping the inner loop bounded.
 *
 * @type {number}
 */
const MAX_POWER_SEARCH = 4;

/**
 * Whether the given exponent vector has 2 or more non-zero components — i.e. whether the
 * named unit it belongs to expresses a genuine compound dimension (Newton, Volt, ...) rather
 * than just a renaming of a single base dimension.
 *
 * @param {Float64Array} exponents
 * @returns {boolean}
 */
function is_compound(exponents) {
    let count = 0;

    for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
        if (exponents[i] !== 0) {
            count++;

            if (count >= 2) {
                return true;
            }
        }
    }

    return false;
}

/**
 * Compute `k` such that every component of `matrix` equals `k * named_unit`. Returns `null`
 * when no such scalar exists, or when `named_unit` is itself dimensionless.
 *
 * @param {Float64Array} matrix
 * @param {Float64Array} named_unit
 * @returns {number|null}
 */
function compute_scalar_power(matrix, named_unit) {
    let k = null;

    for (let i = 0; i < UNIT_DIMENSION_COUNT; i++) {
        const ni = named_unit[i];
        const mi = matrix[i];

        if (ni === 0) {
            if (mi !== 0) {
                return null;
            }
            continue;
        }

        const candidate = mi / ni;

        if (k === null) {
            k = candidate;
        } else if (k !== candidate) {
            return null;
        }
    }

    return k;
}

/**
 * Look for a registry entry that is a non-zero **integer** scalar multiple of `matrix` — i.e.
 * an exact match (including positive/negative integer powers like `V`, `V^2`, `1/V`). Fractional
 * scalars are rejected; that prevents nonsensical renders like `Gy^0.5` for a speed matrix.
 *
 * Returns the registry index and matching power, or `index === -1` when no integer scalar
 * match exists.
 *
 * @param {Float64Array} matrix
 * @param {NamedUnit[]} named_units
 * @returns {{ index: number, power: number }}
 */
function find_scalar_match(matrix, named_units) {
    for (let i = 0; i < named_units.length; i++) {
        const power = compute_scalar_power(matrix, named_units[i].unit.exponents);

        if (power !== null && power !== 0 && Number.isInteger(power)) {
            return { index: i, power };
        }
    }

    return { index: -1, power: 0 };
}

/**
 * Pick the (named-unit-index, power) pair that minimises the non-zero count of
 * `remaining - power * named_unit.unit`, considering only **compound** registry entries
 * (those with at least two non-zero exponents).
 *
 * Single-dimension entries (e.g. METER, SECOND) are skipped: they would just substitute their
 * symbol for the corresponding base symbol while reordering terms by registry order rather
 * than the canonical RENDER_ORDER. The base-residual pass handles those cleanly.
 *
 * Ties are broken by registry order (first match wins).
 *
 * @param {Float64Array} remaining
 * @param {NamedUnit[]} named_units
 * @param {number} baseline Current non-zero count of `remaining`.
 * @returns {{ index: number, power: number, nonzero: number }}
 */
function find_best_compound_unit(remaining, named_units, baseline) {
    let best_index = -1;
    let best_power = 0;
    let best_nonzero = baseline;

    for (let i = 0; i < named_units.length; i++) {
        const unit_exp = named_units[i].unit.exponents;

        if (!is_compound(unit_exp)) {
            continue;
        }

        for (let k = -MAX_POWER_SEARCH; k <= MAX_POWER_SEARCH; k++) {
            if (k === 0) {
                continue;
            }

            const after = count_nonzero_after_subtract(remaining, unit_exp, k);

            if (after < best_nonzero) {
                best_nonzero = after;
                best_index = i;
                best_power = k;
            }
        }
    }

    return { index: best_index, power: best_power, nonzero: best_nonzero };
}

/**
 * Greedily decompose `unit_matrix` against the **compound** entries of the registry, appending
 * recognised named-unit terms into the numerator/denominator buffers. The leftover residual
 * exponents are returned for later rendering as base-dimension terms.
 *
 * Each iteration picks the (named unit, integer power) pair that strictly reduces the term
 * count: `total terms = #picked + nonzero(residual)`. The algorithm stops once no further
 * pick yields a strict improvement.
 *
 * Note: this function does NOT handle the exact-match case (`matrix == k * named_unit`) — that
 * is detected separately upstream and short-circuits to a single-term render so callers can
 * benefit from named single-dim units like Hz.
 *
 * @param {UnitMatrix} unit_matrix
 * @param {NamedUnit[]} named_units
 * @returns {{ numerator: string, denominator: string, residual: Float64Array }}
 */
function decompose_compound(unit_matrix, named_units) {
    const residual = new Float64Array(UNIT_DIMENSION_COUNT);
    residual.set(unit_matrix.exponents);

    let numerator = '';
    let denominator = '';

    while (true) {
        const baseline = count_nonzero(residual);

        if (baseline === 0) {
            break;
        }

        const best = find_best_compound_unit(residual, named_units, baseline);

        // Strict improvement: total terms decrease only when the new non-zero count is at
        // least 2 below the baseline (a 1-step reduction is offset by adding a named term,
        // for net zero change). This avoids cosmetic substitutions like `m/s -> Hz·m`.
        if (best.index === -1 || best.nonzero > baseline - 2) {
            break;
        }

        const named = named_units[best.index];
        subtract_scaled(residual, named.unit.exponents, best.power);

        const result = append_term(numerator, denominator, named.symbol, best.power);
        numerator = result[0];
        denominator = result[1];
    }

    return { numerator, denominator, residual };
}

/**
 * Append every non-zero residual base-dimension exponent to the numerator/denominator buffers,
 * using `dimension_units[i].symbol` for each dimension `i`.
 *
 * @param {string} numerator
 * @param {string} denominator
 * @param {Float64Array} residual
 * @param {NamedUnit[]} dimension_units One entry per base dimension, in {@link UnitDimension} order.
 * @returns {[string, string]}
 */
function append_base_residual(numerator, denominator, residual, dimension_units) {
    let n = numerator;
    let d = denominator;

    for (let i = 0; i < RENDER_ORDER.length; i++) {
        const dim = RENDER_ORDER[i];
        const e = residual[dim];

        if (e === 0) {
            continue;
        }

        const result = append_term(n, d, dimension_units[dim].symbol, e);
        n = result[0];
        d = result[1];
    }

    return [n, d];
}

/**
 * Convert a {@link UnitMatrix} into a human-readable string such as `m/s`, `kg·m/s^2` or `Hz`.
 *
 * Output rules:
 *  - dimensions with positive exponent appear in the numerator, joined by `·`
 *  - dimensions with negative exponent appear in the denominator, joined by `·`
 *  - exponents of magnitude 1 are written without a suffix; otherwise `^n` is appended
 *  - dimensionless matrices return an empty string
 *  - if every term lives in the denominator, the numerator is rendered as `1`
 *
 * Optional named-unit registry (`named_units` argument):
 *  - if the matrix is a non-zero scalar multiple of any registry entry, that entry is used
 *    directly: `Hz` for `1/s`, `V` for the volt matrix, `V^2` for its square, `1/V` for its
 *    inverse.
 *  - otherwise the matrix is greedily decomposed against compound registry entries (named
 *    units with at least two non-zero exponents). The algorithm tries to use the fewest terms
 *    possible — `V/m^2` is rendered as `V/m^2` (two terms) rather than `kg/(s^3·A)` (four
 *    terms). Single-dimension entries (`METER`, `SECOND`, ...) are skipped here so the
 *    leftover base residual renders in canonical `Mass·Length·Time·...` order.
 *  - registry order acts as a tiebreaker — the first match wins among options with the same
 *    term-count reduction.
 *
 * @param {UnitMatrix} unit_matrix
 * @param {NamedUnit[]} [named_units] Optional registry of compound-unit symbols.
 * @param {NamedUnit[]} [dimension_units=UNIT_DIMENSION_MAPPING] One {@link NamedUnit} per base dimension, indexed by {@link UnitDimension}. Only the `.symbol` field is read.
 * @returns {string}
 */
export function unit_matrix_to_string(
    unit_matrix,
    named_units = undefined,
    dimension_units = UNIT_DIMENSION_MAPPING
) {
    if (unit_matrix.is_dimensionless()) {
        return '';
    }

    let numerator = '';
    let denominator = '';
    let residual;

    if (named_units !== undefined && named_units.length > 0) {
        // Phase 1: exact scalar match short-circuits to a single named term.
        const scalar = find_scalar_match(unit_matrix.exponents, named_units);

        if (scalar.index !== -1) {
            const named = named_units[scalar.index];
            const result = append_term('', '', named.symbol, scalar.power);

            if (result[1] === '') {
                return result[0];
            }

            return '1/' + result[1];
        }

        // Phase 2: greedy decomposition against compound entries; leftover residual renders
        // via the base-residual pass below.
        const decomposed = decompose_compound(unit_matrix, named_units);

        numerator = decomposed.numerator;
        denominator = decomposed.denominator;
        residual = decomposed.residual;
    } else {
        residual = unit_matrix.exponents;
    }

    const result = append_base_residual(numerator, denominator, residual, dimension_units);
    numerator = result[0];
    denominator = result[1];

    if (denominator === '') {
        return numerator;
    }

    if (numerator === '') {
        return '1/' + denominator;
    }

    return numerator + '/' + denominator;
}
