import { assert } from "../../../../../core/assert.js";
import { BinaryBuffer } from "../../../../../core/binary/BinaryBuffer.js";
import { IllegalStateException } from "../../../../../core/fsm/exceptions/IllegalStateException.js";

import { returnEmptyArray } from "../../../../../core/function/returnEmptyArray.js";
import { objectKeyByValue } from "../../../../../core/model/object/objectKeyByValue.js";
import { executeBinaryClassUpgraderChain } from "../executeBinaryClassUpgraderChain.js";
import { BinaryCollectionHeaderCodec, BinaryCollectionHeaderLayout } from "./BinaryCollectionHeaderCodec.js";

/**
 *
 * @enum {number}
 */
const State = {
    Initial: 0,
    Ready: 1
};

/**
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class BinaryCollectionDeSerializer {

    /**
     * @private
     * @type {BinarySerializationRegistry}
     */
    registry = null;

    /**
     * @private
     * @type {BinaryBuffer}
     */
    buffer = null;

    /**
     *
     * @type {boolean}
     * @private
     */
    __dictionaryEnabled = false;
    /**
     *
     * @type {boolean}
     * @private
     */
    __upgradeRequired = false;

    /**
     * @private
     * @type {number}
     */
    elementCount = 0;

    /**
     * @private
     * @type {number}
     */
    elementIndex = 0;

    /**
     * @private
     * @type {number}
     */
    startAddress = 0;

    /**
     * @private
     * @type {BinaryClassSerializationAdapter}
     */
    adapter = null;

    /**
     * @private
     * @type {BinaryClassUpgrader[]}
     */
    upgraders = null;

    /**
     *
     * @type {BinaryBuffer}
     * @private
     */
    __upgradeBuffer0 = new BinaryBuffer();
    /**
     *
     * @type {BinaryBuffer}
     * @private
     */
    __upgradeBuffer1 = new BinaryBuffer();

    /**
     * @private
     * @type {State}
     */
    state = State.Initial;


    /**
     *
     * @param {BinarySerializationRegistry} registry
     */
    setRegistry(registry) {
        assert.notEqual(registry, undefined, 'registry is undefined');
        assert.notEqual(registry, null, 'registry is null');

        this.registry = registry;
    }

    /**
     *
     * @returns {BinarySerializationRegistry}
     */
    getRegistry() {
        return this.registry;
    }

    /**
     *
     * @param {BinaryBuffer} buffer
     */
    setBuffer(buffer) {
        assert.notEqual(buffer, undefined, 'buffer is undefined');
        assert.notEqual(buffer, null, 'buffer is null');

        this.buffer = buffer;
    }

    /**
     *
     * @returns {number}
     */
    getElementCount() {
        return this.elementCount;
    }

    /**
     *
     * @returns {number}
     */
    getElementIndex() {
        return this.elementIndex;
    }

    /**
     * @template
     * @returns {Class<T>|null}
     */
    getElementClass() {
        const adapter = this.adapter;

        if (adapter === null) {
            return null;
        }

        return adapter.klass;
    }

    /**
     *
     * @param {function(name:string, klass: Class, adapter:BinaryClassSerializationAdapter):[]} [adapterOptionsSupplier]
     */
    initialize({ adapterOptionsSupplier = returnEmptyArray } = {}) {
        assert.isFunction(adapterOptionsSupplier, 'adapterOptionsSupplier');

        if (this.state !== State.Initial) {
            throw new IllegalStateException(`Expected state to be Initial, instead was ${objectKeyByValue(State, this.state)}`);
        }

        const buffer = this.buffer;

        const registry = this.registry;

        assert.notEqual(buffer, null, 'buffer is null');
        assert.notEqual(registry, null, 'registry is null');

        const className = buffer.readUTF8String();

        const adapter = registry.getAdapter(className);

        if (adapter === undefined) {
            throw new Error(`No serialization adapter found for '${className}'`);
        }

        this.adapter = adapter;

        const adapterOptions = adapterOptionsSupplier(className, adapter.klass, adapter);

        assert.isArray(adapterOptions, 'adapterOptions');

        //initialize adapter with options
        this.adapter.initialize.apply(this.adapter, adapterOptions);

        const header = buffer.readUint32();

        const headerValues = [];

        BinaryCollectionHeaderCodec.decode(header, headerValues);

        //read serialized version
        const version = headerValues[BinaryCollectionHeaderLayout.Version];

        //read flags
        this.__dictionaryEnabled = headerValues[BinaryCollectionHeaderLayout.Dictionary] !== 0;

        //read element count
        this.elementCount = buffer.readUint32();

        //set element index to 0
        this.elementIndex = 0;

        //check if data needs to be upgraded
        if (version > adapter.version) {
            throw new Error(`${className} data version is ${version}, which is greater than the registered serialization adapter version(=${(adapter.version)})`);
        }

        if (version < adapter.version) {
            this.__upgradeRequired = true;

            //data needs to be upgraded
            const upgradersChain = registry.getUpgradersChain(className, version, adapter.version);

            if (upgradersChain === null) {
                throw new Error(`No upgrade chain exists for class '${className}' from version ${version} to current adapter version ${(adapter.version)}`);
            }

            this.upgraders = upgradersChain;
        } else {
            this.__upgradeRequired = false;
        }

        //
        this.__upgradeBuffer0.position = 0;
        this.__upgradeBuffer1.position = 0;

        this.startAddress = this.buffer.position;


        //Update internal state
        this.state = State.Ready;
    }

    finalize() {

        if (this.state !== State.Ready) {
            throw new IllegalStateException(`Expected state to be Ready, instead was ${objectKeyByValue(State, this.state)}`);
        }

        //finalize current adapter
        this.adapter.finalize();

        //Update internal state
        this.state = State.Initial;
    }

    /**
     * @private
     * @returns T
     */
    readPlainValue() {
        let buffer = this.buffer;

        if (this.__upgradeRequired) {
            //binary representation requires an upgrade
            buffer = executeBinaryClassUpgraderChain(this.upgraders, buffer, this.__upgradeBuffer0, this.__upgradeBuffer1);
        }

        const Klass = this.adapter.klass;

        const value = new Klass();

        this.adapter.deserialize(buffer, value);

        return value;
    }

    /**
     * @template T
     * @returns {{value: T, key: number}}
     */
    read() {
        const buffer = this.buffer;

        //read key
        const key = buffer.readUintVar();
        let value = null;

        if (this.__dictionaryEnabled) {
            //read record header
            const header0 = buffer.readUint8();

            if (header0 === 0) {
                //plain record
                value = this.readPlainValue();

            } else {
                //dictionary record

                const headerType = header0 & 0x3;

                let header;

                if (headerType === 1) {
                    header = header0;
                } else if (headerType === 2) {
                    //16 bit header
                    buffer.position--;

                    header = buffer.readUint16LE();
                } else if (headerType === 3) {
                    buffer.position--;

                    header = buffer.readUint32LE();
                }

                const offset = header >> 2;

                //read address
                const address = offset + this.startAddress;

                const recordEnd = buffer.position;

                buffer.position = address;

                value = this.readPlainValue();

                //restore position
                buffer.position = recordEnd;
            }
        } else {
            value = this.readPlainValue();
        }


        this.elementIndex++;

        return {
            key,
            value
        };
    }

}
