import { type Constructor } from "./engine_types.js";

export declare type TypeResolver<T> = (data) => Constructor<T> | null;

/** Please use {@link serializable} - this version has a typo and will be removed in future versions */
export const serializeable = function <T>(type?: Constructor<T> | null | Array<Constructor<any> | TypeResolver<T>> | TypeResolver<T>) {
    return serializable(type)
}

/**
 * Marks a field for serialization and editor exposure. Required for fields that reference  
 * other objects, components, or assets. Primitive types (string, number, boolean) work without a type argument.  
 *
 * @param type The constructor type for complex objects. Omit for primitives.
 *
 * @example Primitive types (no type needed)
 * ```ts
 * @serializable()
 * speed: number = 1;
 *
 * @serializable()
 * label: string = "Hello";
 * ```
 * @example Object references
 * ```ts
 * @serializable(Object3D)
 * target: Object3D | null = null;
 *
 * @serializable(Renderer)
 * myRenderer: Renderer | null = null;
 * ```
 * @example Arrays
 * ```ts
 * @serializable([Object3D])
 * waypoints: Object3D[] = [];
 * ```
 * @see {@link syncField} for automatic network synchronization
 * @link https://engine.needle.tools/docs/reference/typescript-decorators.html#serializable
 */
export const serializable = function <T>(type?: Constructor<T> | null | Array<Constructor<any> | TypeResolver<T>> | TypeResolver<T>) {
    if (type === undefined) type = null;
    // for primitive types the serialization handles it without the constructor and just assigns the value
    // if the user still passes in a primitive type constructor we can just throw it away :)
    if (!Array.isArray(type)) {
        type = setNullForPrimitiveTypes(type);
    }
    else {
        for (let i = 0; i < type.length; i++) {
            const entry = type[i];
            type[i] = setNullForPrimitiveTypes(entry);
        }
    }

    return function (_target: any, _propertyKey: string | { name: string }) {
        if (!_target) {
            const propertyName = typeof _propertyKey === 'string' ? _propertyKey : _propertyKey.name;
            console.warn(`@serializable without a target at '${propertyName}'.`);
            return;
        }
        // The _propertyKey parameter is a string in TS4 with experimentalDecorators
        // but a ClassFieldDecoratorContext in TS5 without.
        // Seems when a different TS version is used in VSCode for editor checking, we get errors here
        // if we don't also check for any. See https://github.com/needle-tools/needle-engine-support/issues/179
        if (typeof _propertyKey !== 'string') {
            _propertyKey = _propertyKey.name;
        }

        // this is important so objects with inheritance dont override their serialized type 
        // info if e.g. multiple classes inheriting from the same type implement a member with the same name
        // and both use @serializable() with different types 
        if (!Object.getOwnPropertyDescriptor(_target, '$serializedTypes'))
            _target["$serializedTypes"] = {};

        const types = _target["$serializedTypes"] = _target["$serializedTypes"] || {}
        types[_propertyKey] = type;
    }
}

function setNullForPrimitiveTypes(type) {
    switch (type?.prototype?.constructor?.name) {
        case "Number":
        case "String":
        case "Boolean":
            return null;
    }
    return type;
}


/** @internal */
export const ALL_PROPERTIES_MARKER = "__NEEDLE__ALL_PROPERTIES";


/** @internal @deprecated current not used */
export function allProperties(constructor: Function) {
    constructor[ALL_PROPERTIES_MARKER] = true;
}


/**
 * @internal
 */
export const STRICT_MARKER = "__NEEDLE__STRICT";

/** @deprecated  current not used */
export function strict(constructor: Function) {
    constructor[STRICT_MARKER] = true;
}