/**
 * CloudXR.js Simple Example - WebXR Streaming Application
 *
 * CloudXR streams XR content from a powerful server to lightweight clients (think Netflix for VR/AR).
 * Server does the heavy rendering, client displays video and sends back tracking data.
 *
 * Key Flow:
 * 1. constructor()           - Initialize UI and check browser support
 * 2. connectToCloudXR()      - Connect to server (called on CONNECT button click)
 * 3. initializeWebGL()       - Set up graphics rendering
 * 4. createXRSession()       - Enter VR/AR mode
 * 5. createCloudXRSession()  - Configure CloudXR streaming
 * 6. onXRFrame()            - Render loop: send tracking, receive & display video frames
 */

import { checkCapabilities } from '@helpers/BrowserCapabilities';
import { loadIWERIfNeeded } from '@helpers/LoadIWER';
import { overridePressureObserver } from '@helpers/overridePressureObserver';
import {
  enableLocalStorage,
  getConnectionConfig,
  setupCertificateAcceptanceLink,
} from '@helpers/utils';
import { getOrCreateCanvas, logOrThrow } from '@helpers/WebGlUtils';
import * as CloudXR from '@nvidia/cloudxr';

// Override PressureObserver early to catch errors from buggy browser implementations
overridePressureObserver();

/**
 * CloudXR Client - Main Application Class
 *
 * Architecture: WebXR (hardware access) + WebGL (rendering) + CloudXR (streaming)
 */
class CloudXRClient {
  // UI Elements - Form inputs and display elements
  private startButton: HTMLButtonElement;
  private exitButton: HTMLButtonElement;
  private serverIpInput: HTMLInputElement;
  private portInput: HTMLInputElement;
  private proxyUrlInput: HTMLInputElement;
  private immersiveSelect: HTMLSelectElement;
  private deviceFrameRateSelect: HTMLSelectElement;
  private maxStreamingBitrateMbpsSelect: HTMLSelectElement;
  private proxyDefaultText: HTMLElement;
  private statusMessageBox: HTMLElement;
  private statusMessageText: HTMLElement;
  private perEyeWidthInput: HTMLInputElement;
  private perEyeHeightInput: HTMLInputElement;
  private referenceSpaceSelect: HTMLSelectElement;
  private xrOffsetXInput: HTMLInputElement;
  private xrOffsetYInput: HTMLInputElement;
  private xrOffsetZInput: HTMLInputElement;
  private certAcceptanceLink: HTMLElement;
  private certLink: HTMLAnchorElement;
  private enablePoseSmoothingSelect: HTMLSelectElement;
  private posePredictionFactorInput: HTMLInputElement;
  private posePredictionFactorValue: HTMLElement;

  // Core Session Components
  private xrSession: XRSession | null = null; // WebXR session (hardware access)
  private cloudxrSession: CloudXR.Session | null = null; // CloudXR session (streaming)
  private gl: WebGL2RenderingContext | null = null; // WebGL context (rendering)
  private baseLayer: XRWebGLLayer | null = null; // Bridge between WebXR and WebGL
  private deviceFrameRate: number = 0; // Target frame rate for XR session
  private hasSetTargetFrameRate: boolean = false; // Track if we've set target frame rate

  /**
   * Initialize UI, enable localStorage, and check WebXR support
   */
  constructor() {
    // Get references to all UI elements
    this.startButton = document.getElementById('startButton') as HTMLButtonElement;
    this.exitButton = document.getElementById('exitButton') as HTMLButtonElement;
    this.serverIpInput = document.getElementById('serverIpInput') as HTMLInputElement;
    this.portInput = document.getElementById('portInput') as HTMLInputElement;
    this.proxyUrlInput = document.getElementById('proxyUrl') as HTMLInputElement;
    this.immersiveSelect = document.getElementById('immersive') as HTMLSelectElement;
    this.deviceFrameRateSelect = document.getElementById('deviceFrameRate') as HTMLSelectElement;
    this.maxStreamingBitrateMbpsSelect = document.getElementById(
      'maxStreamingBitrateMbps'
    ) as HTMLSelectElement;
    this.proxyDefaultText = document.getElementById('proxyDefaultText') as HTMLElement;
    this.statusMessageBox = document.getElementById('statusMessageBox') as HTMLElement;
    this.statusMessageText = document.getElementById('statusMessageText') as HTMLElement;
    this.perEyeWidthInput = document.getElementById('perEyeWidth') as HTMLInputElement;
    this.perEyeHeightInput = document.getElementById('perEyeHeight') as HTMLInputElement;
    this.referenceSpaceSelect = document.getElementById('referenceSpace') as HTMLSelectElement;
    this.xrOffsetXInput = document.getElementById('xrOffsetX') as HTMLInputElement;
    this.xrOffsetYInput = document.getElementById('xrOffsetY') as HTMLInputElement;
    this.xrOffsetZInput = document.getElementById('xrOffsetZ') as HTMLInputElement;
    this.certAcceptanceLink = document.getElementById('certAcceptanceLink') as HTMLElement;
    this.certLink = document.getElementById('certLink') as HTMLAnchorElement;
    this.enablePoseSmoothingSelect = document.getElementById(
      'enablePoseSmoothing'
    ) as HTMLSelectElement;
    this.posePredictionFactorInput = document.getElementById(
      'posePredictionFactor'
    ) as HTMLInputElement;
    this.posePredictionFactorValue = document.getElementById(
      'posePredictionFactorValue'
    ) as HTMLElement;

    // Enable localStorage to persist user settings
    enableLocalStorage(this.serverIpInput, 'serverIp');
    enableLocalStorage(this.portInput, 'port');
    enableLocalStorage(this.proxyUrlInput, 'proxyUrl');
    enableLocalStorage(this.immersiveSelect, 'immersiveMode');
    enableLocalStorage(this.deviceFrameRateSelect, 'deviceFrameRate');
    enableLocalStorage(this.maxStreamingBitrateMbpsSelect, 'maxStreamingBitrateMbps');
    enableLocalStorage(this.perEyeWidthInput, 'perEyeWidth');
    enableLocalStorage(this.perEyeHeightInput, 'perEyeHeight');
    enableLocalStorage(this.referenceSpaceSelect, 'referenceSpace');
    enableLocalStorage(this.xrOffsetXInput, 'xrOffsetX');
    enableLocalStorage(this.xrOffsetYInput, 'xrOffsetY');
    enableLocalStorage(this.xrOffsetZInput, 'xrOffsetZ');
    enableLocalStorage(this.enablePoseSmoothingSelect, 'enablePoseSmoothing');
    enableLocalStorage(this.posePredictionFactorInput, 'posePredictionFactor');

    // Update slider value display when it changes
    this.posePredictionFactorInput.addEventListener('input', () => {
      this.posePredictionFactorValue.textContent = this.posePredictionFactorInput.value;
    });
    // Set initial display value
    this.posePredictionFactorValue.textContent = this.posePredictionFactorInput.value;

    // Configure proxy information and port placeholder based on protocol
    if (window.location.protocol === 'https:') {
      this.proxyDefaultText.textContent =
        'Optional: Leave empty for direct WSS connection, or provide URL for proxy routing (e.g., https://proxy.example.com/)';
      this.portInput.placeholder = 'Port (default: 48322, or 443 if proxy URL set)';
    } else {
      this.proxyDefaultText.textContent = 'Not needed for HTTP - uses direct WS connection';
      this.portInput.placeholder = 'Port (default: 49100)';
    }

    this.startButton.addEventListener('click', () => this.connectToCloudXR());
    this.exitButton.addEventListener('click', () => this.xrSession?.end());

    // Set up certificate acceptance link
    setupCertificateAcceptanceLink(
      this.serverIpInput,
      this.portInput,
      this.proxyUrlInput,
      this.certAcceptanceLink,
      this.certLink
    );

    this.checkWebXRSupport();
  }

  /**
   * Check browser support: WebXR, WebGL2, WebRTC, and video frame callbacks
   * Also loads Immersive Web Emulator if needed (for desktop development)
   */
  private async checkWebXRSupport(): Promise<void> {
    const { supportsImmersive, iwerLoaded } = await loadIWERIfNeeded();
    if (!supportsImmersive) {
      this.showStatus('Immersive mode not supported', 'error');
      this.startButton.disabled = true;
      return;
    }

    this.startButton.disabled = true;
    this.startButton.innerHTML = 'CONNECT (checking capabilities)';

    const result = await checkCapabilities();
    if (!result.success) {
      this.showStatus(
        'Browser does not meet required capabilities:\n' + result.failures.join('\n'),
        'error'
      );
      this.startButton.innerHTML = 'CONNECT (capabilities check failed)';
      return;
    }

    if (result.warnings.length > 0) {
      this.showStatus('Performance notice:\n' + result.warnings.join('\n'), 'info');
    } else if (iwerLoaded) {
      // Include IWER status in the final success message
      this.showStatus(
        'CloudXR.js SDK is supported. Ready to connect!\nUsing IWER (Immersive Web Emulator Runtime) - Emulating Meta Quest 3.',
        'info'
      );
    } else {
      this.showStatus('CloudXR.js SDK is supported. Ready to connect!', 'success');
    }

    this.startButton.disabled = false;
    this.startButton.innerHTML = 'CONNECT';
  }

  private showStatus(message: string, type: 'success' | 'error' | 'info'): void {
    this.statusMessageText.textContent = message;
    this.statusMessageBox.className = `status-message-box show ${type}`;
    console[type === 'error' ? 'error' : 'info'](message);
  }

  /**
   * Main connection flow - orchestrates WebGL, WebXR, and CloudXR setup
   * Steps: Read config → Initialize WebGL → Create XR session → Connect to CloudXR server
   */
  private async connectToCloudXR(): Promise<void> {
    // Read configuration from UI form
    const serverIp = this.serverIpInput.value.trim() || 'localhost';

    // Determine default port based on connection type and proxy usage
    const useSecureConnection = window.location.protocol === 'https:';
    const portValue = parseInt(this.portInput.value, 10);
    const proxyUrl = this.proxyUrlInput.value;
    const hasProxy = proxyUrl.trim().length > 0;

    let defaultPort = 49100; // HTTP default (direct CloudXR Runtime connection)
    if (useSecureConnection) {
      defaultPort = hasProxy ? 443 : 48322; // HTTPS with proxy → 443, HTTPS without → 48322
    }

    const port = portValue || defaultPort;
    const perEyeWidth = parseInt(this.perEyeWidthInput.value, 10) || 2048;
    const perEyeHeight = parseInt(this.perEyeHeightInput.value, 10) || 1792;
    const deviceFrameRate = parseInt(this.deviceFrameRateSelect.value, 10);
    const maxStreamingBitrateKbps = parseInt(this.maxStreamingBitrateMbpsSelect.value, 10) * 1000;
    const immersiveMode = this.immersiveSelect.value as 'ar' | 'vr';
    const referenceSpaceType = this.referenceSpaceSelect.value as XRReferenceSpaceType;
    const xrOffsetX = (parseFloat(this.xrOffsetXInput.value) || 0) / 100; // cm to meters
    const xrOffsetY = (parseFloat(this.xrOffsetYInput.value) || 0) / 100;
    const xrOffsetZ = (parseFloat(this.xrOffsetZInput.value) || 0) / 100;

    try {
      this.startButton.disabled = true;
      this.startButton.innerHTML = 'CONNECT (connecting)';
      this.showStatus(`Connecting to Server ${serverIp}:${port}...`, 'info');

      // Initialize WebGL, WebXR session, and reference space
      await this.initializeWebGL();
      await this.createXRSession(immersiveMode, deviceFrameRate);

      let referenceSpace = await this.getReferenceSpace(referenceSpaceType);
      if (xrOffsetX !== 0 || xrOffsetY !== 0 || xrOffsetZ !== 0) {
        const offsetTransform = new XRRigidTransform(
          { x: xrOffsetX, y: xrOffsetY, z: xrOffsetZ },
          { x: 0, y: 0, z: 0, w: 1 }
        );
        referenceSpace = referenceSpace.getOffsetReferenceSpace(offsetTransform);
      }

      // Create CloudXR session and connect to server
      await this.createCloudXRSession(
        serverIp,
        port,
        proxyUrl,
        perEyeWidth,
        perEyeHeight,
        maxStreamingBitrateKbps,
        referenceSpace
      );

      this.cloudxrSession!.connect();
      this.startButton.innerHTML = 'CONNECT (waiting for streaming)';
      this.showStatus(`Connected to Server ${serverIp}:${port}...`, 'info');
    } catch (error) {
      this.showStatus(`Connection failed: ${error}`, 'error');
      this.startButton.disabled = false;
      this.startButton.innerHTML = 'CONNECT';

      if (this.xrSession) {
        try {
          await this.xrSession.end();
        } catch (endError) {
          console.error('Error ending XR session during cleanup:', endError);
          this.clearSessionReferences();
        }
      } else {
        this.clearSessionReferences();
      }
    }
  }

  /**
   * Initialize WebGL2 context for rendering (high-performance, XR-compatible)
   */
  private async initializeWebGL(): Promise<void> {
    const webglCanvas = getOrCreateCanvas('webglCanvas');
    const gl = webglCanvas.getContext('webgl2', {
      alpha: true,
      depth: true,
      stencil: false,
      desynchronized: false,
      antialias: false, // No antialiasing (video already rendered)
      failIfMajorPerformanceCaveat: true,
      powerPreference: 'high-performance', // Use discrete GPU if available
      premultipliedAlpha: false,
      preserveDrawingBuffer: false,
    }) as WebGL2RenderingContext;

    if (!gl) throw new Error('Failed to create WebGL2 context');

    await gl.makeXRCompatible(); // Required before using with XRWebGLLayer
    this.gl = gl;
    logOrThrow('Creating WebGL context', this.gl);
  }

  /**
   * Create WebXR session, XRWebGLLayer, and start render loop
   */
  private async createXRSession(
    immersiveMode: 'ar' | 'vr',
    deviceFrameRate: number
  ): Promise<void> {
    const mode = immersiveMode === 'vr' ? 'immersive-vr' : 'immersive-ar';
    const options = {
      requiredFeatures: ['local-floor'],
      optionalFeatures: ['hand-tracking', 'high-fixed-foveation-level'],
    };

    // Try requested mode, fallback to alternative if unsupported
    try {
      this.xrSession = await navigator.xr!.requestSession(mode, options);
    } catch (error) {
      console.warn(`${mode} session failed, trying alternative:`, error);
      const altMode = immersiveMode === 'vr' ? 'immersive-ar' : 'immersive-vr';
      this.xrSession = await navigator.xr!.requestSession(altMode, options);
    }

    // Create XRWebGLLayer - provides framebuffer for CloudXR to render into
    this.baseLayer = new XRWebGLLayer(this.xrSession, this.gl!, {
      alpha: true,
      antialias: false,
      depth: true,
      framebufferScaleFactor: 1.2,
      ignoreDepthValues: false,
      stencil: false,
    });

    // Store frame rate for later use in render loop
    this.deviceFrameRate = deviceFrameRate;
    this.hasSetTargetFrameRate = false;

    this.xrSession.updateRenderState({ baseLayer: this.baseLayer });
    this.xrSession.addEventListener('end', () => this.handleXRSessionEnd());
    this.xrSession.requestAnimationFrame(this.onXRFrame.bind(this));
  }

  /**
   * Get XR reference space with fallbacks
   * Reference space types: 'local-floor' (room-scale), 'local' (seated), 'viewer' (head-locked)
   */
  private async getReferenceSpace(
    referenceSpaceType: XRReferenceSpaceType
  ): Promise<XRReferenceSpace> {
    try {
      return await this.xrSession!.requestReferenceSpace(referenceSpaceType);
    } catch (error) {
      console.warn(`'${referenceSpaceType}' not supported, trying fallbacks...`);
      try {
        return await this.xrSession!.requestReferenceSpace('local-floor');
      } catch {
        try {
          return await this.xrSession!.requestReferenceSpace('local');
        } catch {
          return await this.xrSession!.requestReferenceSpace('viewer');
        }
      }
    }
  }

  /**
   * Configure CloudXR session and set up event handlers
   * Establishes WebRTC connection, receives video stream, sends tracking data
   */
  private async createCloudXRSession(
    serverIp: string,
    port: number,
    proxyUrl: string,
    perEyeWidth: number,
    perEyeHeight: number,
    maxStreamingBitrateKbps: number,
    referenceSpace: XRReferenceSpace
  ): Promise<void> {
    const connectionConfig = getConnectionConfig(serverIp, port, proxyUrl);

    const sessionOptions: CloudXR.SessionOptions = {
      serverAddress: connectionConfig.serverIP,
      serverPort: connectionConfig.port,
      useSecureConnection: connectionConfig.useSecureConnection,
      gl: this.gl!,
      perEyeWidth, // Stream resolution: width = perEyeWidth * 2 (side-by-side)
      perEyeHeight, // Stream resolution: height = perEyeHeight * 9/4 (includes metadata)
      referenceSpace,
      deviceFrameRate: parseInt(this.deviceFrameRateSelect.value, 10),
      maxStreamingBitrateKbps,
      enablePoseSmoothing: this.enablePoseSmoothingSelect.value === 'true',
      posePredictionFactor: parseFloat(this.posePredictionFactorInput.value),
      telemetry: {
        enabled: true,
        appInfo: { version: '6.0.0-beta', product: 'CloudXR.js WebGL Example' },
      },
    };

    const delegates: CloudXR.SessionDelegates = {
      onStreamStarted: () => {
        console.log('CloudXR stream started');
        this.startButton.innerHTML = 'CONNECT (streaming)';
        this.exitButton.style.display = 'block';
        this.showStatus('Streaming started!', 'success');
      },
      onStreamStopped: (error?: Error) => {
        if (error) {
          console.error('Stream stopped with error:', error);
          this.showStatus(`Stream stopped: ${error}`, 'error');
        } else {
          console.log('Stream stopped normally');
          this.showStatus('Stream stopped', 'info');
        }

        if (this.xrSession) {
          this.xrSession
            .end()
            .catch(endError => console.error('Error ending XR session:', endError))
            .finally(() => (this.exitButton.style.display = 'none'));
        } else {
          this.exitButton.style.display = 'none';
        }

        this.startButton.disabled = false;
        this.startButton.innerHTML = 'CONNECT';
      },
      onWebGLStateChangeBegin: () => console.debug('WebGL state change begin'),
      onWebGLStateChangeEnd: () => console.debug('WebGL state change end'),
      onServerMessageReceived: (messageData: Uint8Array) => {
        const messageString = new TextDecoder().decode(messageData);
        console.debug('Server message:', messageString);
      },
    };

    try {
      this.cloudxrSession = CloudXR.createSession(sessionOptions, delegates);
    } catch (error) {
      console.error('Failed to create CloudXR session:', error);
      throw error;
    }
  }

  /**
   * Main render loop - runs every frame (72-120 FPS)
   * Sends tracking data to server, receives video frame, renders to display
   */
  private async onXRFrame(timestamp: DOMHighResTimeStamp, frame: XRFrame): Promise<void> {
    this.xrSession!.requestAnimationFrame(this.onXRFrame.bind(this));

    // Set target frame rate on first frame only
    if (!this.hasSetTargetFrameRate && 'updateTargetFrameRate' in this.xrSession!) {
      this.hasSetTargetFrameRate = true;
      try {
        await this.xrSession!.updateTargetFrameRate(this.deviceFrameRate);
        console.debug(
          `Target frame rate set to ${this.deviceFrameRate}, current: ${this.xrSession!.frameRate}`
        );
      } catch (error) {
        console.error('Failed to set target frame rate:', error);
      }
    }

    if (!this.cloudxrSession) {
      console.debug('Skipping frame, CloudXR session not created yet');
      return;
    }

    if (this.cloudxrSession.state !== CloudXR.SessionState.Connected) {
      console.debug('Skipping frame, session not ready');
      return;
    }

    try {
      // Send tracking (head/hand positions) → Receive video → Render
      this.cloudxrSession.sendTrackingStateToServer(timestamp, frame);
      this.gl!.bindFramebuffer(this.gl!.FRAMEBUFFER, this.baseLayer!.framebuffer);
      this.cloudxrSession.render(timestamp, frame, this.baseLayer!);
    } catch (error) {
      console.error('Error in render frame:', error);
    }
  }

  /**
   * Cleanup when XR session ends (user exits, removes headset, or error occurs)
   */
  private handleXRSessionEnd(): void {
    try {
      if (this.cloudxrSession) {
        this.cloudxrSession.disconnect();
        this.cloudxrSession = null;
      }

      this.clearSessionReferences();

      this.startButton.disabled = false;
      this.startButton.innerHTML = 'CONNECT';
      this.exitButton.style.display = 'none';
    } catch (error) {
      this.showStatus(`Disconnect error: ${error}`, 'error');
    }
  }

  private clearSessionReferences(): void {
    this.baseLayer = null;
    this.xrSession = null;
    this.gl = null;
    this.hasSetTargetFrameRate = false;
  }
}

// Application entry point - wait for DOM to load, then initialize client
document.addEventListener('DOMContentLoaded', () => {
  new CloudXRClient();
});
