import { isDevEnvironment } from "./debug/index.js";
import { INetworkConnection, SendQueue } from "./engine_networking_types.js";
import type { IComponent } from "./engine_types.js";
import { getParam } from "./engine_utils.js";

const debug = getParam("debugautosync");

const $syncerId = Symbol("syncerId");
class ComponentsSyncerManager {
    private _syncers: { [key: string]: ComponentPropertiesSyncer } = {};

    getOrCreateSyncer(comp: IComponent): ComponentPropertiesSyncer | null {
        if (!comp.guid) return null;
        if (this._syncers[comp.guid]) return this._syncers[comp.guid];
        const syncer = new ComponentPropertiesSyncer(comp);
        syncer[$syncerId] = comp.guid;
        this._syncers[syncer[$syncerId]] = syncer;
        return syncer;
    }

    removeSyncer(syncer: ComponentPropertiesSyncer) {
        delete this._syncers[syncer[$syncerId]];
    }
}
const syncerHandler = new ComponentsSyncerManager();

/**
 * Collects and bundles all changes in properties per component in a frame
 */
class ComponentPropertiesSyncer {

    comp: IComponent;

    constructor(comp: IComponent) {
        this.comp = comp;
    }

    // private getters: { [key: string]: Function } = {};
    private hasChanges: boolean = false;
    private changedProperties: { [key: string]: any } = {};

    get networkingKey(): string {
        return this.comp.guid;
    }

    /** is set to true in on receive call to avoid circular sending */
    private _isReceiving: boolean = false;
    private _isInit = false;


    init(comp) {
        if (this._isInit) return;
        this._isInit = true;
        this.comp = comp;
        // console.log("INIT", this.comp.name, this.networkingKey);
        this.comp.context.post_render_callbacks.push(this.onHandleSending);
        this.comp.context.connection.beginListen(this.networkingKey, this.onHandleReceiving);

        const state = this.comp.context.connection.tryGetState(this.comp.guid);
        if (state) this.onHandleReceiving(state);
    }

    destroy() {
        if (!this._isInit) return;
        this.comp.context.post_render_callbacks.splice(this.comp.context.post_render_callbacks.indexOf(this.onHandleSending), 1);
        this.comp.context.connection.stopListen(this.networkingKey, this.onHandleReceiving);
        //@ts-ignore
        this.comp = null;
        this._isInit = false;
    }

    notifyChanged(propertyName: string, value: any) {
        if (this._isReceiving) return;
        if (debug) console.log("Property changed: " + propertyName, value);
        this.hasChanges = true;
        this.changedProperties[propertyName] = value;
    }

    private onHandleSending = () => {
        if (!this.hasChanges) return;
        this.hasChanges = false;
        const net = this.comp.context.connection as INetworkConnection
        if (!net || !net.isConnected || !net.isInRoom) {
            for (const key in this.changedProperties)
                delete this.changedProperties[key];
            return;
        }
        for (const name in this.changedProperties) {
            const value = this.changedProperties[name];
            if (debug) console.log("SEND", this.comp.guid, this.networkingKey);
            net.send(this.networkingKey, { guid: this.comp.guid, property: name, data: value }, SendQueue.Queued);
            delete this.changedProperties[name];
        }
    }

    private onHandleReceiving = (val: { guid: string, property: string, data: any }) => {
        if (debug) console.log("SYNCFIELD RECEIVE", this.comp.name, this.comp.guid, val);
        if (!this._isInit) return;
        if (!this.comp) return;
        // check if this change belongs to this component
        if (val.guid !== this.comp.guid) {
            return;
        }
        try {
            this._isReceiving = true;
            this.comp[val.property] = val.data;
        }
        catch (err) {
            console.error(err);
        }
        finally {
            this._isReceiving = false;
        }
    }
}

function testValueChanged(newValue, previousValue): boolean {
    let valueChanged = previousValue !== newValue;
    if (!valueChanged && newValue && previousValue) {
        // TODO: array are reference types 
        // so we need to copy the previous array if we really want to compare it
        if (Array.isArray(newValue) && Array.isArray(previousValue)) {
            valueChanged = true;
            // if (value.length !== previousValue.length) {
            //     shouldSend = true;
            // }
            // else {
            //     for (let i = 0; i < value.length; i++) {
            //         if (value[i] !== previousValue[i]) {
            //             shouldSend = true;
            //             break;
            //         }
            //     }
            // }
        }
        else if (typeof newValue === "object" && typeof previousValue === "object") {
            valueChanged = true;
            // The following code doesnt work because the object is a reference type
            // To properly detect changes we would have to detect assignments for each property #
            // OR keep a copy of the previous object
            // for (const key of Object.keys(newValue)) {
            //     if (newValue[key] !== previousValue[key]) {
            //         valueChanged = true;
            //         break;
            //     }
            // }
        }
    }
    return valueChanged;
}

const $syncer = Symbol("AutoSyncHandler");
function getSyncer(instance): ComponentPropertiesSyncer | null {
    if (instance[$syncer]) {
        return instance[$syncer];
    }
    const syncer = syncerHandler.getOrCreateSyncer(instance);
    syncer?.init(instance);
    instance[$syncer] = syncer;
    return syncer;
}

function destroySyncer(instance) {
    const syncer = instance[$syncer];
    if (syncer) {
        syncerHandler.removeSyncer(syncer);
        syncer.destroy();
        delete instance[$syncer];
    }
}

export declare type SyncFieldOptions = {
    onPropertyChanged: Function,
};

export declare type FieldChangedCallbackFn = (newValue: any, previousValue: any) => void | boolean | any;

/**
 * Marks a field for automatic network synchronization across connected clients.  
 * When a synced field changes, the new value is automatically broadcast to all users in the room.  
 *
 * Primitives (string, number, boolean) sync automatically.  
 * For arrays/objects, reassign to trigger sync: `this.myArray = this.myArray`  
 *
 * @param onFieldChanged Optional callback when the field changes (locally or from network).  
 * Return `false` to prevent syncing this change to others.
 *
 * @example Basic sync
 * ```ts
 * class MyComponent extends Behaviour {
 *   @syncField() playerScore: number = 0;
 * }
 * ```
 * @example With change callback
 * ```ts
 * class MyComponent extends Behaviour {
 *   @syncField("onHealthChanged") health: number = 100;
 *
 *   onHealthChanged(newValue: number, oldValue: number) {
 *     console.log(`Health: ${oldValue} → ${newValue}`);
 *   }
 * }
 * ```
 * @example Preventing sync (one-way)
 * ```ts
 * class MyComponent extends Behaviour {
 *   @syncField(function(newVal, oldVal) {
 *     // Process incoming value but don't sync our changes
 *     return false;
 *   }) serverControlled: string = "";
 * }
 * ```
 * @see {@link serializable} for editor serialization
 * @link https://engine.needle.tools/docs/how-to-guides/networking/
 */
export const syncField = function (onFieldChanged: string | FieldChangedCallbackFn | undefined | null = null) {


    return function (target: any, _propertyKey: string | { name: string }) {
        let propertyKey = "";
        if (typeof _propertyKey === "string") propertyKey = _propertyKey;
        else propertyKey = _propertyKey.name;

        let syncer: ComponentPropertiesSyncer | null = null;

        let fn: Function | undefined = undefined;
        if (typeof onFieldChanged === "string")
            fn = target[onFieldChanged];
        else if (typeof onFieldChanged === "function") {
            fn = onFieldChanged;
        }

        if (fn == undefined) {
            if (isDevEnvironment() || debug) {
                if (onFieldChanged != undefined)
                    console.warn("syncField: no callback function found for property \"" + propertyKey + "\"", "\"" + onFieldChanged + "\"");
            }
        }

        const t = target;
        const internalAwake = t.__internalAwake;
        if (typeof internalAwake !== "function") {
            if (debug || isDevEnvironment())
                console.error("@syncField can currently only used on Needle Engine Components, custom object of type \"" + target?.constructor?.name + "\" is not supported", target);
            return;
        }
        if (debug)
            console.log(propertyKey);
        const backingFieldName = Symbol(propertyKey);

        t.__internalAwake = function () {
            if (this[backingFieldName] !== undefined) {
                return;
            }
            this[backingFieldName] = this[propertyKey];

            syncer = syncerHandler.getOrCreateSyncer(this);

            const desc = Object.getOwnPropertyDescriptor(this, propertyKey);
            if (desc?.set === undefined) {
                let invokingCallback = false;
                Object.defineProperty(this, propertyKey, {
                    set: function (value) {
                        const oldValue = this[backingFieldName];
                        this[backingFieldName] = value;
                        // Prevent recursive calls when object is assigned in callback
                        if (invokingCallback) {
                            if (isDevEnvironment() || debug)
                                console.warn("Recursive call detected", propertyKey);
                            return;
                        }
                        invokingCallback = true;
                        try {
                            const valueChanged = testValueChanged(value, oldValue);
                            if (debug) console.log("SyncField assignment", propertyKey, "changed?", valueChanged, value, fn);
                            if (valueChanged) {
                                const res = fn?.call(this, value, oldValue);
                                if (res !== false) {
                                    getSyncer(this)?.notifyChanged(propertyKey, value);
                                }
                            }
                        }
                        finally {
                            invokingCallback = false;
                        }
                    },
                    get: function () {
                        return this[backingFieldName];
                    },
                    configurable: true,
                    enumerable: true,
                });
            }

            syncer?.init(this);
            internalAwake.call(this);
        }

        const internalDestroy = t.__internalDestroy;
        t.__internalDestroy = function () {
            destroySyncer(this);
            internalDestroy.call(this);
        }

    }
}


export declare type SyncOptions = {
    key?: string,
    fieldName?: string,
};

/** experimental - use syncField instead */
export const sync = function (_options?: SyncOptions) {

    return function <T>(target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
        // override awake
        const comp = target as IComponent;
        let syncer: ComponentPropertiesSyncer | null;
        const internalAwake = comp.__internalAwake.bind(comp);
        comp.__internalAwake = function () {
            if (!this.guid) {
                internalAwake?.call(this);
                return;
            }
            internalAwake();
            syncer = syncerHandler.getOrCreateSyncer(this);
            syncer?.init(this);
        }

        // inject getter and setter
        if (!descriptor.get) {
            const previousSetter = descriptor.set;
            const $backingField = Symbol(_propertyKey);
            Object.defineProperty(target, _propertyKey, {
                set: function (value) {
                    this[$backingField] = value;
                    previousSetter?.call(this, value);
                },
                get: function () {
                    return this[$backingField];
                }
            });
            const newDescriptor = Object.getOwnPropertyDescriptor(target, _propertyKey);
            if (newDescriptor) {
                descriptor.set = newDescriptor.set;
                descriptor.get = newDescriptor.get;
            }
        }

        const setter = descriptor.set;
        const getter = descriptor.get;
        let previousValue: T | undefined = undefined;

        if (setter) {
            descriptor.set = function (value: T) {
                const valueChanged = false;

                const syncer = getSyncer(this);

                // test change
                if (syncer && comp.context && comp.context.connection?.isConnected) {
                    testValueChanged(value, previousValue);
                }

                if (valueChanged) {
                    // set the value
                    previousValue = value;
                    setter.call(this, value);
                    syncer?.notifyChanged(_propertyKey, value);
                }
            };
        }
    }

};


