import { assert } from "../../../core/assert.js";
import { current_time_in_seconds } from "../../../core/time/current_time_in_seconds.js";
import { logger } from "../../logging/GlobalLogger.js";
import { SerializationFlags, SerializationMetadata } from "../components/SerializationMetadata.js";
import { BinaryCollectionSerializer } from "./binary/collection/BinaryCollectionSerializer.js";
import { BinaryObjectSerializationAdapter } from "./binary/object/BinaryObjectSerializationAdapter.js";
import { COMPONENT_SERIALIZATION_TRANSIENT_FIELD } from "./COMPONENT_SERIALIZATION_TRANSIENT_FIELD.js";

/**
 * @template T
 * @param {Class<T>|constructor|function|{serializable?:boolean}} componentClass
 * @returns {boolean}
 */
function isComponentClassSerializable(componentClass) {
    return componentClass.serializable !== false;
}

/**
 * Whether an entity should be serialized at all
 * @param {number} entity
 * @param {EntityComponentDataset} dataset
 * @param {number} smComponentIndex
 * @param {Object} componentInstance
 * @returns {boolean}
 */
function isEntityTransient(entity, dataset, smComponentIndex, componentInstance) {
    if (smComponentIndex === -1) {
        return false;
    }

    //check component instance flag
    if (componentInstance[COMPONENT_SERIALIZATION_TRANSIENT_FIELD] === true) {
        return true;
    }


    /**
     *
     * @type {SerializationMetadata}
     */
    const serializationMetadata = dataset.getComponentByIndex(entity, smComponentIndex);

    if (serializationMetadata === undefined) {
        return false;
    }

    const isTransient = serializationMetadata.getFlag(SerializationFlags.Transient);

    return isTransient;
}

/**
 * @example
 *   const serializer = new BinaryBufferSerializer();
 *
 *   const registry = new BinarySerializationRegistry();
 *   registry.registerAdapter(new NameSerializationAdapter());
 *
 *   serializer.registry = registry;
 *
 *   const ecd = new EntityComponentDataset();
 *   new Entity()
 *      .add(new Name("John Doe"))
 *      .build(ecd);
 *
 *   serializer.process(buffer, ecd);
 *
 * @template CTX
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
class BinaryBufferSerializer {
    /**
     *
     * @type {BinarySerializationRegistry}
     */
    registry = null;

    /**
     * Context to be supplied to individual serialization adapters
     * @type {CTX|null}
     */
    engine = null;

    /**
     *
     * @param {EntityComponentDataset} dataset
     * @param {BinaryBuffer} buffer
     */
    process(buffer, dataset) {
        assert.defined(buffer, 'buffer');
        assert.notNull(buffer, 'buffer');
        assert.equal(buffer.isBinaryBuffer, true, 'buffer.isBinaryBuffer !== true');

        assert.defined(dataset, 'dataset');
        assert.notNull(dataset, 'dataset');
        assert.equal(dataset.isEntityComponentDataset, true, 'dataset.isEntityComponentDataset !== true');

        console.groupCollapsed('Serialization');
        console.time('serializing');

        const smComponentIndex = dataset.computeComponentTypeIndex(SerializationMetadata);

        // get a list of serializable component classes
        const serializableComponentTypes = dataset.getComponentTypeMap().filter(isComponentClassSerializable);

        const numSerializableTypes = serializableComponentTypes.length;


        const FORMAT_VERSION = 0;

        buffer.writeUint16(FORMAT_VERSION)

        const typeCountAddress = buffer.position;

        buffer.writeUint16(numSerializableTypes);

        const collectionSerializer = new BinaryCollectionSerializer();

        const registry = this.registry;

        collectionSerializer.setRegistry(registry);
        collectionSerializer.setBuffer(buffer);

        const objectAdapter = new BinaryObjectSerializationAdapter();
        objectAdapter.initialize(registry);

        let numTypesWritten = 0;

        let i;

        const engine = this.engine;

        for (i = 0; i < numSerializableTypes; i++) {

            const componentType = serializableComponentTypes[i];

            const positionComponentsStart = buffer.position;

            const className = componentType.typeName;

            if (className === undefined) {
                throw new Error(`Component type has no .typeName property`);
            }

            collectionSerializer.setClass(className);

            const problemLog = [];

            const initialized = collectionSerializer.initialize({
                /**
                 *
                 * @param {string} className
                 * @param {Class} klass
                 * @param {BinaryClassSerializationAdapter} adapter
                 */
                adapterOptionsSupplier(className, klass, adapter) {
                    return [engine, objectAdapter];
                },
                problemConsumer(message) {
                    problemLog.push(message);
                }
            });

            if (!initialized) {
                logger.error(`Failed to initialize serializer for class '${className}': ${problemLog.join('; ')}`)
                continue;
            }

            const __start_time = current_time_in_seconds();

            let lastEntity = 0;

            dataset.traverseComponents(componentType, function (componentInstance, entity) {
                if (isEntityTransient(entity, dataset, smComponentIndex, componentInstance)) {
                    // skip
                    return;
                }

                assert.ok(entity >= lastEntity, `entity[=${entity}] < lastEntity[=${lastEntity}]`);

                // write only the entity step
                const step = entity - lastEntity;

                lastEntity = entity;

                collectionSerializer.write(step, componentInstance);
            });

            const numComponentsWritten = collectionSerializer.getElementCount();

            const __end_time = current_time_in_seconds();

            collectionSerializer.finalize();

            if (numComponentsWritten > 0) {
                numTypesWritten++;

                const currentPosition = buffer.position;

                // print size of the component set
                const componentsByteSize = currentPosition - positionComponentsStart;
                console.log(`Component ${componentType.typeName} written. Count: ${numComponentsWritten}, Bytes: ${componentsByteSize}, Bytes/component: ${Math.ceil(componentsByteSize / numComponentsWritten)}. Time taken: ${((__end_time - __start_time) * 1000).toFixed(2)}ms`);
            } else {
                // no elements written, lets re-wind to skip the type
                buffer.position = positionComponentsStart;
            }
        }

        // go back and patch actual number of written classes
        const p = buffer.position;

        buffer.position = typeCountAddress;
        buffer.writeUint16(numTypesWritten);

        // restore position
        buffer.position = p;

        console.timeEnd('serializing');
        console.groupEnd();
    }
}

export default BinaryBufferSerializer;
