/**
 * CloudXRComponent.tsx - CloudXR WebXR Integration Component
 *
 * This component handles the core CloudXR streaming functionality and WebXR integration.
 * It manages:
 * - CloudXR session lifecycle (creation, connection, disconnection, cleanup)
 * - WebXR session event handling (sessionstart, sessionend)
 * - WebGL state management and render target preservation
 * - Frame-by-frame rendering loop with pose tracking and stream rendering
 * - Server configuration and connection parameters
 * - Status reporting back to parent components
 *
 * The component accepts configuration via props and communicates status changes
 * and disconnect requests through callback props. It integrates with Three.js
 * and React Three Fiber for WebXR rendering while preserving WebGL state
 * for CloudXR's custom rendering pipeline.
 */

import { getConnectionConfig, ConnectionConfiguration, CloudXRConfig } from '@helpers/utils';
import { bindGL } from '@helpers/WebGLStateBinding';
import * as CloudXR from '@nvidia/cloudxr';
import { useThree, useFrame } from '@react-three/fiber';
import { useXR } from '@react-three/xr';
import { useRef, useEffect } from 'react';
import type { WebGLRenderer } from 'three';

interface CloudXRComponentProps {
  config: CloudXRConfig;
  onStatusChange?: (isConnected: boolean, status: string) => void;
  onError?: (error: string) => void;
  onSessionReady?: (session: CloudXR.Session | null) => void;
  onServerAddress?: (address: string) => void;
}

// React component that integrates CloudXR with Three.js/WebXR
// This component handles the CloudXR session lifecycle and render loop
export default function CloudXRComponent({
  config,
  onStatusChange,
  onError,
  onSessionReady,
  onServerAddress,
}: CloudXRComponentProps) {
  const threeRenderer: WebGLRenderer = useThree().gl;
  const { session } = useXR();
  // React reference to the CloudXR session that persists across re-renders.
  const cxrSessionRef = useRef<CloudXR.Session | null>(null);

  // Disable Three.js so it doesn't clear the framebuffer after CloudXR renders.
  threeRenderer.autoClear = false;

  // Access Three.js WebXRManager and WebGL context.
  const gl: WebGL2RenderingContext = threeRenderer.getContext() as WebGL2RenderingContext;

  const trackedGL = bindGL(gl);

  // Set up event listeners in useEffect to add them only once
  useEffect(() => {
    const webXRManager = threeRenderer.xr;

    if (webXRManager) {
      const handleSessionStart = async () => {
        // Explicitly request the desired reference space from the XRSession to avoid
        // inheriting a default 'local-floor' space that could stack with UI offsets.
        let referenceSpace: XRReferenceSpace | null = null;
        try {
          const xrSession: XRSession | null = (webXRManager as any).getSession
            ? (webXRManager as any).getSession()
            : null;
          if (xrSession) {
            if (config.referenceSpaceType === 'auto') {
              const fallbacks: XRReferenceSpaceType[] = [
                'local-floor',
                'local',
                'viewer',
                'unbounded',
              ];
              for (const t of fallbacks) {
                try {
                  referenceSpace = await xrSession.requestReferenceSpace(t);
                  if (referenceSpace) break;
                } catch (_) {}
              }
            } else {
              try {
                referenceSpace = await xrSession.requestReferenceSpace(
                  config.referenceSpaceType as XRReferenceSpaceType
                );
              } catch (error) {
                console.error(
                  `Failed to request reference space '${config.referenceSpaceType}':`,
                  error
                );
              }
            }
          }
        } catch (error) {
          console.error('Failed to request XR reference space:', error);
          referenceSpace = null;
        }

        if (!referenceSpace) {
          // As a last resort, fall back to WebXRManager's current reference space
          referenceSpace = webXRManager.getReferenceSpace();
        }

        if (referenceSpace) {
          // Ensure that the session is not already created.
          if (cxrSessionRef.current) {
            console.error('CloudXR session already exists');
            return;
          }

          const glBinding = webXRManager.getBinding();
          if (!glBinding) {
            console.warn('No WebGL binding found');
          }

          // Apply proxy configuration logic
          let connectionConfig: ConnectionConfiguration;
          try {
            connectionConfig = getConnectionConfig(config.serverIP, config.port, config.proxyUrl);
            onServerAddress?.(connectionConfig.serverIP);
          } catch (error) {
            onStatusChange?.(false, 'Configuration Error');
            onError?.(`Proxy configuration failed: ${error}`);
            return;
          }

          // Apply XR offset if provided in config (meters)
          const offsetX = config.xrOffsetX || 0;
          const offsetY = config.xrOffsetY || 0;
          const offsetZ = config.xrOffsetZ || 0;
          if (offsetX !== 0 || offsetY !== 0 || offsetZ !== 0) {
            const offsetTransform = new XRRigidTransform(
              { x: offsetX, y: offsetY, z: offsetZ },
              { x: 0, y: 0, z: 0, w: 1 }
            );
            referenceSpace = referenceSpace.getOffsetReferenceSpace(offsetTransform);
          }

          // Fill in CloudXR session options.
          const cloudXROptions: CloudXR.SessionOptions = {
            serverAddress: connectionConfig.serverIP,
            serverPort: connectionConfig.port,
            useSecureConnection: connectionConfig.useSecureConnection,
            perEyeWidth: config.perEyeWidth,
            perEyeHeight: config.perEyeHeight,
            gl: gl,
            referenceSpace: referenceSpace,
            deviceFrameRate: config.deviceFrameRate,
            maxStreamingBitrateKbps: config.maxStreamingBitrateMbps * 1000, // Convert Mbps to Kbps
            glBinding: glBinding,
            telemetry: {
              enabled: true,
              appInfo: {
                version: '6.0.0-beta',
                product: 'CloudXR.js React Example',
              },
            },
          };

          // Store the render target and key GL bindings to restore after CloudXR rendering
          const cloudXRDelegates: CloudXR.SessionDelegates = {
            onWebGLStateChangeBegin: () => {
              // Save the current render target before CloudXR changes state
              trackedGL.save();
            },
            onWebGLStateChangeEnd: () => {
              // Restore the tracked GL state to the state before CloudXR rendering.
              trackedGL.restore();
            },
            onStreamStarted: () => {
              console.debug('CloudXR stream started');
              onStatusChange?.(true, 'Connected');
            },
            onStreamStopped: (error?: Error) => {
              if (error) {
                onStatusChange?.(false, 'Error');
                onError?.(`CloudXR session stopped with error: ${error.message}`);
              } else {
                console.debug('CloudXR session stopped');
                onStatusChange?.(false, 'Disconnected');
              }
              // Clear the session reference
              cxrSessionRef.current = null;
              onSessionReady?.(null);
            },
          };

          // Create the CloudXR session.
          let cxrSession: CloudXR.Session;
          try {
            cxrSession = CloudXR.createSession(cloudXROptions, cloudXRDelegates);
          } catch (error) {
            onStatusChange?.(false, 'Session Creation Failed');
            onError?.(`Failed to create CloudXR session: ${error}`);
            return;
          }

          // Store the session in the ref so it persists across re-renders
          cxrSessionRef.current = cxrSession;

          // Notify parent that session is ready
          onSessionReady?.(cxrSession);

          // Start session (synchronous call that initiates connection)
          try {
            cxrSession.connect();
            console.log('CloudXR session connect initiated');
            // Note: The session will transition to Connected state via the onStreamStarted callback
            // Use cxrSession.state to check if streaming has actually started
          } catch (error) {
            onStatusChange?.(false, 'Connection Failed');
            // Report error via callback
            onError?.('Failed to connect CloudXR session');
            // Clean up the failed session
            cxrSessionRef.current = null;
          }
        }
      };

      const handleSessionEnd = () => {
        if (cxrSessionRef.current) {
          cxrSessionRef.current.disconnect();
          cxrSessionRef.current = null;
          onSessionReady?.(null);
        }
      };

      // Add start+end session event listeners to the WebXRManager.
      webXRManager.addEventListener('sessionstart', handleSessionStart);
      webXRManager.addEventListener('sessionend', handleSessionEnd);

      // Cleanup function to remove listeners
      return () => {
        webXRManager.removeEventListener('sessionstart', handleSessionStart);
        webXRManager.removeEventListener('sessionend', handleSessionEnd);
      };
    }
  }, [threeRenderer, config]); // Re-register handlers when renderer or config changes

  // Custom render loop - runs every frame
  useFrame((state, delta) => {
    const webXRManager = threeRenderer.xr;

    if (webXRManager.isPresenting && session) {
      // Access the current WebXR XRFrame
      const xrFrame = state.gl.xr.getFrame();
      if (xrFrame) {
        // Get THREE WebXRManager from the the useFrame state.
        const webXRManager = state.gl.xr;

        if (!cxrSessionRef || !cxrSessionRef.current) {
          console.debug('Skipping frame, no session yet');
          // Clear the framebuffer as we've set autoClear to false.
          threeRenderer.clear();
          return;
        }

        // Get session from reference.
        const cxrSession: CloudXR.Session = cxrSessionRef.current;

        // If the CloudXR session is not connected, skip the frame.
        if (cxrSession.state !== CloudXR.SessionState.Connected) {
          console.debug('Skipping frame, session not connected, state:', cxrSession.state);
          // Clear the framebuffer as we've set autoClear to false.
          threeRenderer.clear();
          return;
        }

        // Get timestamp from useFrame state and convert to milliseconds.
        const timestamp: DOMHighResTimeStamp = state.clock.elapsedTime * 1000;

        // Send the tracking state (including viewer pose and hand/controller data) to the server, this will trigger server-side rendering for frame.
        cxrSession.sendTrackingStateToServer(timestamp, xrFrame);

        // Get the WebXR layer from THREE WebXRManager.
        let layer: XRWebGLLayer = webXRManager.getBaseLayer() as XRWebGLLayer;

        // Render the current streamed CloudXR frame (not the frame that was just sent to the server).
        cxrSession.render(timestamp, xrFrame, layer);
      }
    }
  }, -1000);

  return null;
}

