import { assert } from "../../assert.js";
import { makeArrayBuffer } from "../../binary/makeArrayBuffer.js";
import Signal from "../../events/signal/Signal.js";
import { max2 } from "../../math/max2.js";
import { array_copy } from "../array/array_copy.js";
import { array_buffer_copy } from "../array/typed/array_buffer_copy.js";

/**
 * How much space to reserve in a newly created table, counted in ROWs
 * @type {number}
 */
const DEFAULT_CAPACITY = 8;

/**
 * Must be greater than 1
 * @type {number}
 */
const ALLOCATION_GROW_FACTOR = 1.5;

/**
 * Minimum number of rows to add when growing
 * @type {number}
 */
const ALLOCATION_GROW_MINIMUM_STEP = 32;

/**
 * Must be less than 1 and greater than 0
 * @type {number}
 */
const ALLOCATION_SHRINK_FACTOR = 0.5;

/**
 * Compact binary table storage.
 * Very efficient in terms of memory usage and allocation.
 * Read and write speeds are optimized through code generation.
 * You can think of it as an SQL table but only for numeric types.
 *
 * @copyright Company Named Limited (c) 2026
 * @author Alex Goldring
 */
export class RowFirstTable {
    /**
     *
     * @param {RowFirstTableSpec} spec what does the schema look like? How many columns do we have and what are their types?
     * @param {boolean} [shared_array] should we use SharedArrayBuffer instead of ArrayBuffer?
     * @constructor
     */
    constructor(
        spec,
        shared_array = false
    ) {
        assert.defined(spec, 'spec');
        assert.notNull(spec, 'spec');
        assert.equal(spec.isRowFirstTableSpec, true, 'spec.isRowFirstTableSpec !== true');

        assert.isBoolean(shared_array, 'shared_array');

        /**
         *
         * @type {RowFirstTableSpec}
         */
        this.spec = spec;

        /**
         *
         * @type {ArrayBuffer}
         */
        this.data = makeArrayBuffer(spec.bytesPerRecord * DEFAULT_CAPACITY, shared_array);

        /**
         * number of records
         * @type {number}
         */
        this.length = 0;

        /**
         * capacity in number of records
         * @type {number}
         */
        this.capacity = DEFAULT_CAPACITY;

        /**
         *
         * @type {DataView}
         */
        this.dataView = new DataView(this.data);

        /**
         *
         * @type {{added: Signal}}
         */
        this.on = {
            /**
             * @readonly
             */
            added: new Signal()
        };

    }


    /**
     * Useful for deserialization.
     * This is an unsafe method, avoid using it if you are not sure.
     * NOTE: capacity is set automatically
     * NOTE: length is not set automatically, you have to do that yourself
     * @param {ArrayBuffer|SharedArrayBuffer} buffer
     */
    set array_buffer(buffer) {
        this.data = buffer;
        this.dataView = new DataView(buffer);

        this.capacity = Math.floor(buffer.byteLength / this.spec.bytesPerRecord);
        this.length = 0;
    }

    /**
     *
     * @returns {number}
     */
    hash() {

        const byteLength = this.data.byteLength;
        const dataView = this.dataView;

        let result = byteLength;


        const uint32_length = byteLength >> 2;

        // to keep hash calculation fast, do a fixed number evenly spaced taps inside the data table
        const tap_count = 31;

        const step_size = max2(
            Math.floor(uint32_length / tap_count),
            1
        );

        const step_count = Math.floor(uint32_length / step_size);

        for (let i = 0; i < step_count; i++) {
            const address = (i * step_size) << 2;

            const uint32 = dataView.getUint32(address);

            result = ((result << 5) - result) + uint32;
        }

        return result;
    }

    /**
     *
     * @param {number} rowCount
     */
    setCapacity(rowCount) {
        assert.isNonNegativeInteger(rowCount, 'rowCount');

        if (this.capacity === rowCount) {
            // already right size
            return;
        }

        const oldData = this.data;

        const bytesPerRecord = this.spec.bytesPerRecord;
        const byteSize = rowCount * bytesPerRecord;

        try {
            // can be either ArrayBuffer or SharedArrayBuffer
            const BufferConstructor = this.data.constructor;

            this.data = new BufferConstructor(byteSize);
        } catch (e) {
            throw new Error("failed to create a new array buffer of size: " + byteSize);
        }

        //check the size of new array
        if (this.data.byteLength !== byteSize) {
            throw new Error("Generated array was truncated unexpectedly from " + byteSize + " to " + this.data.byteLength);
        }

        // TODO consider using {@link array_buffer_copy} instead for speed

        const newArray = new Uint8Array(this.data);
        const oldArray = new Uint8Array(oldData);

        const sourceCopyLength = this.length * bytesPerRecord;
        try {
            newArray.set(oldArray.subarray(0, sourceCopyLength), 0);
        } catch (e) {
            if (e instanceof RangeError) {
                throw new Error("Failed to copy contents of original due to to size violation. OldSize: " + sourceCopyLength + ", NewSize: " + this.data.byteLength);
            } else {
                throw e;
            }
        }

        this.capacity = rowCount;

        this.dataView = new DataView(this.data, 0);
    }

    /**
     * Drop excess capacity, setting capacity exactly to the current length
     */
    trim() {
        this.setCapacity(this.length);
    }

    /**
     *
     * @param {number} row_count
     */
    resize(row_count) {
        assert.isNonNegativeInteger(row_count, 'row_count');

        if (this.capacity < row_count) {

            //grow
            const new_size = Math.max(
                Math.ceil(row_count * ALLOCATION_GROW_FACTOR),
                row_count + ALLOCATION_GROW_MINIMUM_STEP
            );

            this.setCapacity(new_size);

        } else if (this.capacity * ALLOCATION_SHRINK_FACTOR > row_count) {

            //shrink
            this.setCapacity(row_count);

        }

    }

    /**
     *
     * @param {number} row_index
     * @param {number} column_index
     * @param {number} value
     */
    writeCellValue(row_index, column_index, value) {
        const spec = this.spec;

        assert.lessThan(column_index, spec.getColumnCount(), 'overflow');
        assert.isNonNegativeInteger(column_index, 'column_index');

        assert.isNonNegativeInteger(row_index, 'row_index');

        const bytesPerRecord = spec.bytesPerRecord;

        const rowAddress = row_index * bytesPerRecord;

        const cellWriters = spec.cellWriters;

        const cellWriter = cellWriters[column_index];

        cellWriter(this.dataView, rowAddress, value);
    }

    /**
     * read a single cell value from the table
     * @param {number} row_index
     * @param {number} column_index
     * @returns {number}
     */
    readCellValue(row_index, column_index) {
        const spec = this.spec;

        assert.lessThan(column_index, spec.getColumnCount(), 'overflow');
        assert.isNonNegativeInteger(column_index, 'column_index');

        assert.isNonNegativeInteger(row_index, 'row_index');

        const bytesPerRecord = spec.bytesPerRecord;

        const rowAddress = row_index * bytesPerRecord;

        const cellReaders = spec.cellReaders;

        const cellReader = cellReaders[column_index];

        return cellReader(this.dataView, rowAddress);
    }

    /**
     * Remove rows from the table
     * @param {number} index starting row
     * @param {number} row_count number of rows to be removed
     */
    removeRows(index, row_count) {

        //validate presence to requested rows
        assert.lessThanOrEqual(index + row_count, this.length, 'underflow');

        const data = this.data;

        const array = new Uint8Array(data);

        const bytesPerRecord = this.spec.bytesPerRecord;

        //shift tail of the table forward
        const target = index * bytesPerRecord;
        const start = target + row_count * bytesPerRecord;
        const end = this.length * bytesPerRecord;

        array.copyWithin(target, start, end);

        //adjust new length
        this.length -= row_count;

        //resize table
        this.resize(this.length);
    }

    /**
     * Insert a number of blank rows at the given offset
     * Table becomes larger as a result
     * NOTE: doesn't send {@link on.added} signal
     * @param {number} index
     * @param {number} row_count
     */
    insertRows(index, row_count) {
        assert.isNonNegativeInteger(index, 'index');
        assert.isNonNegativeInteger(row_count, 'row_count');

        const future_length = this.length + row_count;

        this.resize(future_length);

        const data = this.data;

        const bytesPerRecord = this.spec.bytesPerRecord;

        const array = new Uint8Array(data);

        //shift tail of the table forward
        const target = (index + row_count) * bytesPerRecord;
        const end = future_length * bytesPerRecord;
        const start = index * bytesPerRecord;

        array.copyWithin(target, start, end);

        this.length = future_length;
    }

    /**
     * Created a new row at the end of the table, does not dispatch {@link on.added} signal
     * Values are undefined, typically it will be 0s, but if data was previously written in that memory region - that data will be retained.
     * Make sure to clear the row or write to it before reading it
     * @return {number} index of the created row
     */
    createEmptyRow() {
        const newRowCount = this.length + 1;

        this.resize(newRowCount);

        const rowIndex = this.length;

        this.length = newRowCount;

        return rowIndex;
    }

    /**
     *
     * @param {Array.<number>} values
     * @returns {number} index of newly added row
     */
    addRow(values) {
        assert.isArrayLike(values, 'values');

        const rowIndex = this.createEmptyRow();

        this.spec.writeRowMethod(this.dataView, this.spec.bytesPerRecord * rowIndex, values);

        this.on.added.send2(rowIndex, values);

        return rowIndex;
    }

    /**
     * @deprecated Use {@link addRow} and {@link writeRow} instead
     * @param {number} count number of rows to be added
     * @param {function(row_index:number, row:Array.<number>):*} valueSupplier supplier of row values, called with row index and an empty row to be filled
     */
    addRows(count, valueSupplier) {
        throw new Error('deprecated, use .addRow and .writeRow instead');
    }

    /**
     * Copy a single row, value in the source row is unaffected
     * @param {number} source
     * @param {number} target
     */
    copyRow(source, target) {
        assert.isNonNegativeInteger(source, 'source');
        assert.isNonNegativeInteger(target, 'target');

        if (source === target) {
            // no operation
            return;
        }

        // figure out addresses
        const bytes_per_record = this.spec.bytesPerRecord;

        const source_address = source * bytes_per_record;
        const target_address = target * bytes_per_record;

        const data_view = this.dataView;

        for (let i = 0; i < bytes_per_record; i++) {
            const v = data_view.getUint8(source_address + i);

            data_view.setUint8(target_address + i, v);
        }
    }


    /**
     * Read a single row of values from the table
     * @param {number} index
     * @param {number[]} [result] where row values are to be stored
     * @returns {number[]} result
     */
    readRow(index, result = []) {
        assert.isNonNegativeInteger(index, 'index');

        const spec = this.spec;

        spec.readRowMethod(this.dataView, spec.bytesPerRecord * index, result);

        return result;
    }

    /**
     * Write a single row of values
     * @param {number} index
     * @param {number[]} record
     */
    writeRow(index, record) {
        assert.isNonNegativeInteger(index, 'index');

        const spec = this.spec;

        assert.greaterThanOrEqual(record.length, spec.getColumnCount(), 'input record length is too small');

        spec.writeRowMethod(this.dataView, spec.bytesPerRecord * index, record);
    }

    /**
     * Sets memory region of the row to 0s.
     * Useful for initializing dirty rows for re-use.
     * If you have a specific value in mind - use {@link writeRow} method instead
     * NOTE: All numeric types will produce a 0 after this, so every cell of this row will be 0
     * @param {number} index
     */
    clearRow(index) {
        assert.isNonNegativeInteger(index, 'index');

        const spec = this.spec;

        const bytesPerRecord = spec.bytesPerRecord;

        const address = index * bytesPerRecord;

        const dataView = this.dataView;

        for (let i = 0; i < bytesPerRecord; i++) {
            dataView.setUint8(address + i, 0);
        }
    }

    /**
     * Reverse order of rows, row-0 will end up at and previously last row will become the first row etc.
     */
    reverse_rows() {
        const bpr = this.spec.bytesPerRecord;
        const copy_buffer = new Uint8Array(bpr);

        const buffer = this.data;
        const wrapper = new Uint8Array(buffer);

        const length = this.length;

        if (length <= 1) {
            // need at least 2 rows for reversal to make any change
            return;
        }

        const last_row_index = length - 1;

        const traversal_limit = last_row_index >>> 1;

        for (let i = 0; i <= traversal_limit; i++) {
            const address = i * bpr;
            array_copy(wrapper, address, copy_buffer, 0, bpr);

            const swap_index = last_row_index - i;
            const swap_address = swap_index * bpr;

            wrapper.copyWithin(address, swap_address, swap_address + bpr)
            wrapper.set(copy_buffer, swap_address);
        }
    }

    /**
     * clear out all the data and free memory
     */
    clear() {
        //clear out data
        this.length = 0;
        this.setCapacity(0);
    }

    /**
     * Utility method. Returns table contents as a 2d array of [row][cell] form.
     * Primarily useful for data conversion and debugging.
     * @returns {number[][]}
     */
    toRowArray() {

        const result = [];

        for (let i = 0; i < this.length; i++) {

            const row = this.readRow(i);

            result.push(row);
        }

        return result;

    }

    /**
     * Print the table to console.
     * Useful for debugging.
     */
    printToConsole() {
        const rows = this.toRowArray();

        console.table(rows);
    }

    /**
     * Copy data from another table. Specs must match.
     * NOTE: does not dispatch {@link onAdded} signal
     * @param {RowFirstTable} other
     */
    copy(other) {

        // check that the spec is equivalent
        if (!this.spec.equals(other.spec)) {
            throw new Error('Different table specs');
        }

        const record_count = other.length;

        this.resize(record_count);

        this.length = other.length;

        // copy data
        const data_byte_count = other.spec.bytesPerRecord * record_count;

        array_buffer_copy(other.data, 0, this.data, 0, data_byte_count);
    }

    /**
     * @param {RowFirstTable} other
     * @returns {boolean}
     */
    equals(other) {
        if (other === this) {
            // shortcut
            return true;
        }

        if (other === undefined || other === null) {
            // this should not generally not happen
            return false;
        }

        if (!this.spec.equals(other.spec)) {
            return false;
        }

        const this_length = this.length;

        if (this_length !== other.length) {
            return false;
        }

        const total_bytes = this_length * this.spec.bytesPerRecord;

        // TODO can use `is_array_buffer_equals` to speed this up

        const this_data_view = this.dataView;
        const other_data_view = other.dataView;

        for (let i = 0; i < total_bytes; i++) {
            const a = this_data_view.getUint8(i);
            const b = other_data_view.getUint8(i);

            if (a !== b) {
                return false;
            }
        }

        return true;
    }
}