import { InstrumentLayer } from './types';
import { WaveformAnalyzer } from './waveformAnalyzer';

export class LayeredAudioEngine {
  private audioContext: AudioContext;
  private masterGain: GainNode;
  private layerNodes: Map<string, {
    buffer?: AudioBuffer;
    source?: AudioBufferSourceNode;
    gain: GainNode;
  }>;
  private isPlaying: boolean = false;
  private startTime: number = 0;
  private pauseTime: number = 0;

  constructor() {
    this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
    this.masterGain = this.audioContext.createGain();
    this.masterGain.connect(this.audioContext.destination);
    this.layerNodes = new Map();
  }

  async loadLayer(layer: InstrumentLayer): Promise<InstrumentLayer> {
    if (!layer.audioUrl) {
      throw new Error('Layer has no audio URL');
    }

    try {
      // Fetch audio data with better error handling
      console.log(`[AUDIO ENGINE] Attempting to load audio from: ${layer.audioUrl}`);
      console.log(`[AUDIO ENGINE] Fetch options:`, { mode: 'cors', credentials: 'omit' });
      
      const response = await fetch(layer.audioUrl, {
        mode: 'cors',
        credentials: 'omit'
      });
      
      console.log(`[AUDIO ENGINE] Fetch response:`, {
        ok: response.ok,
        status: response.status,
        statusText: response.statusText,
        headers: Object.fromEntries(response.headers.entries())
      });
      
      if (!response.ok) {
        throw new Error(`Failed to fetch audio: ${response.status} ${response.statusText}`);
      }
      
      const arrayBuffer = await response.arrayBuffer();
      console.log(`[AUDIO ENGINE] ArrayBuffer size:`, arrayBuffer.byteLength);
      
      if (arrayBuffer.byteLength === 0) {
        throw new Error('Received empty audio data');
      }
      
      // Decode audio data
      console.log(`[AUDIO ENGINE] Attempting to decode audio data...`);
      const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
      console.log(`[AUDIO ENGINE] Successfully decoded audio:`, {
        duration: audioBuffer.duration,
        channels: audioBuffer.numberOfChannels,
        sampleRate: audioBuffer.sampleRate
      });
      
      // Generate waveform data
      const waveformData = WaveformAnalyzer.generateWaveformData(audioBuffer, 400);
      
      // Create gain node for this layer
      const gainNode = this.audioContext.createGain();
      gainNode.connect(this.masterGain);
      gainNode.gain.setValueAtTime(layer.volume, this.audioContext.currentTime);
      
      // Store layer nodes
      this.layerNodes.set(layer.id, {
        buffer: audioBuffer,
        gain: gainNode
      });
      
      console.log(`Loaded audio for layer ${layer.instrumentType}`);
      
      // Return updated layer with waveform data
      return {
        ...layer,
        audioBuffer,
        waveformData: waveformData.peaks
      };
    } catch (error) {
      console.error(`[AUDIO ENGINE] Failed to load audio for layer ${layer.id}:`, error);
      console.error(`[AUDIO ENGINE] Error details:`, {
        message: error instanceof Error ? error.message : 'Unknown error',
        stack: error instanceof Error ? error.stack : undefined,
        audioUrl: layer.audioUrl
      });
      throw error;
    }
  }

  async playAll(layers: InstrumentLayer[]): Promise<void> {
    if (this.isPlaying) {
      this.stopAll();
    }

    // Resume audio context if suspended
    if (this.audioContext.state === 'suspended') {
      await this.audioContext.resume();
    }

    // Load any unloaded layers
    for (const layer of layers) {
      if (layer.audioUrl && !this.layerNodes.has(layer.id)) {
        await this.loadLayer(layer);
      }
    }

    // Create and start source nodes for all ready layers
    this.startTime = this.audioContext.currentTime;
    this.pauseTime = 0;

    for (const layer of layers) {
      const layerNode = this.layerNodes.get(layer.id);
      if (layerNode?.buffer && layer.status === 'ready' && !layer.muted) {
        // Create new source node
        const source = this.audioContext.createBufferSource();
        source.buffer = layerNode.buffer;
        source.connect(layerNode.gain);
        
        // Update volume
        layerNode.gain.gain.setValueAtTime(layer.volume, this.audioContext.currentTime);
        
        // Start playback
        source.start(this.startTime);
        
        // Store source reference
        layerNode.source = source;
      }
    }

    this.isPlaying = true;
  }

  stopAll(): void {
    // Stop all source nodes
    for (const [layerId, layerNode] of this.layerNodes) {
      if (layerNode.source) {
        try {
          layerNode.source.stop();
        } catch (e) {
          // Source may already be stopped
        }
        layerNode.source = undefined;
      }
    }

    this.isPlaying = false;
    this.startTime = 0;
    this.pauseTime = 0;
  }

  pauseAll(): void {
    if (!this.isPlaying) return;

    this.pauseTime = this.audioContext.currentTime - this.startTime;
    this.stopAll(); // Stop current sources
  }

  async resumeAll(layers: InstrumentLayer[]): Promise<void> {
    if (this.isPlaying) return;

    // Resume from pause point
    await this.audioContext.resume();
    
    this.startTime = this.audioContext.currentTime;

    for (const layer of layers) {
      const layerNode = this.layerNodes.get(layer.id);
      if (layerNode?.buffer && layer.status === 'ready' && !layer.muted) {
        const source = this.audioContext.createBufferSource();
        source.buffer = layerNode.buffer;
        source.connect(layerNode.gain);
        
        // Update volume
        layerNode.gain.gain.setValueAtTime(layer.volume, this.audioContext.currentTime);
        
        // Start from pause point
        source.start(this.startTime, this.pauseTime);
        layerNode.source = source;
      }
    }

    this.isPlaying = true;
  }

  setMasterVolume(volume: number): void {
    this.masterGain.gain.setValueAtTime(volume, this.audioContext.currentTime);
  }

  setLayerVolume(layerId: string, volume: number): void {
    const layerNode = this.layerNodes.get(layerId);
    if (layerNode) {
      layerNode.gain.gain.setValueAtTime(volume, this.audioContext.currentTime);
    }
  }

  setLayerMute(layerId: string, muted: boolean): void {
    const layerNode = this.layerNodes.get(layerId);
    if (layerNode) {
      layerNode.gain.gain.setValueAtTime(muted ? 0 : 1, this.audioContext.currentTime);
    }
  }

  removeLayer(layerId: string): void {
    const layerNode = this.layerNodes.get(layerId);
    if (layerNode) {
      // Stop and disconnect source if playing
      if (layerNode.source) {
        try {
          layerNode.source.stop();
        } catch (e) {
          // Source may already be stopped
        }
        layerNode.source.disconnect();
      }
      
      // Disconnect gain node
      layerNode.gain.disconnect();
      
      // Remove from map
      this.layerNodes.delete(layerId);
    }
  }

  getCurrentTime(): number {
    if (!this.isPlaying) return this.pauseTime;
    return Math.max(0, this.audioContext.currentTime - this.startTime + this.pauseTime);
  }

  getIsPlaying(): boolean {
    return this.isPlaying;
  }

  // Get duration of longest layer
  getDuration(layers: InstrumentLayer[]): number {
    let maxDuration = 0;
    
    for (const layer of layers) {
      const layerNode = this.layerNodes.get(layer.id);
      if (layerNode?.buffer) {
        maxDuration = Math.max(maxDuration, layerNode.buffer.duration);
      }
    }
    
    return maxDuration;
  }

  // Seek to a specific time
  async seekTo(time: number, layers: InstrumentLayer[]): Promise<void> {
    const wasPlaying = this.isPlaying;
    
    // Stop current playback
    this.stopAll();
    
    // Set pause time to the seek position
    this.pauseTime = time;
    
    // If we were playing, resume from the new position
    if (wasPlaying) {
      await this.resumeAll(layers);
    }
  }

  // Clean up resources
  dispose(): void {
    this.stopAll();
    
    // Disconnect all nodes
    for (const [layerId, layerNode] of this.layerNodes) {
      layerNode.gain.disconnect();
    }
    
    this.masterGain.disconnect();
    this.layerNodes.clear();
    
    // Close audio context
    if (this.audioContext.state !== 'closed') {
      this.audioContext.close();
    }
  }
}