import { Color, CompressedTexture, Euler, LinearSRGBColorSpace, Object3D, RGBAFormat, Texture, WebGLRenderTarget } from "three";

import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
import { Behaviour, Component, GameObject } from "../engine-components/Component.js";
import { CallInfo, EventList } from "../engine-components/EventList.js";
import { AssetReference } from "./engine_addressables.js";
import { debugExtension } from "./engine_default_parameters.js";
import { SerializationContext, TypeSerializer } from "./engine_serialization_core.js";
import { RenderTexture } from "./engine_texture.js";
import { IComponent } from "./engine_types.js";
import { resolveUrl } from "./engine_utils.js";
import { RGBAColor } from "./js-extensions/index.js";

// export class SourcePath {
//     src?:string
// };

// class SourcePathSerializer extends TypeSerializer{
//     constructor(){
//         super(SourcePath);
//     }
//     onDeserialize(data: any, _context: SerializationContext) {
//         if(data.src && typeof data.src === "string"){
//             return data.src;
//         }
//     }
//     onSerialize(_data: any, _context: SerializationContext) {

//     }
// }
// new SourcePathSerializer();

class ColorSerializer extends TypeSerializer {
    constructor() {
        super([Color, RGBAColor], "ColorSerializer")
    }
    onDeserialize(data: any): Color | RGBAColor | void {
        if (data === undefined || data === null) return;
        if (data.a !== undefined) {
            return new RGBAColor(data.r, data.g, data.b, data.a);
        }
        else if (data.alpha !== undefined) {
            return new RGBAColor(data.r, data.g, data.b, data.alpha);
        }
        return new Color(data.r, data.g, data.b);
    }
    onSerialize(data: any): any | void {
        if (data === undefined || data === null) return;
        if (data.a !== undefined)
            return { r: data.r, g: data.g, b: data.b, a: data.a }
        else
            return { r: data.r, g: data.g, b: data.b }
    }
}
export const colorSerializer = new ColorSerializer();

class EulerSerializer extends TypeSerializer {
    constructor() {
        super([Euler], "EulerSerializer");
    }
    onDeserialize(data: any, _context: SerializationContext) {
        if (data === undefined || data === null) return undefined;
        if (data.order) {
            return new Euler(data.x, data.y, data.z, data.order);
        }
        else if (data.x != undefined) {
            return new Euler(data.x, data.y, data.z);
        }
        return undefined;
    }
    onSerialize(data: any, _context: SerializationContext) {
        return { x: data.x, y: data.y, z: data.z, order: data.order };
    }
}
export const euler = new EulerSerializer();

declare type ObjectData = {
    node?: number;
    guid?: string;
}
class ObjectSerializer extends TypeSerializer {
    constructor() {
        super(Object3D, "ObjectSerializer");
    }

    onSerialize(data: any, context: SerializationContext) {
        if (context.objectToNode !== undefined && data.uuid) {
            const node = context.objectToNode[data.uuid];
            if (debugExtension)
                console.log(node, data.name, data.uuid);
            return { node: node }
        }
        return undefined;
    }

    onDeserialize(data: ObjectData | string | null, context: SerializationContext) {

        if (typeof data === "string") {
            if (data.endsWith(".glb") || data.endsWith(".gltf")) {
                // If the @serializable([Object3D, AssetReference]) looks like this we don't need to warn here. This is the case e.g. with SyncedCamera referencing a scene
                if (context.serializable instanceof Array) {
                    if (context.serializable.includes(AssetReference)) return undefined;
                }
                if (isDevEnvironment())
                    showBalloonWarning("Detected wrong usage of @serializable with Object3D or GameObject. Instead you should use AssetReference here! Please see the console for details.");
                const scriptname = context.target?.constructor?.name;
                console.warn(`Wrong usage of @serializable detected in your script \"${scriptname}\"\n\nIt looks like you used @serializable(Object3D) or @serializable(GameObject) for a prefab or scene reference which is exported to a separate glTF file.\n\nTo fix this please change your code to:\n\n@serializable(AssetReference)\n${context.path}! : AssetReference;\n\0`);
            }
            // ACTUALLY: this is already handled by the extension_utils where we resolve json pointers recursively
            // if(data.startsWith("/nodes/")){
            //     const node = parseInt(data.substring("/nodes/".length));
            //     if (context.nodeToObject) {
            //         const res = context.nodeToObject[node];
            //         if (debugExtension)
            //             console.log("Deserialized object reference?", data, res, context?.nodeToObject);
            //         if (!res) console.warn("Did not find node: " + data, context.nodeToObject, context.object);
            //         return res;
            //     }
            // }
            return undefined;
        }

        if (data) {
            if (data.node !== undefined && context.nodeToObject) {
                const res = context.nodeToObject[data.node];
                if (debugExtension)
                    console.log("Deserialized object reference?", data, res, context?.nodeToObject);
                if (!res) console.warn("Did not find node: " + data.node, context.nodeToObject, context.object);
                return res;
            }
            else if (data.guid) {
                if (!context.context) {
                    console.error("Missing context");
                    return undefined;
                }
                // it is possible that the object is not yet loaded 
                // e.g. if we have a scene with multiple gltf files and the first gltf references something in the second gltf
                // we need a way to wait for all components to be created before we can resolve those references
                // independent of order of loading
                let res: GameObject | Behaviour | undefined | null = undefined;
                // first try to search in the current gltf scene (if any)
                const gltfScene = context.gltf?.scene;
                if (gltfScene) {
                    res = GameObject.findByGuid(data.guid, gltfScene);
                }
                // if not found, search in the whole scene
                if (!res) {
                    res = GameObject.findByGuid(data.guid, context.context.scene);
                }
                if (!res) {
                    if (isDevEnvironment() || debugExtension)
                        console.warn("Could not resolve object reference", context.path, data, context.target, context.context.scene);
                    data["could_not_resolve"] = true;
                }
                else {
                    if (res && (res as IComponent).isComponent === true) {
                        if(debugExtension) console.warn("Deserialized object reference is a component");
                        res = (res as IComponent).gameObject;
                    }
                    if (debugExtension)
                        console.log("Deserialized object reference?", data, res, context?.nodeToObject);
                }
                return res;
            }
        }
        return undefined;
    }
}
export const objectSerializer = new ObjectSerializer();


class ComponentSerializer extends TypeSerializer {

    constructor() {
        super([Component, Behaviour], "ComponentSerializer");
    }

    onSerialize(data: any, _context: SerializationContext) {
        if (data?.guid) {
            return { guid: data.guid }
        }
        return undefined;
    }

    onDeserialize(data: any, context: SerializationContext) {
        if (data?.guid) {

            // it's a workaround for VolumeParameter having a guid as well. Generally we will probably never have to resolve a component in the scene when it's coming from the persistent asset extension (like in the case for postprocessing volume parameters)
            if (data.___persistentAsset) {
                if(debugExtension) console.log("Skipping component deserialization because it's a persistent asset", data);
                return undefined;
            }

            const currentPath = context.path;
            // TODO: need to serialize some identifier for referenced components as well, maybe just guid?
            // because here the components are created but dont have their former guid assigned
            // and will later in the stack just get a newly generated guid
            if (debugExtension)
                console.log(data.guid, context.root, context.object, context.target);
            // first search within the gltf (e.g. necessary when using AssetReference and loading a gltf without adding it to the scene)
            // if we would search JUST the scene those references would NEVER be resolved
            let res = this.findObjectForGuid(data.guid, context.root);
            if (res) {
                return res;
            }
            if (context.context) {
                // if not found within the gltf use the provided context scene
                // to find references outside
                res = this.findObjectForGuid(data.guid, context.context?.scene);
                if (res) return res;
            }
            if (isDevEnvironment() || debugExtension) {
                console.warn("Could not resolve component reference: \"" + currentPath + "\" using guid " + data.guid, context.target);
            }
            data["could_not_resolve"] = true;
            return undefined;
        }
        // if (data?.node !== undefined && context.nodeToObject) {
        //     return context.nodeToObject[data.node];
        // }
        return undefined;
    }

    findObjectForGuid(guid: string, root: Object3D): any {
        // recursively search root
        // need to check the root object too
        if (root["guid"] === guid) return root;

        const res = GameObject.foreachComponent(root, (c) => {
            if (c.guid === guid) return c;
            return undefined;
        }, false);
        if (res !== undefined)
            return res;

        // if not found, search in children
        for (let i = 0; i < root.children.length; i++) {
            const child = root.children[i];
            const res = this.findObjectForGuid(guid, child);
            if (res) return res;
        }
    }
}
export const componentSerializer = new ComponentSerializer();


declare class EventListData {
    type: string;
    calls: Array<EventListCall>;
}
declare type EventListCall = {
    method: string,
    target: string,
    argument?: any,
    arguments?: Array<any>,
    enabled?: boolean,
}

const $eventListDebugInfo = Symbol("eventListDebugInfo");

class EventListSerializer extends TypeSerializer {
    constructor() {
        super([EventList]);
    }

    onSerialize(_data: EventList<any>, _context: SerializationContext): EventListData | undefined {
        console.log("TODO: SERIALIZE EVENT");
        return undefined;
    }

    onDeserialize(data: EventListData, context: SerializationContext): EventList<any> | undefined | null {
        // TODO: check that we dont accidentally deserialize methods to EventList objects. This is here to make is easy for react-three-fiber to just add props as { () => { } } 
        if (typeof data === "function") {
            const evtList = new EventList([new CallInfo(data, null, [], true)]);
            return evtList;
        }
        else if (data && data.type === "EventList") {
            if (debugExtension)
                console.log("DESERIALIZE EVENT", data);
            const fns = new Array<CallInfo>();
            if (data.calls && Array.isArray(data.calls)) {
                for (const call of data.calls) {
                    if (debugExtension)
                        console.log(call);
                    let target = componentSerializer.findObjectForGuid(call.target, context.root);
                    // if the object is not found in the current glb try find it in the whole scene
                    if (!target && context.context?.scene) {
                        target = componentSerializer.findObjectForGuid(call.target, context.context?.scene);
                    }
                    const hasMethod = call.method?.length > 0;
                    if (target && hasMethod) {
                        const printWarningMethodNotFound = () => {
                            const uppercaseMethodName = call.method[0].toUpperCase() + call.method.slice(1);
                            if (typeof target[uppercaseMethodName] === "function") {
                                console.warn(`EventList method:\nCould not find method ${call.method} on object ${target.name}. Please rename ${call.method} to ${uppercaseMethodName}?\n`, target[uppercaseMethodName], "\n in script: ", target);
                                showBalloonWarning("EventList methods must start with lowercase letter, see console for details");
                                return;
                            }
                            else {
                                console.warn(`EventList method:\nCould not find method ${call.method} on object ${target.name}`, target, typeof target[call.method]);
                            }
                        }
                        const method = target[call.method];
                        if (typeof method !== "function") {
                            let foundMethod = false;
                            let currentPrototype = target;
                            // test if the target method is actually a property setter
                            while (currentPrototype) {
                                const desc = Object.getOwnPropertyDescriptor(currentPrototype, call.method);
                                if (desc && (desc.writable === true || desc.set)) {
                                    foundMethod = true;
                                    break;
                                }
                                currentPrototype = Object.getPrototypeOf(currentPrototype);
                            }
                            if (!foundMethod && (isDevEnvironment() || debugExtension))
                                printWarningMethodNotFound();
                        }
                    }

                    function deserializeArgument(arg: any) {
                        if (typeof arg === "object") {
                            // Try to deserialize the call argument to either a object or a component reference
                            let argRes = objectSerializer.onDeserialize(arg, context);
                            if (!argRes) argRes = componentSerializer.onDeserialize(arg, context);
                            if (argRes) return argRes;
                        }
                        return arg;
                    }

                    if (target) {
                        let args = call.argument;
                        if (args !== undefined) {
                            args = deserializeArgument(args);
                        }
                        else if (call.arguments !== undefined) {
                            args = call.arguments.map(deserializeArgument);
                        }
                        const method = target[call.method];
                        if (method === undefined) {
                            console.warn(`EventList method not found: \"${call.method}\" on ${target?.name}`);
                        }
                        else {
                            if (args !== undefined && !Array.isArray(args)) {
                                args = [args];
                            }
                            // This is the final method we pass to the call info (or undefined if the method couldnt be resolved)
                            // const eventMethod = hasMethod ? this.createEventMethod(target, call.method, args) : undefined;
                            const fn = new CallInfo(target, call.method, args, call.enabled);
                            fns.push(fn);
                        }
                    }
                    else if (isDevEnvironment()) {
                        console.warn(`[Dev] EventList: Could not find event listener in scene (${context.object?.name})`, call);
                    }
                }
            }
            const evt = new EventList(fns);

            if (debugExtension)
                console.log(evt);

            const eventListOwner = context.target;
            if (eventListOwner !== undefined && context.path !== undefined) {
                evt.setEventTarget(context.path, eventListOwner);
            }

            return evt;
        }
        return undefined;
    }

    // private createEventMethod(target: object, methodName: string, args?: any): Function | undefined {

    //     return function (...forwardedArgs: any[]) {
    //         const method = target[methodName];
    //         if (typeof method === "function") {
    //             if (args !== undefined) {
    //                 // we now have support for creating event methods with multiple arguments
    //                 // an argument can not be an array right now - so if we receive an array we assume it's the array of arguments that we want to call the method with
    //                 // this means ["test", true] will invoke the method like this: myFunction("test", true) 
    //                 if (Array.isArray(args))
    //                     method?.call(target, ...args);
    //                 // in any other case (when we just have one argument) we just call the method with the argument
    //                 // we can not use ...args by default becaue that would break string arguments (it would then just use the first character)
    //                 else
    //                     method?.call(target, args);
    //             }
    //             else // support invoking EventList with any number of arguments (if none were declared in unity)
    //                 method?.call(target, ...forwardedArgs);
    //         }
    //         else // the target "method" can be a property too
    //         {
    //             target[methodName] = args;
    //         }
    //     };
    // }
}
export const eventListSerializer = new EventListSerializer();


/** Map<Clone, Original> texture. This is used for compressed textures (or when the GLTFLoader is cloning RenderTextures)
 * It's a weak map so we don't have to worry about memory leaks
 */
const cloneOriginalMap = new WeakMap<Texture, Texture>();
const textureClone = Texture.prototype.clone;
Texture.prototype.clone = function () {
    const clone = textureClone.call(this);
    if (!cloneOriginalMap.has(clone)) {
        cloneOriginalMap.set(clone, this);
    }
    return clone;
}

export class RenderTextureSerializer extends TypeSerializer {
    constructor() {
        super([RenderTexture, WebGLRenderTarget]);
    }

    onSerialize(_data: any, _context: SerializationContext) {
    }

    onDeserialize(data: any, context: SerializationContext) {
        if (data instanceof Texture && context.type === RenderTexture) {
            let tex = data as Texture;
            // If this is a cloned render texture we want to map it back to the original texture
            // See https://linear.app/needle/issue/NE-5530
            if (cloneOriginalMap.has(tex)) {
                const original = cloneOriginalMap.get(tex)!;
                tex = original;
            }
            tex.isRenderTargetTexture = true;
            tex.flipY = true;
            tex.offset.y = 1;
            tex.repeat.y = -1;
            tex.needsUpdate = true;
            // when we have a compressed texture using mipmaps causes error in threejs because the bindframebuffer call will then try to set an array of framebuffers https://linear.app/needle/issue/NE-4294
            tex.mipmaps = [];
            if (tex instanceof CompressedTexture) {
                //@ts-ignore
                tex["isCompressedTexture"] = false;
                //@ts-ignore
                tex.format = RGBAFormat;
            }

            const rt = new RenderTexture(tex.image.width, tex.image.height, {
                colorSpace: LinearSRGBColorSpace,
            });
            rt.texture = tex;
            return rt;
        }
        return undefined;
    }
}
new RenderTextureSerializer();


export class UriSerializer extends TypeSerializer {
    constructor() {
        super([URL]);
    }

    onSerialize(_data: string, _context: SerializationContext) {
        return null;
    }

    onDeserialize(data: string, _context: SerializationContext) {
        if (typeof data === "string" && data.length > 0) {
            return resolveUrl(_context.gltfId, data);
        }
        return undefined;
    }
}
new UriSerializer();