import * as flatbuffers from "flatbuffers";
import { Euler, Quaternion, Vector3 } from "three";

import { InstancingUtil } from "../engine/engine_instancing.js";
import { onUpdate } from '../engine/engine_lifecycle_api.js';
import { OwnershipModel, RoomEvents } from "../engine/engine_networking.js"
import { sendDestroyed } from '../engine/engine_networking_instantiate.js';
import { setWorldEuler } from '../engine/engine_three_utils.js';
import * as utils from "../engine/engine_utils.js"
import { registerBinaryType } from '../engine-schemes/schemes.js';
import { SyncedTransformModel } from '../engine-schemes/synced-transform-model.js';
import { Transform } from '../engine-schemes/transform.js';
import { Behaviour, GameObject } from "./Component.js";
import { Rigidbody } from "./RigidBody.js";

const debug = utils.getParam("debugsync");
export const SyncedTransformIdentifier = "STRS";
registerBinaryType(SyncedTransformIdentifier, SyncedTransformModel.getRootAsSyncedTransformModel);

const builder = new flatbuffers.Builder();

/** 
 * Creates a flatbuffer model containing the transform data of a game object. Used by {@link SyncedTransform}
 * @param guid The unique identifier of the object to sync
 * @param b The behavior component containing transform data
 * @param fast Whether to use fast mode synchronization (syncs more frequently)
 * @returns A Uint8Array containing the serialized transform data
 */
export function createTransformModel(guid: string, b: Behaviour, fast: boolean = true): Uint8Array {
    builder.clear();
    const guidObj = builder.createString(guid);
    SyncedTransformModel.startSyncedTransformModel(builder);
    SyncedTransformModel.addGuid(builder, guidObj);
    SyncedTransformModel.addFast(builder, fast);
    const p = b.worldPosition;
    const r = b.worldEuler;
    const s = b.gameObject.scale; // todo: world scale
    // console.log(p, r, s);
    SyncedTransformModel.addTransform(builder, Transform.createTransform(builder, p.x, p.y, p.z, r.x, r.y, r.z, s.x, s.y, s.z));
    const res = SyncedTransformModel.endSyncedTransformModel(builder);
    // SyncedTransformModel.finishSyncedTransformModelBuffer(builder, res);
    builder.finish(res, SyncedTransformIdentifier);
    return builder.asUint8Array();
}


let FAST_ACTIVE_SYNCTRANSFORMS = 0;
let FAST_INTERVAL = 0;
onUpdate((ctx) => {
    const isRunningOnGlitch = ctx.connection.currentServerUrl?.includes("glitch");
    const threshold = isRunningOnGlitch ? 10 : 40;
    FAST_INTERVAL = Math.floor(FAST_ACTIVE_SYNCTRANSFORMS / threshold);
    FAST_ACTIVE_SYNCTRANSFORMS = 0;
    if (debug && FAST_INTERVAL > 0) console.log("Sync Transform Fast Interval", FAST_INTERVAL);
})

/**
 * SyncedTransform synchronizes position and rotation of a GameObject across the network.
 * When users interact with an object (e.g., via {@link DragControls}), they automatically
 * take ownership and their changes are broadcast to other users.
 *
 * **Features:**
 * - Automatic ownership transfer when interacting
 * - Smooth interpolation of remote updates
 * - Physics integration (can override kinematic state)
 * - Fast mode for rapidly moving objects
 *
 * **Requirements:**
 * - Active network connection via {@link SyncedRoom}
 * - Objects must have unique GUIDs (set automatically in Unity/Blender export)
 *
 * **Ownership:**
 * This component uses {@link OwnershipModel} internally to manage object ownership.
 * Only the client that owns an object can send transform updates. Use `requestOwnership()`
 * before modifying the transform, or check `hasOwnership()` to see if you can modify it.
 *
 * **Debug:** Use `?debugsync` URL parameter for logging.
 *
 * @example Basic networked object
 * ```ts
 * // Add to any object you want synced
 * const sync = myObject.addComponent(SyncedTransform);
 * sync.fastMode = true; // For fast-moving objects
 *
 * // Request ownership before modifying
 * sync.requestOwnership();
 * myObject.position.x += 1;
 * ```
 *
 * - Example: https://engine.needle.tools/samples/collaborative-sandbox
 *
 * @summary Synchronizes object transform over the network with ownership management
 * @category Networking
 * @group Components
 * @see {@link SyncedRoom} for room/session management
 * @see {@link OwnershipModel} for ownership management details
 * @see {@link DragControls} for interactive dragging with sync
 * @see {@link Duplicatable} for networked object spawning
 * @link https://engine.needle.tools/docs/networking.html
 */
export class SyncedTransform extends Behaviour {


    // public autoOwnership: boolean = true;
    /** When true, overrides physics behavior when this object is owned by the local user */
    public overridePhysics: boolean = true

    /** Whether to smoothly interpolate position changes when receiving updates */
    public interpolatePosition: boolean = true;

    /** Whether to smoothly interpolate rotation changes when receiving updates */
    public interpolateRotation: boolean = true;

    /** When true, sends updates at a higher frequency, useful for fast-moving objects */
    public fastMode: boolean = false;

    /** When true, notifies other clients when this object is destroyed */
    public syncDestroy: boolean = false;

    // private _state!: SyncedTransformModel;
    private _model: OwnershipModel | null = null;
    private _needsUpdate: boolean = true;
    private rb: Rigidbody | null = null;
    private _wasKinematic: boolean | undefined = false;
    private _receivedDataBefore: boolean = false;

    private _targetPosition!: Vector3;
    private _targetRotation!: Quaternion;

    private _receivedFastUpdate: boolean = false;
    private _shouldRequestOwnership: boolean = false;

    /**
     * Requests ownership of this object on the network.
     * You need to be connected to a room for this to work.
     * Call this before modifying the object's transform to ensure your changes are synchronized.
     *
     * @example
     * ```ts
     * // Request ownership before modifying
     * syncedTransform.requestOwnership();
     * this.gameObject.position.y += 1;
     * ```
     * @see {@link OwnershipModel.requestOwnership} for more details
     */
    public requestOwnership() {
        if (debug)
            console.log("Request ownership");
        if (!this._model) {
            this._shouldRequestOwnership = true;
            this._needsUpdate = true;
        }
        else
            this._model.requestOwnership();
    }

    /**
     * Free ownership of this object on the network.
     * You need to be connected to a room for this to work.
     * This will also be called automatically when the component is disabled.
     * Call this when you're done modifying an object to allow other users to interact with it.
     * @see {@link OwnershipModel.freeOwnership} for more details
     */
    public freeOwnership() {
        this._model?.freeOwnership();
    }

    /**
     * Checks if this client has ownership of the object.
     * @returns `true` if this client has ownership, `false` if not, `undefined` if ownership state is unknown
     * @see {@link OwnershipModel.hasOwnership} for more details
     */
    public hasOwnership(): boolean | undefined {
        return this._model?.hasOwnership ?? undefined;
    }

    /**
     * Checks if the object is owned by any client (local or remote).
     * @returns `true` if the object is owned, `false` if not, `undefined` if ownership state is unknown
     * @see {@link OwnershipModel.isOwned} for more details
     */
    public isOwned(): boolean | undefined {
        return this._model?.isOwned;
    }

    private joinedRoomCallback: any = null;
    private receivedDataCallback: any = null;

    /** @internal */
    awake() {
        if (debug)
            console.log("new instance", this.guid, this);
        this._receivedDataBefore = false;
        this._targetPosition = new Vector3();
        this._targetRotation = new Quaternion();

        // sync instantiate issue was because they shared the same last pos vector!
        this.lastPosition = new Vector3();
        this.lastRotation = new Quaternion();
        this.lastScale = new Vector3();

        this.rb = GameObject.getComponentInChildren(this.gameObject, Rigidbody);
        if (this.rb) {
            this._wasKinematic = this.rb.isKinematic;
        }

        this.receivedUpdate = true;
        // this._state = new TransformModel(this.guid, this);
        this._model = new OwnershipModel(this.context.connection, this.guid);
        if (this.context.connection.isConnected) {
            this.tryGetLastState();
        }

        this.joinedRoomCallback = this.tryGetLastState.bind(this);
        this.context.connection.beginListen(RoomEvents.JoinedRoom, this.joinedRoomCallback);
        this.receivedDataCallback = this.onReceivedData.bind(this);
        this.context.connection.beginListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
    }

    /** @internal */
    onDestroy(): void {
        // TODO: can we add a new component for this?! do we really need this?!
        if (this.syncDestroy)
            sendDestroyed(this.guid, this.context.connection);
        this._model = null;
        this.context.connection.stopListen(RoomEvents.JoinedRoom, this.joinedRoomCallback);
        this.context.connection.stopListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
    }

    /**
     * Attempts to retrieve and apply the last known network state for this transform
     */
    private tryGetLastState() {
        const model = this.context.connection.tryGetState(this.guid) as unknown as SyncedTransformModel;
        if (model) this.onReceivedData(model);
    }

    private tempEuler: Euler = new Euler();

    /**
     * Handles incoming network data for this transform
     * @param data The model containing transform information
     */
    private onReceivedData(data: SyncedTransformModel) {
        if (this.destroyed) return;
        if (typeof data.guid === "function" && data.guid() === this.guid) {
            if (debug)
                console.log("new data", this.context.connection.connectionId, this.context.time.frameCount, this.guid, data);
            this.receivedUpdate = true;
            this._receivedFastUpdate = data.fast();
            const transform = data.transform();
            if (transform) {
                InstancingUtil.markDirty(this.gameObject, true);
                const position = transform.position();
                if (position) {
                    if (this.interpolatePosition)
                        this._targetPosition?.set(position.x(), position.y(), position.z());
                    if (!this.interpolatePosition || !this._receivedDataBefore)
                        this.setWorldPosition(position.x(), position.y(), position.z());
                }

                const rotation = transform.rotation();
                if (rotation) {
                    this.tempEuler.set(rotation.x(), rotation.y(), rotation.z());
                    if (this.interpolateRotation) {
                        this._targetRotation.setFromEuler(this.tempEuler);
                    }
                    if (!this.interpolateRotation || !this._receivedDataBefore)
                        setWorldEuler(this.gameObject, this.tempEuler);
                }

                const scale = transform.scale();
                if (scale) {
                    this.gameObject.scale.set(scale.x(), scale.y(), scale.z());
                }
            }
            this._receivedDataBefore = true;

            // if (this.rb && !this._model?.hasOwnership) {
            //     this.rb.setBodyFromGameObject(data.velocity)
            // }
        }
    }

    /** 
     * @internal 
     * Initializes tracking of position and rotation when component is enabled
     */
    onEnable(): void {
        this.lastPosition.copy(this.worldPosition);
        this.lastRotation.copy(this.worldQuaternion);
        this.lastScale.copy(this.gameObject.scale);
        this._needsUpdate = true;
        // console.log("ENABLE", this.guid, this.gameObject.guid, this.lastWorldPos);
        if (this._model) {
            this._model.updateIsOwned();
        }
    }

    /** 
     * @internal 
     * Releases ownership when component is disabled
     */
    onDisable(): void {
        if (this._model)
            this._model.freeOwnership();
    }


    private receivedUpdate = false;
    private lastPosition!: Vector3;
    private lastRotation!: Quaternion;
    private lastScale!: Vector3;

    /** 
     * @internal 
     * Handles transform synchronization before each render frame
     * Sends updates when owner, receives and applies updates when not owner
     */
    onBeforeRender() {
        if (!this.activeAndEnabled || !this.context.connection.isConnected) return;
        // console.log("BEFORE RENDER", this.destroyed, this.guid, this._model?.isOwned, this.name, this.gameObject);

        if (!this.context.connection.isInRoom || !this._model) {
            if (debug)
                console.log("no model or room", this.name, this.guid, this.context.connection.isInRoom);
            return;
        }

        if (this._shouldRequestOwnership) {
            this._shouldRequestOwnership = false;
            this._model.requestOwnership();
        }

        const pos = this.worldPosition;
        const rot = this.worldQuaternion;
        const scale = this.gameObject.scale;
        if (this._model.isOwned && !this.receivedUpdate) {
            const threshold = this._model.hasOwnership || this.fastMode ? .0001 : .001;
            if (pos.distanceTo(this.lastPosition) > threshold ||
                rot.angleTo(this.lastRotation) > threshold ||
                scale.distanceTo(this.lastScale) > threshold) {
                // console.log(worlddiff, worldRot);
                if (!this._model.hasOwnership) {

                    if (debug)
                        console.log(this.guid, "reset because not owned but", this.gameObject.name, this.lastPosition);

                    this.worldPosition = this.lastPosition;
                    pos.copy(this.lastPosition);

                    this.worldQuaternion = this.lastRotation;
                    rot.copy(this.lastRotation);

                    this.gameObject.scale.copy(this.lastScale);

                    InstancingUtil.markDirty(this.gameObject, true);
                    this._needsUpdate = false;
                }
                else {
                    this._needsUpdate = true;
                }
            }
        }
        // else if (this._model.isOwned === false) {
        //     if (!this._didRequestOwnershipOnce && this.autoOwnership) {
        //         this._didRequestOwnershipOnce = true;
        //         this._model.requestOwnershipIfNotOwned();
        //     }
        // }


        if (this._model && !this._model.hasOwnership && this._model.isOwned) {
            if (this._receivedDataBefore) {
                const t = this._receivedFastUpdate || this.fastMode ? .5 : .3; 
                let requireMarkDirty = false;
                if (this.interpolatePosition && this._targetPosition) {
                    const pos = this.worldPosition;
                    pos.lerp(this._targetPosition, t);
                    this.worldPosition = pos;
                    requireMarkDirty = true;
                }
                if (this.interpolateRotation && this._targetRotation) {
                    const rot = this.worldQuaternion;
                    rot.slerp(this._targetRotation, t);
                    this.worldQuaternion = rot;
                    requireMarkDirty = true;
                }
                if (requireMarkDirty)
                    InstancingUtil.markDirty(this.gameObject, true);
            }
        }


        this.receivedUpdate = false;
        this.lastPosition.copy(pos);
        this.lastRotation.copy(rot);
        this.lastScale.copy(scale);


        if (!this._model) return;

        if (!this._model || this._model.hasOwnership === undefined || !this._model.hasOwnership) {
            // if we're not the owner of this synced transform then don't send any data
            return;
        }

        // local user is owner:
        if (this.rb && this.overridePhysics) {
            if (this._wasKinematic !== undefined) {
                if (debug)
                    console.log("reset kinematic", this.rb.name, this._wasKinematic);
                this.rb.isKinematic = this._wasKinematic;
            }

            // TODO: if the SyncedTransform has a dynamic rigidbody we should probably synchronize the Rigidbody's properties (velocity etc)
            // Or the Rigidbody should be synchronized separately in which case the SyncedTransform should be passive
        }

        const updateInterval = 10;
        const fastUpdate = this.rb || this.fastMode;

        if (this._needsUpdate && (updateInterval <= 0 || updateInterval > 0 && this.context.time.frameCount % updateInterval === 0 || fastUpdate)) {

            FAST_ACTIVE_SYNCTRANSFORMS++;
            if (fastUpdate && FAST_INTERVAL > 0 && this.context.time.frameCount % FAST_INTERVAL !== 0) return;

            if (debug) console.debug("[SyncedTransform] Send update", this.context.connection.connectionId, this.guid, this.gameObject.name, this.gameObject.guid);

            this._needsUpdate = false;
            const st = createTransformModel(this.guid, this, fastUpdate ? true : false);
            this.context.connection.sendBinary(st);
        }
    }
}