/**
 * @readonly
 * @enum {string}
 */
import { assert } from "../../assert.js";
import { DataType2DataViewReaders } from "../../binary/data_view/DataType2DataViewReaders.js";
import { DataType2DataViewWriters } from "../../binary/data_view/DataType2DataViewWriters.js";
import { EndianType } from "../../binary/EndianType.js";
import { BinaryDataType } from "../../binary/type/BinaryDataType.js";
import { DataTypeByteSizes } from "../../binary/type/DataTypeByteSizes.js";
import { Cache } from "../../cache/Cache.js";
import { FunctionCompiler } from "../../function/FunctionCompiler.js";
import { objectKeyByValue } from "../../model/object/objectKeyByValue.js";
import { computeStringHash } from "../../primitives/strings/computeStringHash.js";
import { computeHashArray } from "../array/computeHashArray.js";
import { isArrayEqualStrict } from "../array/isArrayEqualStrict.js";

/**
 *
 * @param {BinaryDataType[]} types
 * @param {EndianType} [endianType]
 * @returns {function(data_view:DataView, byte_offset:number, result:number[]):void}
 */
function genRowReader(types, endianType = EndianType.BigEndian) {
    let offset = 0;

    const lines = [];

    const numTypes = types.length;

    for (let i = 0; i < numTypes; i++) {

        const type = types[i];

        const reader_name = DataType2DataViewReaders[type];

        assert.isString(reader_name, 'reader_name');

        const littleEndianFlag = endianType === EndianType.BigEndian ? 'false' : 'true';

        // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
        lines.push(`result[${i}] = dataView.${reader_name}(${offset} + byteOffset, ${littleEndianFlag});`);

        offset += DataTypeByteSizes[type];

    }

    const result = FunctionCompiler.INSTANCE.compile({
        args: ['dataView, byteOffset, result'],
        code: lines.join("\n")
    });

    return result;
}

/**
 *
 * @param {BinaryDataType[]} types
 * @param {EndianType} [endianType]
 * @returns {function(data_view:DataView, byte_offset:number, record:number[]):void}
 */
function genRowWriter(types, endianType = EndianType.BigEndian) {
    let offset = 0;

    const lines = [];

    const numTypes = types.length;

    for (let i = 0; i < numTypes; i++) {
        const type = types[i];

        const writer_name = DataType2DataViewWriters[type];

        assert.isString(writer_name, 'writer_name');

        const littleEndianFlag = endianType === EndianType.BigEndian ? 'false' : 'true';

        // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
        lines.push(`dataView.${writer_name}(${offset} + byteOffset, record[${i}], ${littleEndianFlag});`);

        offset += DataTypeByteSizes[type];
    }

    const result = FunctionCompiler.INSTANCE.compile({
            args: ['dataView, byteOffset, record'],
            code: lines.join("\n")
        }
    );

    return result;
}

/**
 *
 * @param {BinaryDataType} type
 * @param {number} offset
 * @param {EndianType} [endianType]
 * @returns {function(data_view:DataView, byte_offset:number, value:number):void}
 */
function compileDataViewValueWriter(type, offset, endianType = EndianType.BigEndian) {
    const writeMethod = DataType2DataViewWriters[type];

    const littleEndianFlag = endianType === EndianType.BigEndian ? 'false' : 'true';

    return FunctionCompiler.INSTANCE.compile({
        args: ['dataView, byteOffset, value'],
        code: `dataView.${writeMethod}(byteOffset+${offset}, value, ${littleEndianFlag});`
    });
}

/**
 *
 * @param {BinaryDataType} type
 * @param {number} offset
 * @param {EndianType} [endianType]
 * @returns {function(data_view:DataView, byte_offset:number):number}
 */
function compileDataViewValueReader(type, offset, endianType = EndianType.BigEndian) {
    const readMethod = DataType2DataViewReaders[type];

    const littleEndianFlag = endianType === EndianType.BigEndian ? 'false' : 'true';

    return FunctionCompiler.INSTANCE.compile({
        args: ['dataView, byteOffset'],
        code: `return dataView.${readMethod}(byteOffset+${offset}, ${littleEndianFlag});`
    });
}

/**
 * This is a schema class for binary data tables of class {@link RowFirstTable}
 * @class
 * @copyright Company Named Limited (c) 2026
 * @author Alex Goldring
 */
export class RowFirstTableSpec {
    /**
     *
     * @param {BinaryDataType[]} types
     * @param {EndianType} [endianness]
     * @constructor
     */
    constructor(
        types,
        endianness = EndianType.LittleEndian
    ) {

        assert.isArray(types, 'types');
        assert.enum(endianness, EndianType, 'endianness');

        const numTypes = types.length;

        /**
         * @readonly
         * @type {BinaryDataType[]}
         */
        this.types = types;

        /**
         * @readonly
         * @type {EndianType}
         */
        this.endianType = endianness;

        /**
         * @readonly
         * @type {Uint32Array}
         */
        this.columnOffsets = new Uint32Array(numTypes);

        let byteOffset = 0;

        for (let index = 0; index < numTypes; index++) {
            const type = types[index];

            assert.enum(type, BinaryDataType, `type[${index}]`);

            this.columnOffsets[index] = byteOffset;

            const columnByteSize = DataTypeByteSizes[type];

            byteOffset += columnByteSize;
        }


        /**
         * Number of bytes taken up by a single row of values
         * @readonly
         * @type {number}
         */
        this.bytesPerRecord = byteOffset;

        /**
         * @readonly
         * @type {function(DataView, number, number[]): void}
         */
        this.readRowMethod = genRowReader(types, endianness);

        /**
         * @readonly
         * @type {function(DataView, number, number[]): void}
         */
        this.writeRowMethod = genRowWriter(types, endianness);


        //generate cell readers/writers
        this.cellWriters = new Array(numTypes);
        this.cellReaders = new Array(numTypes);

        for (let i = 0; i < numTypes; i++) {
            this.cellReaders[i] = compileDataViewValueReader(types[i], this.columnOffsets[i], endianness);
            this.cellWriters[i] = compileDataViewValueWriter(types[i], this.columnOffsets[i], endianness);
        }
    }

    /**
     *
     * @return {number}
     */
    getColumnCount() {
        return this.types.length;
    }

    /**
     *
     * @return {number}
     */
    hash() {
        let hash = this.endianType === EndianType.BigEndian ? 1 : 0;

        hash += computeHashArray(this.types, computeStringHash);

        return hash;
    }

    /**
     *
     * @param {RowFirstTableSpec} other
     * @returns {boolean}
     */
    equals(other) {
        if (this.endianType !== other.endianType) {
            return false;
        }

        return isArrayEqualStrict(this.types, other.types);
    }

    toString() {
        return `TableSpec{types=[${this.types.join(', ')}], endian=${objectKeyByValue(EndianType, this.endianType)}}`;
    }

    /**
     * Produces an immutable spec, either creates a new spec or retrieves a cached version
     * @param {BinaryDataType[]} types
     * @param {EndianType} [endianType]
     * @returns {RowFirstTableSpec}
     */
    static get(types, endianType = EndianType.BigEndian) {
        assert.isArray(types, 'types');
        assert.enum(endianType, EndianType, 'endianType');

        //compute hash
        const hash = types.join('.') + ':' + endianType;

        const cachedValue = SPEC_CACHE.get(hash);

        if (cachedValue !== null) {
            return cachedValue;
        }

        const newValue = Object.freeze(new RowFirstTableSpec(types));
        SPEC_CACHE.put(hash, newValue);

        return newValue;
    }
}

/**
 * @readonly
 * @type {boolean}
 */
RowFirstTableSpec.prototype.isRowFirstTableSpec = true;


/**
 * @readonly
 * @type {Cache<string,RowFirstTableSpec>}
 */
const SPEC_CACHE = new Cache({
    keyHashFunction: computeStringHash
});

