import { assert } from "../../../../../core/assert.js";
import { HashMap } from "../../../../../core/collection/map/HashMap.js";

import { returnEmptyArray } from "../../../../../core/function/returnEmptyArray.js";
import { BinaryCollectionHeaderCodec, BinaryCollectionHeaderLayout } from "./BinaryCollectionHeaderCodec.js";

export class BinaryCollectionSerializer {

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

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

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


    /**
     *
     * @type {String}
     */
    className = null;

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

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


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

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

    /**
     * @private
     * @type {Map<any, number>}
     */
    dictionary = new HashMap({
        capacity: 1024, //pre-allocate larger size to avoid rehashing
    });

    /**
     *
     * @param {string} className
     */
    setClass(className) {
        assert.isString(className, 'className');

        this.className = className;
    }

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

        this.registry = 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;
    }

    /**
     *
     * @param {function(string,Class, BinaryClassSerializationAdapter):[]} [adapterOptionsSupplier]
     * @param {function(string):*} [problemConsumer]
     * @returns {boolean}
     */
    initialize({
                   adapterOptionsSupplier = returnEmptyArray,
                   problemConsumer = console.warn
               } = {}) {

        assert.isFunction(adapterOptionsSupplier, 'adapterOptionsSupplier');

        const className = this.className;

        assert.isString(className, 'className');

        const registry = this.registry;

        const adapter = registry.getAdapter(className);

        if (adapter === undefined) {
            problemConsumer(`No adapter for class '${className}'`);

            return false;
        }

        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);

        this.elementCount = 0;

        const buffer = this.buffer;


        //write class name
        buffer.writeUTF8String(className);

        this.headerAddress = buffer.position;

        //write empty header, to be written later
        buffer.writeUint32(0);

        //write placeholder element count
        buffer.writeUint32(0);

        this.startAddress = buffer.position;

        const componentType = adapter.klass;

        //determine if dictionary can be used
        this.__dictionaryEnabled = typeof componentType.prototype.hash === "function"
            && typeof componentType.prototype.equals === "function";

        //clear dictionary
        this.dictionary.clear();

        return true;
    }

    finalize() {
        const buffer = this.buffer;
        //remember current position
        const endAddress = buffer.position;

        buffer.position = this.headerAddress;

        const headerValues = [];

        headerValues[BinaryCollectionHeaderLayout.Dictionary] = this.__dictionaryEnabled ? 1 : 0;
        headerValues[BinaryCollectionHeaderLayout.Version] = this.adapter.version;

        const header = BinaryCollectionHeaderCodec.encode.apply(null, headerValues);

        //write header
        buffer.writeUint32(header);

        //write element count
        buffer.writeUint32(this.elementCount);

        //restore buffer position
        buffer.position = endAddress;

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

        //clear dictionary
        this.dictionary.clear();
    }

    /**
     * @template T
     * @param {T} value
     * @private
     */
    writePlainValue(value) {
        this.adapter.serialize(this.buffer, value);
    }

    /**
     * @template T
     * @param {number} key
     * @param {T} value
     */
    write(key, value) {
        const buffer = this.buffer;

        //write key
        buffer.writeUintVar(key);

        if (this.__dictionaryEnabled) {

            const dictionary = this.dictionary;

            let address = dictionary.get(value);

            if (address !== undefined) {

                if (address < 63) {
                    buffer.writeUint8(1 | (address << 2));
                } else {

                    if (address < 16383) {
                        buffer.writeUint16LE(2 | (address << 2));
                    } else if (address < 1073741823) {
                        buffer.writeUint32LE(3 | (address << 2));
                    } else {
                        throw new Error(`Address value is too high(=${address})`);
                    }
                }
            } else {
                buffer.writeUint8(0); //original value

                address = buffer.position - this.startAddress;

                this.writePlainValue(value);

                dictionary.set(value, address);
            }

        } else {
            this.writePlainValue(value);
        }

        this.elementCount++;
    }


}
