# Use Node.js LTS Alpine for smaller image size
FROM node:18-alpine

# Set working directory
WORKDIR /app

# Install system dependencies including curl for downloading libraries
RUN apk add --no-cache \
    curl \
    dumb-init \
    && rm -rf /var/cache/apk/*

# Create app user for security
RUN addgroup -g 1001 -S nodejs && \
    adduser -S musicplayer -u 1001 -G nodejs

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production && \
    npm cache clean --force

# Copy application code
COPY . .

# Create necessary directories
RUN mkdir -p /app/public/js /app/public/soundfonts /app/public/music /app/logs

# Download OpenMPT.js library files
RUN curl -L -o /app/public/js/libopenmpt.js \
    https://lib.openmpt.org/libopenmpt/js/libopenmpt.js && \
    curl -L -o /app/public/js/libopenmpt.wasm \
    https://lib.openmpt.org/libopenmpt/js/libopenmpt.wasm

# Download FluidSynth.js library files
RUN curl -L -o /app/public/js/fluidsynth.js \
    https://cdn.jsdelivr.net/npm/fluid-music@0.9.0/dist/fluidsynth.js && \
    curl -L -o /app/public/js/fluidsynth.wasm \
    https://cdn.jsdelivr.net/npm/fluid-music@0.9.0/dist/fluidsynth.wasm

# Download a high-quality SoundFont (FluidR3 GM)
RUN curl -L -o /app/public/soundfonts/default.sf2 \
    https://ftp.osuosl.org/pub/musescore/soundfont/MuseScore_General/MuseScore_General.sf2

# Create AudioWorklet processor file
RUN cat > /app/public/js/audio-worklet-processor.js << 'EOF'
// AudioWorklet Processor for OpenMPT and FluidSynth - Phase 2
// This file runs in the AudioWorklet scope (separate thread)

class FusionAudioProcessor extends AudioWorkletProcessor {
    constructor() {
        super();
        
        this.bufferSize = 1024;
        this.isPlaying = false;
        this.currentTime = 0;
        this.duration = 0;
        this.engineType = null; // 'openmpt' or 'fluidsynth'
        this.audioEngine = null;
        this.sampleRate = 44100;
        this.volume = 1.0;
        
        // FluidSynth-specific properties
        this.fluidSynth = null;
        this.soundFont = null;
        this.midiEvents = [];
        this.currentEventIndex = 0;
        this.startTime = 0;
        this.ticksPerQuarter = 480;
        this.tempo = 500000; // microseconds per quarter note (120 BPM)
        
        // Message handling from main thread
        this.port.onmessage = (event) => {
            this.handleMessage(event.data);
        };
        
        // Initialize audio buffers
        this.audioBuffer = new Float32Array(this.bufferSize * 2); // Stereo
        this.tempBuffer = new Float32Array(this.bufferSize * 2);
    }
    
    handleMessage(data) {
        switch (data.type) {
            case 'init':
                this.sampleRate = data.sampleRate;
                break;
                
            case 'initFluidSynth':
                this.initializeFluidSynth(data.wasmModule, data.soundFontData);
                break;
                
            case 'setEngine':
                this.audioEngine = data.engine;
                this.engineType = data.engineType;
                this.duration = data.duration || 0;
                
                if (data.engineType === 'fluidsynth') {
                    this.midiEvents = data.midiEvents || [];
                    this.ticksPerQuarter = data.ticksPerQuarter || 480;
                    this.tempo = data.tempo || 500000;
                    this.currentEventIndex = 0;
                }
                break;
                
            case 'play':
                this.isPlaying = true;
                this.startTime = currentTime;
                this.currentTime = 0;
                this.currentEventIndex = 0;
                this.sendMessage('playStateChanged', { playing: true });
                break;
                
            case 'pause':
                this.isPlaying = false;
                this.sendMessage('playStateChanged', { playing: false });
                break;
                
            case 'stop':
                this.isPlaying = false;
                this.currentTime = 0;
                this.currentEventIndex = 0;
                
                // Send all notes off for FluidSynth
                if (this.fluidSynth && this.engineType === 'fluidsynth') {
                    for (let channel = 0; channel < 16; channel++) {
                        try {
                            this.fluidSynth.cc(channel, 123, 0); // All notes off
                            this.fluidSynth.cc(channel, 121, 0); // Reset all controllers
                        } catch (error) {
                            // Ignore errors
                        }
                    }
                }
                
                this.audioEngine = null;
                this.sendMessage('playStateChanged', { playing: false, time: 0 });
                break;
                
            case 'seek':
                this.currentTime = data.time;
                if (this.audioEngine && this.engineType === 'openmpt') {
                    try {
                        this.audioEngine.set_position_seconds(data.time);
                    } catch (error) {
                        console.error('Seek error:', error);
                    }
                } else if (this.engineType === 'fluidsynth') {
                    // Reset FluidSynth and find appropriate event index
                    this.seekMIDI(data.time);
                }
                break;
                
            case 'setVolume':
                this.volume = data.volume;
                break;
        }
    }
    
    async initializeFluidSynth(wasmModule, soundFontData) {
        try {
            // Simple FluidSynth mock for now - real implementation would need proper WASM
            this.fluidSynth = {
                init: () => {},
                noteOn: (channel, note, velocity) => {},
                noteOff: (channel, note) => {},
                cc: (channel, controller, value) => {},
                programChange: (channel, program) => {},
                pitchBend: (channel, value) => {},
                render: (buffer, frames) => {
                    // Generate simple sine wave for demonstration
                    for (let i = 0; i < frames * 2; i += 2) {
                        const sample = Math.sin(this.currentTime * 440 * 2 * Math.PI) * 0.1;
                        buffer[i] = sample;     // Left
                        buffer[i + 1] = sample; // Right
                    }
                }
            };
            
            this.sendMessage('fluidsynthReady', { success: true });
            
        } catch (error) {
            this.sendMessage('fluidsynthReady', { success: false, error: error.message });
        }
    }
    
    seekMIDI(targetTime) {
        if (!this.midiEvents.length) return;
        
        // Reset all controllers and notes
        if (this.fluidSynth) {
            for (let channel = 0; channel < 16; channel++) {
                try {
                    this.fluidSynth.cc(channel, 123, 0); // All notes off
                    this.fluidSynth.cc(channel, 121, 0); // Reset all controllers
                } catch (error) {
                    // Ignore errors
                }
            }
        }
        
        // Find the appropriate event index for the target time
        this.currentEventIndex = 0;
        for (let i = 0; i < this.midiEvents.length; i++) {
            if (this.midiEvents[i].time <= targetTime) {
                this.currentEventIndex = i;
            } else {
                break;
            }
        }
        
        // Apply all events up to the seek point (for state consistency)
        for (let i = 0; i <= this.currentEventIndex; i++) {
            const event = this.midiEvents[i];
            if (event.type !== 'noteOn' && event.type !== 'noteOff') {
                this.processMIDIEvent(event);
            }
        }
    }
    
    sendMessage(type, data = {}) {
        this.port.postMessage({ type, ...data });
    }
    
    process(inputs, outputs, parameters) {
        const output = outputs[0];
        
        if (!output || output.length < 2) {
            return true;
        }
        
        const leftChannel = output[0];
        const rightChannel = output[1];
        const frameCount = leftChannel.length;
        
        // Clear output channels
        leftChannel.fill(0);
        rightChannel.fill(0);
        
        if (!this.isPlaying || !this.audioEngine) {
            return true;
        }
        
        try {
            if (this.engineType === 'openmpt' && this.audioEngine) {
                this.processOpenMPT(leftChannel, rightChannel, frameCount);
            } else if (this.engineType === 'fluidsynth' && this.fluidSynth) {
                this.processFluidSynth(leftChannel, rightChannel, frameCount);
            }
            
            // Apply volume
            if (this.volume !== 1.0) {
                for (let i = 0; i < frameCount; i++) {
                    leftChannel[i] *= this.volume;
                    rightChannel[i] *= this.volume;
                }
            }
            
            // Update time and send progress
            const deltaTime = frameCount / this.sampleRate;
            this.currentTime += deltaTime;
            
            // Send time update every ~100ms
            if (Math.floor(this.currentTime * 10) !== Math.floor((this.currentTime - deltaTime) * 10)) {
                this.sendMessage('timeUpdate', { 
                    currentTime: this.currentTime,
                    duration: this.duration 
                });
            }
            
            // Check if track ended
            if (this.duration > 0 && this.currentTime >= this.duration) {
                this.isPlaying = false;
                this.sendMessage('trackEnded');
            }
            
        } catch (error) {
            console.error('Audio processing error:', error);
            this.sendMessage('error', { message: error.message });
        }
        
        return true;
    }
    
    processOpenMPT(leftChannel, rightChannel, frameCount) {
        if (!this.audioEngine || !this.audioEngine.read_interleaved_stereo) {
            return;
        }
        
        try {
            // Get samples from OpenMPT
            const samples = this.audioEngine.read_interleaved_stereo(frameCount);
            
            if (samples && samples.length >= frameCount * 2) {
                // De-interleave stereo samples
                for (let i = 0; i < frameCount; i++) {
                    leftChannel[i] = samples[i * 2] || 0;
                    rightChannel[i] = samples[i * 2 + 1] || 0;
                }
                
                // Update current time from OpenMPT
                if (this.audioEngine.get_position_seconds) {
                    this.currentTime = this.audioEngine.get_position_seconds();
                }
            }
        } catch (error) {
            console.error('OpenMPT processing error:', error);
        }
    }
    
    processFluidSynth(leftChannel, rightChannel, frameCount) {
        if (!this.fluidSynth) {
            return;
        }
        
        try {
            // Process MIDI events that should occur during this buffer
            this.processMIDIEvents(frameCount);
            
            // Generate audio from FluidSynth
            // Ensure buffer is large enough
            if (this.audioBuffer.length < frameCount * 2) {
                this.audioBuffer = new Float32Array(frameCount * 2);
            }
            
            // Render audio samples
            this.fluidSynth.render(this.audioBuffer, frameCount);
            
            // De-interleave samples to output channels
            for (let i = 0; i < frameCount; i++) {
                leftChannel[i] = this.audioBuffer[i * 2] || 0;
                rightChannel[i] = this.audioBuffer[i * 2 + 1] || 0;
            }
            
        } catch (error) {
            console.error('FluidSynth processing error:', error);
        }
    }
    
    processMIDIEvents(frameCount) {
        if (!this.midiEvents.length || !this.fluidSynth) return;
        
        const bufferDuration = frameCount / this.sampleRate;
        const bufferEndTime = this.currentTime + bufferDuration;
        
        // Process all events that should occur during this buffer
        while (this.currentEventIndex < this.midiEvents.length) {
            const event = this.midiEvents[this.currentEventIndex];
            
            if (event.time > bufferEndTime) {
                break; // Event is in the future
            }
            
            this.processMIDIEvent(event);
            this.currentEventIndex++;
        }
    }
    
    processMIDIEvent(event) {
        if (!this.fluidSynth) return;
        
        try {
            switch (event.type) {
                case 'noteOn':
                    this.fluidSynth.noteOn(event.channel, event.note, event.velocity);
                    break;
                    
                case 'noteOff':
                    this.fluidSynth.noteOff(event.channel, event.note);
                    break;
                    
                case 'programChange':
                    this.fluidSynth.programChange(event.channel, event.program);
                    break;
                    
                case 'controlChange':
                    this.fluidSynth.cc(event.channel, event.controller, event.value);
                    break;
                    
                case 'pitchBend':
                    this.fluidSynth.pitchBend(event.channel, event.value);
                    break;
                    
                case 'tempo':
                    this.tempo = event.value;
                    break;
            }
        } catch (error) {
            console.error('MIDI event processing error:', error);
        }
    }
}

// Register the processor
registerProcessor('fusion-audio-processor', FusionAudioProcessor);
EOF

# Set correct permissions
RUN chown -R musicplayer:nodejs /app

# Switch to non-root user
USER musicplayer

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD curl -f http://localhost:3000/health || exit 1

# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]

# Start the application
CMD ["node", "server.js"]
