//! Cagire as a CLAP/VST3 plugin via NIH-plug.

mod editor;
mod params;

use std::collections::HashMap;
use std::sync::Arc;

use arc_swap::ArcSwap;
use crossbeam_channel::{bounded, Receiver, Sender};
use nih_plug::prelude::*;
use parking_lot::Mutex;
use rand::rngs::StdRng;
use rand::SeedableRng;
use ringbuf::traits::Producer;

use cagire::engine::{
    parse_midi_command, spawn_analysis_thread, AnalysisHandle, AudioCommand, MidiCommand,
    PatternSnapshot, ScopeBuffer, SeqCommand, SequencerState, SharedSequencerState, SpectrumBuffer,
    StepSnapshot, TickInput,
};
use cagire::model::{Dictionary, Rng, Variables};
use params::CagireParams;

/// Channel bridge between the plugin editor and the audio/sequencer threads.
pub struct PluginBridge {
    pub cmd_tx: Sender<SeqCommand>,
    pub cmd_rx: Receiver<SeqCommand>,
    pub audio_cmd_tx: Sender<AudioCommand>,
    pub audio_cmd_rx: Receiver<AudioCommand>,
    pub shared_state: Arc<ArcSwap<SharedSequencerState>>,
    pub scope_buffer: Arc<ScopeBuffer>,
    pub spectrum_buffer: Arc<SpectrumBuffer>,
    pub sample_registry: ArcSwap<Option<Arc<doux::SampleRegistry>>>,
}

struct PendingNoteOff {
    target_sample: u64,
    channel: u8,
    note: u8,
}

/// NIH-plug plugin implementing sequencer, synthesis, and MIDI I/O.
pub struct CagirePlugin {
    params: Arc<CagireParams>,
    seq_state: Option<SequencerState>,
    engine: Option<doux::Engine>,
    sample_rate: f32,
    prev_beat: f64,
    sample_pos: u64,
    bridge: Arc<PluginBridge>,
    variables: Variables,
    dict: Dictionary,
    rng: Rng,
    cmd_buffer: String,
    audio_buffer: Vec<f32>,
    output_channels: usize,
    scope_extract_buffer: Vec<f32>,
    fft_producer: Option<ringbuf::HeapProd<f32>>,
    _analysis: Option<AnalysisHandle>,
    pending_note_offs: Vec<PendingNoteOff>,
}

impl Default for CagirePlugin {
    fn default() -> Self {
        let variables: Variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
        let dict: Dictionary = Arc::new(Mutex::new(HashMap::new()));
        let rng: Rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));

        let (cmd_tx, cmd_rx) = bounded(64);
        let (audio_cmd_tx, audio_cmd_rx) = bounded(64);
        let bridge = Arc::new(PluginBridge {
            cmd_tx,
            cmd_rx,
            audio_cmd_tx,
            audio_cmd_rx,
            shared_state: Arc::new(ArcSwap::from_pointee(SharedSequencerState::default())),
            scope_buffer: Arc::new(ScopeBuffer::default()),
            spectrum_buffer: Arc::new(SpectrumBuffer::default()),
            sample_registry: ArcSwap::from_pointee(None),
        });

        Self {
            params: Arc::new(CagireParams::default()),
            seq_state: None,
            engine: None,
            sample_rate: 44100.0,
            prev_beat: -1.0,
            sample_pos: 0,
            bridge,
            variables,
            dict,
            rng,
            cmd_buffer: String::with_capacity(256),
            audio_buffer: Vec::new(),
            output_channels: 2,
            scope_extract_buffer: Vec::new(),
            fft_producer: None,
            _analysis: None,
            pending_note_offs: Vec::new(),
        }
    }
}

impl Plugin for CagirePlugin {
    type SysExMessage = ();
    type BackgroundTask = ();

    const NAME: &'static str = "Cagire";
    const VENDOR: &'static str = "Bubobubobubobubo";
    const URL: &'static str = "https://cagire.raphaelforment.fr";
    const EMAIL: &'static str = "raphael.forment@gmail.com";
    const VERSION: &'static str = env!("CARGO_PKG_VERSION");

    const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[
        AudioIOLayout {
            main_input_channels: None,
            main_output_channels: Some(new_nonzero_u32(2)),
            aux_input_ports: &[],
            aux_output_ports: &[new_nonzero_u32(2); 7],
            names: PortNames {
                layout: Some("Multi-output"),
                main_input: None,
                main_output: Some("Orbit 0"),
                aux_inputs: &[],
                aux_outputs: &[
                    "Orbit 1", "Orbit 2", "Orbit 3", "Orbit 4", "Orbit 5", "Orbit 6", "Orbit 7",
                ],
            },
        },
        AudioIOLayout {
            main_input_channels: None,
            main_output_channels: Some(new_nonzero_u32(2)),
            aux_input_ports: &[],
            aux_output_ports: &[],
            names: PortNames {
                layout: Some("Stereo"),
                main_input: None,
                main_output: Some("Output"),
                aux_inputs: &[],
                aux_outputs: &[],
            },
        },
    ];

    const MIDI_INPUT: MidiConfig = MidiConfig::MidiCCs;
    const MIDI_OUTPUT: MidiConfig = MidiConfig::MidiCCs;

    fn params(&self) -> Arc<dyn Params> {
        self.params.clone()
    }

    fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
        editor::create_editor(
            self.params.clone(),
            self.params.editor_state.clone(),
            Arc::clone(&self.bridge),
            Arc::clone(&self.variables),
            Arc::clone(&self.dict),
            Arc::clone(&self.rng),
        )
    }

    fn initialize(
        &mut self,
        audio_io_layout: &AudioIOLayout,
        buffer_config: &BufferConfig,
        _context: &mut impl InitContext<Self>,
    ) -> bool {
        self.sample_rate = buffer_config.sample_rate;
        self.sample_pos = 0;
        self.prev_beat = -1.0;

        let num_aux = audio_io_layout.aux_output_ports.len();
        self.output_channels = 2 + num_aux * 2;

        self.seq_state = Some(SequencerState::new(
            Arc::clone(&self.variables),
            Arc::clone(&self.dict),
            Arc::clone(&self.rng),
            None,
        ));

        let engine = doux::Engine::new_with_channels(
            self.sample_rate,
            self.output_channels,
            64,
        );
        self.bridge
            .sample_registry
            .store(Arc::new(Some(Arc::clone(&engine.sample_registry))));
        self.engine = Some(engine);

        let (fft_producer, analysis_handle) = spawn_analysis_thread(
            self.sample_rate,
            Arc::clone(&self.bridge.spectrum_buffer),
        );
        self.fft_producer = Some(fft_producer);
        self._analysis = Some(analysis_handle);

        // Seed sequencer with persisted project data
        let project = self.params.project.lock().clone();
        for (bank_idx, bank) in project.banks.iter().enumerate() {
            for (pat_idx, pat) in bank.patterns.iter().enumerate() {
                let has_content = pat.steps.iter().any(|s| !s.script.is_empty());
                if !has_content {
                    continue;
                }
                let snapshot = PatternSnapshot {
                    speed: pat.speed,
                    length: pat.length,
                    steps: pat
                        .steps
                        .iter()
                        .take(pat.length)
                        .map(|s| StepSnapshot {
                            active: s.active,
                            script: s.script.clone(),
                            source: s.source,
                        })
                        .collect(),
                    quantization: pat.quantization,
                    sync_mode: pat.sync_mode,
                    follow_up: pat.follow_up,
                };
                let _ = self.bridge.cmd_tx.send(SeqCommand::PatternUpdate {
                    bank: bank_idx,
                    pattern: pat_idx,
                    data: snapshot,
                });
            }
        }

        true
    }

    fn reset(&mut self) {
        self.prev_beat = -1.0;
        self.sample_pos = 0;
        if let Some(engine) = &mut self.engine {
            engine.hush();
        }
    }

    fn process(
        &mut self,
        buffer: &mut Buffer,
        aux: &mut AuxiliaryBuffers,
        context: &mut impl ProcessContext<Self>,
    ) -> ProcessStatus {
        let Some(seq_state) = &mut self.seq_state else {
            return ProcessStatus::Normal;
        };
        let Some(engine) = &mut self.engine else {
            return ProcessStatus::Normal;
        };

        let transport = context.transport();
        let buffer_len = buffer.samples();

        let playing = transport.playing;
        let tempo = transport.tempo.unwrap_or(self.params.tempo.value() as f64);
        let beat = transport.pos_beats().unwrap_or(0.0);
        let quantum = transport
            .time_sig_numerator
            .map(|n| n as f64)
            .unwrap_or(4.0);

        let effective_tempo = if self.params.sync_to_host.value() {
            tempo
        } else {
            self.params.tempo.value() as f64
        };

        let buffer_secs = buffer_len as f64 / self.sample_rate as f64;
        let lookahead_beats = if effective_tempo > 0.0 {
            buffer_secs * effective_tempo / 60.0
        } else {
            0.0
        };
        let lookahead_end = beat + lookahead_beats;

        let engine_time = self.sample_pos as f64 / self.sample_rate as f64;

        // Drain commands from the editor
        let commands: Vec<SeqCommand> = self.bridge.cmd_rx.try_iter().collect();

        let input = TickInput {
            commands,
            playing,
            beat,
            lookahead_end,
            tempo: effective_tempo,
            quantum,
            fill: false,
            nudge_secs: 0.0,
            current_time_us: 0,
            engine_time,
            mouse_x: 0.5,
            mouse_y: 0.5,
            mouse_down: 0.0,
        };

        let output = seq_state.tick(input);

        // Publish snapshot for the editor
        self.bridge
            .shared_state
            .store(Arc::new(output.shared_state));

        // Drain audio commands from the editor (preview, hush, load samples, etc.)
        for audio_cmd in self.bridge.audio_cmd_rx.try_iter() {
            match audio_cmd {
                AudioCommand::Evaluate { ref cmd, time } => {
                    let cmd_ref = match time {
                        Some(t) => {
                            self.cmd_buffer.clear();
                            use std::fmt::Write;
                            let _ = write!(&mut self.cmd_buffer, "{cmd}/time/{t:.6}");
                            self.cmd_buffer.as_str()
                        }
                        None => cmd.as_str(),
                    };
                    engine.evaluate(cmd_ref);
                }
                AudioCommand::Hush => engine.hush(),
                AudioCommand::Panic => engine.panic(),
                AudioCommand::LoadSamples(samples) => {
                    engine.sample_index.extend(samples);
                }
                AudioCommand::LoadSoundfont(path) => {
                    if let Err(e) = engine.load_soundfont(&path) {
                        eprintln!("Failed to load soundfont: {e}");
                    }
                }
            }
        }

        // Drain expired pending note-offs
        self.pending_note_offs.retain(|off| {
            if off.target_sample <= self.sample_pos {
                context.send_event(NoteEvent::NoteOff {
                    timing: 0,
                    voice_id: None,
                    channel: off.channel,
                    note: off.note,
                    velocity: 0.0,
                });
                false
            } else {
                true
            }
        });

        // Feed audio + MIDI commands from sequencer
        for tsc in &output.audio_commands {
            if tsc.cmd.starts_with("/midi/") {
                if let Some((midi_cmd, dur, _delta)) = parse_midi_command(&tsc.cmd) {
                    match midi_cmd {
                        MidiCommand::NoteOn { channel, note, velocity, .. } => {
                            context.send_event(NoteEvent::NoteOn {
                                timing: 0,
                                voice_id: None,
                                channel,
                                note,
                                velocity: velocity as f32 / 127.0,
                            });
                            if let Some(dur) = dur {
                                self.pending_note_offs.push(PendingNoteOff {
                                    target_sample: self.sample_pos
                                        + (dur * self.sample_rate as f64) as u64,
                                    channel,
                                    note,
                                });
                            }
                        }
                        MidiCommand::NoteOff { channel, note, .. } => {
                            context.send_event(NoteEvent::NoteOff {
                                timing: 0,
                                voice_id: None,
                                channel,
                                note,
                                velocity: 0.0,
                            });
                        }
                        MidiCommand::CC { channel, cc, value, .. } => {
                            context.send_event(NoteEvent::MidiCC {
                                timing: 0,
                                channel,
                                cc,
                                value: value as f32 / 127.0,
                            });
                        }
                        MidiCommand::PitchBend { channel, value, .. } => {
                            context.send_event(NoteEvent::MidiPitchBend {
                                timing: 0,
                                channel,
                                value: value as f32 / 16383.0,
                            });
                        }
                        MidiCommand::Pressure { channel, value, .. } => {
                            context.send_event(NoteEvent::MidiChannelPressure {
                                timing: 0,
                                channel,
                                pressure: value as f32 / 127.0,
                            });
                        }
                        MidiCommand::ProgramChange { channel, program, .. } => {
                            context.send_event(NoteEvent::MidiProgramChange {
                                timing: 0,
                                channel,
                                program,
                            });
                        }
                        MidiCommand::Clock { .. }
                        | MidiCommand::Start { .. }
                        | MidiCommand::Stop { .. }
                        | MidiCommand::Continue { .. } => {}
                    }
                }
                continue;
            }
            let cmd_ref = match tsc.time {
                Some(t) => {
                    self.cmd_buffer.clear();
                    use std::fmt::Write;
                    let _ = write!(&mut self.cmd_buffer, "{}/time/{t:.6}", tsc.cmd);
                    self.cmd_buffer.as_str()
                }
                None => &tsc.cmd,
            };
            engine.evaluate(cmd_ref);
        }

        // Process audio block — doux writes interleaved into our buffer
        let total_samples = buffer_len * self.output_channels;
        self.audio_buffer.resize(total_samples, 0.0);
        self.audio_buffer.fill(0.0);
        engine.process_block(&mut self.audio_buffer, &[], &[]);

        // Feed scope and spectrum analysis (orbit 0 only)
        if self.output_channels == 2 {
            self.bridge.scope_buffer.write(&self.audio_buffer);
        } else {
            self.scope_extract_buffer.resize(buffer_len * 2, 0.0);
            for i in 0..buffer_len {
                self.scope_extract_buffer[i * 2] =
                    self.audio_buffer[i * self.output_channels];
                self.scope_extract_buffer[i * 2 + 1] =
                    self.audio_buffer[i * self.output_channels + 1];
            }
            self.bridge.scope_buffer.write(&self.scope_extract_buffer);
        }
        if let Some(producer) = &mut self.fft_producer {
            let stride = self.output_channels;
            for i in 0..buffer_len {
                let left = self.audio_buffer[i * stride];
                let right = self.audio_buffer[i * stride + 1];
                let _ = producer.try_push((left + right) * 0.5);
            }
        }

        // De-interleave doux output into nih-plug channel buffers
        let stride = self.output_channels;
        deinterleave_stereo(&self.audio_buffer, buffer.as_slice(), stride, 0, buffer_len);
        for (aux_idx, aux_buf) in aux.outputs.iter_mut().enumerate() {
            deinterleave_stereo(
                &self.audio_buffer,
                aux_buf.as_slice(),
                stride,
                (aux_idx + 1) * 2,
                buffer_len,
            );
        }

        self.sample_pos += buffer_len as u64;
        self.prev_beat = lookahead_end;

        ProcessStatus::Normal
    }
}

impl ClapPlugin for CagirePlugin {
    const CLAP_ID: &'static str = "com.sova.cagire";
    const CLAP_DESCRIPTION: Option<&'static str> = Some("Forth-based music sequencer");
    const CLAP_MANUAL_URL: Option<&'static str> = Some("https://cagire.raphaelforment.fr");
    const CLAP_SUPPORT_URL: Option<&'static str> = Some("https://cagire.raphaelforment.fr");
    const CLAP_FEATURES: &'static [ClapFeature] = &[
        ClapFeature::Instrument,
        ClapFeature::Synthesizer,
        ClapFeature::Stereo,
        ClapFeature::Surround,
    ];
}

impl Vst3Plugin for CagirePlugin {
    const VST3_CLASS_ID: [u8; 16] = *b"CagireSovaVST3!!";
    const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[
        Vst3SubCategory::Instrument,
        Vst3SubCategory::Synth,
        Vst3SubCategory::Stereo,
        Vst3SubCategory::Surround,
    ];
}

fn deinterleave_stereo(
    interleaved: &[f32],
    out: &mut [&mut [f32]],
    stride: usize,
    ch_offset: usize,
    len: usize,
) {
    let (left, right) = out.split_at_mut(1);
    for (i, (l, r)) in left[0][..len].iter_mut().zip(right[0][..len].iter_mut()).enumerate() {
        *l = interleaved[i * stride + ch_offset];
        *r = interleaved[i * stride + ch_offset + 1];
    }
}

nih_export_clap!(CagirePlugin);
nih_export_vst3!(CagirePlugin);
