/**
 *
 * @param {Function} Klass
 * @param {Map<Function, string>} classNames
 * @param {number} [max_depth] prevents infinite recursion
 * @returns {*}
 */
function resolve_class_name(Klass, classNames, max_depth = 64) {
    let typeName = classNames.get(Klass);

    if (typeName === undefined && Klass.hasOwnProperty("typeName")) {
        // try fallback to "typeName" field on constructor
        typeName = Klass.typeName;
    }

    if (typeName === undefined && max_depth > 0) {
        // try walking up prototype chain

        const proto = Object.getPrototypeOf(Klass);

        if (proto !== null && typeof proto === "object") {

            return resolve_class_name(proto, classNames, max_depth - 1);

        }

    }

    return typeName;
}

/**
 * @template T
 * @param {T} source
 * @param {Map<string, function(v:Object):Object>} serializers
 * @param {Map<*,string>} classNames
 * @param {boolean} [constructorNameFallback]
 */
export function abstractJSONSerializer(source, serializers, classNames, constructorNameFallback = true) {


    if (source === null || typeof source !== "object") {
        // primitive type
        return source;
    }

    // parameter is an object

    // extract type name
    const Klass = source.constructor;
    let typeName = resolve_class_name(Klass, classNames);

    if (typeName === undefined && constructorNameFallback) {
        typeName = Klass.name;

        console.warn(`No type name could be resolved, using constructor name '${typeName}' as a fallback.`);

        const matching_class_entries = [];
        classNames.forEach((value, key) => {
            if (value === typeName) {
                matching_class_entries.push(key);
            }
        });

        if (matching_class_entries.length > 0) {
            console.error(
                `Found a class that resolved to name ${typeName}. className registry might be corrupted, possible reason is multiple versions of the same class being imported.`,
                "offending object:", source,
                "matching class name entries:", matching_class_entries,
                "Note that constructor of offending object does not match any of these classes (!==)"
            );
        }
    }

    if (typeName === undefined) {
        throw new Error(`Failed to resolve type name. No name found in classNames registry and no .typeName field on the constructor {name: '${Klass.name}}'`);
    }

    let serializer = serializers.get(typeName);
    let serializerContext = undefined;

    if (serializer === undefined) {
        serializer = source.toJSON;
        serializerContext = source;
    }

    if (serializer === undefined) {
        throw new Error(`Failed to resolve serializer for object type '${typeName}'. No serializer found in serializers registry and no .toJSON method on object itself`);
    }

    return {
        type: typeName,
        data: serializer.call(serializerContext, source)
    };


}
