import { assert } from "../../../../core/assert.js";
import { Graph } from "../../../../core/graph/v2/Graph.js";

/**
 * Contains serializers for various data types as well as data upgraders which enable support for serialization format changes
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class BinarySerializationRegistry {

    /**
     * @readonly
     * @private
     * @type {Map<string, Graph<BinaryClassUpgrader>>}
     */
    upgraders = new Map();
    /**
     * @readonly
     * @private
     * @type {Map<string, BinaryClassSerializationAdapter>}
     */
    serializers = new Map();

    /**
     *
     * @param {BinaryClassSerializationAdapter[]} adapters
     */
    registerAdapters(adapters) {
        adapters.forEach(adapter => {
            this.registerAdapter(adapter);
        });
    }

    /**
     *
     * @param {BinaryClassSerializationAdapter} adapter
     * @param {string} [className] if not specified, will be taken from the {@link BinaryClassSerializationAdapter.klass.typeName}
     * @returns {boolean} true if added, false if adapter already existed
     * @throws {Error} if `className` could not be inferred
     */
    registerAdapter(adapter, className) {
        assert.defined(adapter, 'adapter');
        assert.ok(adapter.isBinaryClassSerializationAdapter, 'adapter is not a BinaryClassSerializationAdapter');

        let _className = className;

        if (_className === undefined) {
            const klass = adapter.klass;

            const typeName = klass.typeName;

            if (typeof typeName === "string") {
                _className = typeName;
            } else {
                throw new Error(`className not specified, could not infer class name from the class itself`);
            }
        }

        assert.isString(_className, 'className');

        if (this.serializers.has(_className)) {
            // a serializer already exists

            console.warn(`Serializer for class '${_className}' already exists, ignoring request`);

            return false;
        }

        this.serializers.set(_className, adapter);

        return true;
    }

    /**
     *
     * @param {string} className
     * @returns {BinaryClassSerializationAdapter|undefined}
     */
    removeAdapter(className) {
        assert.isString(className, 'className');

        const adapter = this.serializers.get(className);

        if (adapter !== undefined) {
            this.serializers.delete(className);
        }

        return adapter;
    }

    /**
     *
     * @param {string} className
     * @returns {BinaryClassSerializationAdapter|undefined}
     */
    getAdapter(className) {
        assert.isString(className, 'className');

        return this.serializers.get(className);
    }

    /**
     *
     * @param {string} className
     * @param {BinaryClassUpgrader} upgrader
     * @returns {boolean} true iff added, false if already exists
     */
    registerUpgrader(className, upgrader) {
        assert.isString(className, 'className');
        assert.defined(upgrader, 'upgrader');
        assert.notNull(upgrader, 'upgrader');
        assert.equal(upgrader.isBinaryClassUpgrader, true, 'upgrader.isBinaryClassUpgrader !== true');

        if (upgrader.getStartVersion() === upgrader.getTargetVersion()) {
            throw new Error(`Upgrader for '${className}' start(=${upgrader.getStartVersion()}) and target(=${upgrader.getTargetVersion()}) versions are the same, must be different`);
        }

        if (upgrader.getStartVersion() > upgrader.getTargetVersion()) {
            console.warn(`Possible error in '${className}' upgrader code, target version(=${upgrader.getTargetVersion()}) < start version(=${upgrader.getStartVersion()})`);
        }

        let classUpgraders = this.upgraders.get(className);

        if (classUpgraders === undefined) {
            classUpgraders = new Graph();

            this.upgraders.set(className, classUpgraders);
        }

        if (classUpgraders.hasNode(upgrader)) {
            // upgrader already registered
            return false;
        }

        classUpgraders.addNode(upgrader);

        // find other upgraders that would connect to this upgrader
        classUpgraders.traverseNodes(u => {
            if (u === upgrader) {
                return;
            }

            if (u.getStartVersion() === upgrader.getTargetVersion()) {
                classUpgraders.createEdge(u, upgrader);
            } else if (u.getTargetVersion() === upgrader.getStartVersion()) {
                classUpgraders.createEdge(upgrader, u);
            }
        });

        return true;
    }

    /**
     *
     * @param {string} className
     * @param {number} startVersion
     * @param {number} goalVersion
     * @returns {BinaryClassUpgrader[]|null} sequential shortest chain of upgraders or null if no valid chain exists
     */
    getUpgradersChain(className, startVersion, goalVersion) {
        assert.isString(className, 'className');
        assert.isNumber(startVersion, 'startVersion');
        assert.isNumber(goalVersion, 'goalVersion');

        if (startVersion === goalVersion) {
            //already at the goal version
            return [];
        }

        const classUpgraders = this.upgraders.get(className);

        if (classUpgraders === undefined) {
            throw new Error(`No upgraders available for class '${className}', upgrade is required from from version ${startVersion} to ${goalVersion}`);
        }

        const starts = [];
        const goals = [];

        classUpgraders.traverseNodes(n => {
            if (n.getStartVersion() === startVersion) {
                starts.push(n);
            }

            if (n.getTargetVersion() === goalVersion) {
                goals.push(n);
            }
        });

        let bestPath = null;
        let bestPathLength = Number.POSITIVE_INFINITY;

        for (let s = 0, sL = starts.length; s < sL; s++) {
            const start = starts[s];

            for (let g = 0, gL = goals.length; g < gL; g++) {
                const goal = goals[g];

                const path = classUpgraders.findPath(start, goal);

                if (path !== null) {

                    const pathLength = path.length;

                    if (bestPathLength > pathLength) {
                        //new best path found

                        bestPath = path;
                        bestPathLength = pathLength;

                    }
                }
            }
        }

        return bestPath;
    }

}

/**
 * @readonly
 * @type {boolean}
 */
BinarySerializationRegistry.prototype.isBinarySerializationRegistry = true;
