import { assert } from "../../assert.js";
import { is_typed_array_equals } from "../../collection/array/typed/is_typed_array_equals.js";
import { uint32_array_hash } from "../../collection/array/typed/uint32_array_hash.js";

/**
 * Compressed Sparse Row representation of a weighted graph.
 *
 * The four parallel typed arrays — `edge_addresses`, `adjacency`,
 * `edge_weights`, `vertex_weights` — share a single `ArrayBuffer` so that
 * they are co-located in memory. This gives the allocator one allocation
 * instead of four, the GC one reference to track, and the cache one
 * contiguous region to prefetch when walking adjacency rows.
 *
 * Layout inside the buffer (all uint32, 4 bytes each):
 *   [0]                       edge_addresses, length vertex_count + 1
 *   [vertex_count + 1]        adjacency,      length adjacency_length
 *   [+adjacency_length]       edge_weights,   length adjacency_length
 *   [+adjacency_length]       vertex_weights, length vertex_count
 *
 * Conventions match `mesh_to_metis_graph`:
 *   - `edge_addresses[v]`     = start of vertex v's adjacency in `adjacency`
 *   - `edge_addresses[v + 1]` = end (exclusive)
 *   - `adjacency[k]`          = neighbour vertex id
 *   - `edge_weights[k]`       = weight of that edge
 *   - `vertex_weights[v]`     = vertex weight (default 1)
 *
 * The class deliberately exposes the raw typed arrays as fields rather than
 * via accessor methods. Hot loops in graph algorithms want
 * `for (let k = xadj[v]; k < xadj[v+1]; k++) { ... adj[k] ... }` patterns; a
 * callback-based iterator API would force a closure allocation per outer
 * iteration and defeat V8's inlining.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class CSRGraph {
    /**
     * @param {number} vertex_count
     * @param {number} adjacency_length total length of the adjacency list
     *   (each undirected edge appears twice for symmetric graphs)
     */
    constructor(vertex_count, adjacency_length) {
        assert.isNonNegativeInteger(vertex_count, 'vertex_count');
        assert.isNonNegativeInteger(adjacency_length, 'adjacency_length');

        /** @type {number} */
        this.vertex_count = vertex_count;
        /** @type {number} */
        this.adjacency_length = adjacency_length;

        // Total uint32 slots: (vertex_count + 1) for edge_addresses,
        // adjacency_length × 2 for adjacency and edge_weights,
        // vertex_count for vertex_weights.
        const total_uint32_slots = (vertex_count + 1) + (adjacency_length * 2) + vertex_count;

        /**
         * Single backing buffer for all four arrays.
         * @type {ArrayBuffer}
         * @private
         */
        this.__data_buffer = new ArrayBuffer(total_uint32_slots * 4);

        let byte_offset = 0;

        /**
         * CSR row pointers. Length = vertex_count + 1; the final entry equals
         * adjacency_length.
         * @type {Uint32Array}
         */
        this.edge_addresses = new Uint32Array(this.__data_buffer, byte_offset, vertex_count + 1);
        byte_offset += (vertex_count + 1) * 4;

        /**
         * CSR column indices — neighbour vertex ids.
         * @type {Uint32Array}
         */
        this.adjacency = new Uint32Array(this.__data_buffer, byte_offset, adjacency_length);
        byte_offset += adjacency_length * 4;

        /**
         * Edge weights parallel to `adjacency`.
         * @type {Uint32Array}
         */
        this.edge_weights = new Uint32Array(this.__data_buffer, byte_offset, adjacency_length);
        byte_offset += adjacency_length * 4;

        /**
         * Per-vertex weight. Initialised to all-1s; overwrite via direct
         * indexing or `vertex_weights.set(...)` if your domain needs non-uniform
         * weights. Remember to call `refresh_total_vertex_weight()` after.
         * @type {Uint32Array}
         */
        this.vertex_weights = new Uint32Array(this.__data_buffer, byte_offset, vertex_count);
        this.vertex_weights.fill(1);

        /**
         * Cached `Σ vertex_weights[v]`. Kept in sync via `refresh_total_vertex_weight()`.
         * @type {number}
         */
        this.total_vertex_weight = vertex_count;
    }

    /**
     * The underlying shared buffer. Useful for fast bulk operations and for
     * passing the graph between Web Workers via transfer.
     * @returns {ArrayBuffer}
     */
    get arrayBuffer() {
        return this.__data_buffer;
    }

    /**
     * Degree of vertex `vertex` (number of outgoing edges in the CSR).
     * @param {number} vertex
     * @returns {number}
     */
    degree_of(vertex) {
        return this.edge_addresses[vertex + 1] - this.edge_addresses[vertex];
    }

    /**
     * Sum of all entries in `edge_weights[0..adjacency_length)`.
     * Each undirected edge contributes twice for a symmetric graph.
     * @returns {number}
     */
    sum_of_edge_weights() {
        const edge_weights = this.edge_weights;
        const n = this.adjacency_length;
        let sum = 0;
        for (let i = 0; i < n; i++) {
            sum += edge_weights[i];
        }
        return sum;
    }

    /**
     * Recompute and store `total_vertex_weight` from the current `vertex_weights`.
     */
    refresh_total_vertex_weight() {
        const vertex_weights = this.vertex_weights;
        const n = this.vertex_count;
        let sum = 0;
        for (let i = 0; i < n; i++) {
            sum += vertex_weights[i];
        }
        this.total_vertex_weight = sum;
    }

    /**
     * Shrink `adjacency` and `edge_weights` in place to `new_adjacency_length`.
     *
     * Use after a worst-case allocation has been partially filled (e.g. during
     * coarse-graph contraction where the exact edge count is only known after
     * the merge pass). Reallocates the shared buffer at the smaller size and
     * recreates all four typed-array views; existing references to
     * `this.adjacency` etc. are invalidated.
     *
     * Vertex-side data (`edge_addresses`, `vertex_weights`) and
     * `total_vertex_weight` are preserved.
     *
     * No-op if `new_adjacency_length === adjacency_length`.
     *
     * @param {number} new_adjacency_length must be <= current `adjacency_length`
     */
    truncate_adjacency(new_adjacency_length) {
        assert.isNonNegativeInteger(new_adjacency_length, 'new_adjacency_length');
        assert.lessThanOrEqual(new_adjacency_length, this.adjacency_length, 'truncate_adjacency cannot grow');

        if (new_adjacency_length === this.adjacency_length) {
            return;
        }

        // Keep references to the OLD views — they remain valid because the old
        // buffer stays alive until we drop the last reference to it (which
        // happens implicitly when we reassign this.__data_buffer at the end).
        // We can copy DIRECTLY from old views into new views with no
        // intermediate snapshot, since the two buffers don't overlap.
        const old_edge_addresses = this.edge_addresses;
        const old_adjacency = this.adjacency;
        const old_edge_weights = this.edge_weights;
        const old_vertex_weights = this.vertex_weights;

        const total_uint32_slots = (this.vertex_count + 1) + (new_adjacency_length * 2) + this.vertex_count;
        const new_buffer = new ArrayBuffer(total_uint32_slots * 4);

        let byte_offset = 0;
        const new_edge_addresses = new Uint32Array(new_buffer, byte_offset, this.vertex_count + 1);
        byte_offset += (this.vertex_count + 1) * 4;
        const new_adjacency = new Uint32Array(new_buffer, byte_offset, new_adjacency_length);
        byte_offset += new_adjacency_length * 4;
        const new_edge_weights = new Uint32Array(new_buffer, byte_offset, new_adjacency_length);
        byte_offset += new_adjacency_length * 4;
        const new_vertex_weights = new Uint32Array(new_buffer, byte_offset, this.vertex_count);

        // Direct old-view → new-view copies. `subarray` is a view, not a copy,
        // so the only data movement is the underlying `.set()`.
        new_edge_addresses.set(old_edge_addresses);
        new_adjacency.set(old_adjacency.subarray(0, new_adjacency_length));
        new_edge_weights.set(old_edge_weights.subarray(0, new_adjacency_length));
        new_vertex_weights.set(old_vertex_weights);

        this.__data_buffer = new_buffer;
        this.edge_addresses = new_edge_addresses;
        this.adjacency = new_adjacency;
        this.edge_weights = new_edge_weights;
        this.vertex_weights = new_vertex_weights;
        this.adjacency_length = new_adjacency_length;
    }

    /**
     * Copy the contents of `other` into `this`. Other must have matching
     * `vertex_count` and `adjacency_length` (so the layouts align).
     *
     * Performed as a single bulk Uint32Array `.set()` thanks to the shared
     * buffer layout — significantly faster than copying the four arrays
     * individually.
     *
     * @param {CSRGraph} other
     */
    copy(other) {
        assert.equal(other.isCSRGraph, true, 'other.isCSRGraph !== true');
        assert.equal(this.vertex_count, other.vertex_count, 'vertex_count mismatch');
        assert.equal(this.adjacency_length, other.adjacency_length, 'adjacency_length mismatch');

        new Uint32Array(this.__data_buffer).set(new Uint32Array(other.__data_buffer));
        this.total_vertex_weight = other.total_vertex_weight;
    }

    /**
     * Deep copy. Returns a new `CSRGraph` with independent buffers.
     * @returns {CSRGraph}
     */
    clone() {
        const out = new CSRGraph(this.vertex_count, this.adjacency_length);
        out.copy(this);
        return out;
    }

    /**
     * Structural equality — same shape and same buffer contents.
     * @param {CSRGraph} other
     * @returns {boolean}
     */
    equals(other) {
        if (this === other) {
            return true;
        }
        if (other === null || other === undefined || other.isCSRGraph !== true) {
            return false;
        }
        if (this.vertex_count !== other.vertex_count) {
            return false;
        }
        if (this.adjacency_length !== other.adjacency_length) {
            return false;
        }
        if (this.total_vertex_weight !== other.total_vertex_weight) {
            return false;
        }
        return is_typed_array_equals(
            new Uint32Array(this.__data_buffer),
            new Uint32Array(other.__data_buffer)
        );
    }

    /**
     * 32-bit hash mixing the metadata fields and the entire buffer contents.
     * Uses meep's standard `(h*31 + element)` convention via `uint32_array_hash`.
     *
     * @returns {number}
     */
    hash() {
        let h = this.vertex_count;
        h = ((h << 5) - h) + this.adjacency_length;
        h = ((h << 5) - h) + this.total_vertex_weight;
        const buffer_view = new Uint32Array(this.__data_buffer);
        const buffer_hash = uint32_array_hash(buffer_view, 0, buffer_view.length);
        h = ((h << 5) - h) + buffer_hash;
        return h | 0;
    }

    /**
     * Construct a CSRGraph from existing arrays by COPYING into a new
     * shared-buffer instance. The input arrays are not retained.
     *
     * `vertex_weights` is optional — if not provided, the new graph keeps the
     * default all-1s vertex weights and `total_vertex_weight = vertex_count`.
     *
     * @param {number} vertex_count
     * @param {ArrayLike<number>} edge_addresses length vertex_count + 1
     * @param {ArrayLike<number>} adjacency length edge_addresses[vertex_count]
     * @param {ArrayLike<number>} edge_weights parallel to adjacency
     * @param {ArrayLike<number>} [vertex_weights] optional; length vertex_count
     * @returns {CSRGraph}
     */
    static fromArrays(vertex_count, edge_addresses, adjacency, edge_weights, vertex_weights) {
        const adjacency_length = edge_addresses[vertex_count];
        const out = new CSRGraph(vertex_count, adjacency_length);

        out.edge_addresses.set(edge_addresses);
        out.adjacency.set(adjacency);
        out.edge_weights.set(edge_weights);

        if (vertex_weights !== undefined) {
            out.vertex_weights.set(vertex_weights);
            out.refresh_total_vertex_weight();
        }

        return out;
    }
}

/**
 * Type-check marker — avoids `instanceof` across module realms while still
 * letting us assert "is a CSRGraph" cheaply.
 * @readonly
 * @type {boolean}
 */
CSRGraph.prototype.isCSRGraph = true;
