// Streaming audio processor for large files using WebCodecs API
// Handles files > 1GB without loading entire file into memory

export interface DecodedAudioFrame {
  leftChannel: Float32Array;
  rightChannel: Float32Array;
  sampleRate: number;
  timestamp: number;
}

export interface AudioProcessorConfig {
  chunkSize: number;
  bufferSize: number; // seconds of audio to keep in memory
  sampleRate: number;
  channels: number;
}

export class StreamingAudioProcessor {
  private audioContext: AudioContext;
  private decoder: AudioDecoder | null = null;
  private fileBufferQueue: Float32Array[] = [];
  private fileReader: FileReader | null = null;
  private totalSamplesProcessed = 0;
  private sampleRate = 48000;
  private channels = 2;
  private isProcessing = false;
  private abortController: AbortController | null = null;

  constructor(config: AudioProcessorConfig) {
    this.audioContext = new AudioContext();
    this.sampleRate = config.sampleRate;
    this.channels = config.channels;
  }

  async initializeDecoder(file: File): Promise<void> {
    // Detect codec from file type
    const codec = this.getCodecFromFile(file);
    console.log(`Initializing decoder for codec: ${codec}`);
    
    // Check browser support
    if (!this.isWebCodecsSupported()) {
      throw new Error(`WebCodecs AudioDecoder not supported in this browser. codec: ${codec}`);
    }

    // FLAC is not widely supported by WebCodecs, use traditional decode
    if (codec === 'flac') {
      console.warn('FLAC codec not supported by WebCodecs, using traditional decode (may have memory limits)');
      throw new Error('FLAC codec requires traditional decode. For large FLAC files, consider converting to MP3/OGG for streaming support.');
    }

    // Check codec support
    const codecSupport = await AudioDecoder.isConfigSupported({
      codec,
      sampleRate: this.sampleRate,
      numberOfChannels: this.channels
    });

    console.log(`Codec ${codec} support:`, codecSupport);
    
    if (!codecSupport.supported) {
      throw new Error(`Codec ${codec} not supported by this browser`);
    }

    // Create decoder
    this.decoder = new AudioDecoder({
      output: (audioData) => {
        this.handleDecodedFrame(audioData);
      },
      error: (error) => {
        console.error('AudioDecoder error:', error);
        throw new Error(`Decoding failed: ${error}`);
      }
    });

    await this.decoder.configure({
      codec,
      sampleRate: this.sampleRate,
      numberOfChannels: this.channels
    });

    console.log(`AudioDecoder configured successfully for ${codec}`);
  }

  private getCodecFromFile(file: File): string {
    const extension = file.name.split('.').pop()?.toLowerCase();
    const mimeType = file.type;

    // Map file extensions to codec strings
    const codecMap: Record<string, string> = {
      'mp3': 'mp3',
      'flac': 'flac',
      'wav': 'pcm',
      'ogg': 'opus',
      'm4a': 'aac',
      'aac': 'aac'
    };

    // Try extension first
    if (extension && codecMap[extension]) {
      return codecMap[extension];
    }

    // Try MIME type
    if (mimeType) {
      if (mimeType.includes('mp3')) return 'mp3';
      if (mimeType.includes('flac')) return 'flac';
      if (mimeType.includes('wav')) return 'pcm';
      if (mimeType.includes('opus')) return 'opus';
      if (mimeType.includes('aac')) return 'aac';
    }

    throw new Error(`Unsupported file format: ${extension || 'unknown'}`);
  }

  private isWebCodecsSupported(): boolean {
    return 'AudioDecoder' in window && 'EncodedAudioChunk' in window;
  }

  private handleDecodedFrame(audioData: AudioData): void {
    // Convert AudioData to Float32Array channels
    const leftChannel = new Float32Array(audioData.numberOfFrames);
    const rightChannel = new Float32Array(audioData.numberOfFrames);

    audioData.copyTo(leftChannel, { planeIndex: 0 });
    audioData.copyTo(rightChannel, { planeIndex: 1 });

    // Add to buffer queue
    this.fileBufferQueue.push(leftChannel, rightChannel);
    this.totalSamplesProcessed += audioData.numberOfFrames;

    // Limit buffer size to prevent memory growth
    const maxBufferFrames = this.sampleRate * 30; // Keep 30 seconds max
    const currentBufferFrames = this.fileBufferQueue.length / 2 * leftChannel.length;
    
    if (currentBufferFrames > maxBufferFrames) {
      // Remove oldest data to maintain buffer limit
      const framesToRemove = currentBufferFrames - maxBufferFrames;
      const chunksToRemove = Math.ceil(framesToRemove / leftChannel.length) * 2;
      this.fileBufferQueue.splice(0, chunksToRemove);
    }
  }

  async processLargeFile(file: File): Promise<{
    sampleRate: number;
    duration: number;
    totalSamples: number;
    getBufferedSamples: (offset: number, count: number) => { left: Float32Array; right: Float32Array } | null;
  }> {
    console.log(`Starting streaming processing for ${file.name} (${(file.size / (1024 * 1024 * 1024)).toFixed(1)}GB)`);
    
    this.isProcessing = true;
    this.abortController = new AbortController();
    this.fileBufferQueue = [];
    this.totalSamplesProcessed = 0;

    try {
      await this.initializeDecoder(file);
      
      // Get metadata first
      const metadata = await this.extractMetadata(file);
      console.log(`Audio metadata: ${metadata.duration.toFixed(1)}s, sampleRate: ${metadata.sampleRate}`);

      // Process file in chunks
      await this.streamFileChunks(file);

      // Flush remaining decoder data
      if (this.decoder) {
        await this.decoder.flush();
      }

      const duration = this.totalSamplesProcessed / this.sampleRate;
      
      console.log(`✅ Streaming processing complete: ${duration.toFixed(1)}s, ${this.totalSamplesProcessed.toLocaleString()} samples`);

      return {
        sampleRate: this.sampleRate,
        duration,
        totalSamples: this.totalSamplesProcessed,
        getBufferedSamples: (offset: number, count: number) => {
          return this.getSamplesFromBuffer(offset, count);
        }
      };

    } catch (error) {
      console.error('❌ Streaming processing failed:', error);
      throw error;
    } finally {
      this.isProcessing = false;
      this.cleanup();
    }
  }

  private async extractMetadata(file: File): Promise<{ duration: number; sampleRate: number }> {
    // Create temporary audio element to get basic metadata
    return new Promise((resolve, reject) => {
      const audio = new Audio();
      const objectUrl = URL.createObjectURL(file);
      
      audio.addEventListener('loadedmetadata', () => {
        URL.revokeObjectURL(objectUrl);
        resolve({
          duration: audio.duration || 0,
          sampleRate: this.sampleRate
        });
      });

      audio.addEventListener('error', () => {
        URL.revokeObjectURL(objectUrl);
        reject(new Error('Failed to load audio metadata'));
      });

      audio.src = objectUrl;
      audio.load();
    });
  }

  private async streamFileChunks(file: File): Promise<void> {
    const chunkSize = 64 * 1024; // 64KB chunks
    let offset = 0;

    console.log(`Starting chunked reading with ${chunkSize} byte chunks...`);

    while (offset < file.size && !this.abortController?.signal.aborted) {
      const chunk = file.slice(offset, offset + chunkSize);
      
      try {
        const arrayBuffer = await chunk.arrayBuffer();
        
        if (this.decoder && arrayBuffer.byteLength > 0) {
          // Create EncodedAudioChunk
          const encodedChunk = new EncodedAudioChunk({
            type: offset === 0 ? 'key' : 'delta', // First chunk is key frame
            timestamp: (offset / file.size) * 1000000, // microseconds
            data: arrayBuffer
          });

          // Feed to decoder
          this.decoder.decode(encodedChunk);
        }

        offset += arrayBuffer.byteLength;

        // Progress logging for large files
        if (offset % (10 * 1024 * 1024) < chunkSize) { // Every 10MB
          const progress = (offset / file.size) * 100;
          console.log(`Processed ${(offset / (1024 * 1024)).toFixed(1)}MB (${progress.toFixed(1)}%)`);
        }

      } catch (chunkError) {
        console.warn(`Failed to process chunk at offset ${offset}:`, chunkError);
        // Continue with next chunk rather than failing completely
      }
    }
  }

  private getSamplesFromBuffer(offset: number, count: number): { left: Float32Array; right: Float32Array } | null {
    if (this.fileBufferQueue.length === 0) return null;

    const samplesPerChannel = this.fileBufferQueue.length / 2;
    const samplesPerChunk = this.fileBufferQueue[0]?.length || 0;
    const samplesAvailable = Math.min(count, samplesPerChannel * samplesPerChunk);
    
    if (samplesAvailable <= 0) return null;

    const leftResult = new Float32Array(samplesAvailable);
    const rightResult = new Float32Array(samplesAvailable);

    // Copy samples from buffer queue
    let samplesCopied = 0;
    const bufferIndex = Math.floor(offset / samplesPerChunk) * 2;

    for (let i = bufferIndex; i < this.fileBufferQueue.length && samplesCopied < samplesAvailable; i += 2) {
      const leftChunk = this.fileBufferQueue[i];
      const rightChunk = this.fileBufferQueue[i + 1];
      
      const copyLength = Math.min(leftChunk.length, samplesAvailable - samplesCopied);
      
      leftResult.set(leftChunk.subarray(0, copyLength), samplesCopied);
      rightResult.set(rightChunk.subarray(0, copyLength), samplesCopied);
      
      samplesCopied += copyLength;
    }

    return {
      left: leftResult,
      right: rightResult
    };
  }

  cleanup(): void {
    if (this.decoder) {
      try {
        this.decoder.close();
      } catch (e) {
        // Ignore cleanup errors
      }
      this.decoder = null;
    }

    if (this.abortController) {
      this.abortController.abort();
      this.abortController = null;
    }

    this.fileBufferQueue = [];
  }

  abort(): void {
    if (this.abortController) {
      this.abortController.abort();
    }
  }

  get isReady(): boolean {
    return this.decoder?.state === 'configured';
  }

  get progress(): number {
    if (this.fileBufferQueue.length === 0) return 0;
    return this.totalSamplesProcessed / this.sampleRate; // seconds processed
  }
}