mod app;
mod commands;
mod engine;
mod init;
mod input;
mod midi;
mod model;
mod page;
mod services;
mod settings;
mod state;
mod theme;
mod views;
mod widgets;

use std::io;
use std::path::PathBuf;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, Instant};

use clap::Parser;
use crossterm::event::{self, DisableBracketedPaste, EnableBracketedPaste, DisableMouseCapture, EnableMouseCapture, Event};
use crossterm::terminal::{
    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use crossterm::ExecutableCommand;
use ratatui::prelude::CrosstermBackend;
use ratatui::Terminal;

use engine::{build_stream, AudioStreamConfig};
use init::InitArgs;
use input::{handle_key, handle_mouse, InputContext, InputResult};

#[derive(Parser)]
#[command(name = "cagire", version, about = "Forth-based live coding sequencer")]
struct Args {
    /// Directory containing audio samples to load (can be specified multiple times)
    #[arg(short, long)]
    samples: Vec<PathBuf>,

    /// Output audio device (name or index)
    #[arg(short, long)]
    output: Option<String>,

    /// Input audio device (name or index)
    #[arg(short, long)]
    input: Option<String>,

    /// Number of output channels
    #[arg(short, long)]
    channels: Option<u16>,

    /// Audio buffer size in samples
    #[arg(short, long)]
    buffer: Option<u32>,
}

#[cfg(unix)]
fn redirect_stderr() -> Option<std::fs::File> {
    use std::os::fd::FromRawFd;

    let mut fds = [0i32; 2];
    if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
        return None;
    }
    unsafe {
        libc::dup2(fds[1], libc::STDERR_FILENO);
        libc::close(fds[1]);
        libc::fcntl(fds[0], libc::F_SETFL, libc::O_NONBLOCK);
        Some(std::fs::File::from_raw_fd(fds[0]))
    }
}

fn main() -> io::Result<()> {
    #[cfg(unix)]
    let mut stderr_pipe = redirect_stderr();

    #[cfg(unix)]
    engine::realtime::lock_memory();

    let args = Args::parse();

    let b = init::init(InitArgs {
        samples: args.samples,
        output: args.output,
        input: args.input,
        channels: args.channels,
        buffer: args.buffer,
    });

    let mut app = b.app;
    let link = b.link;
    let sequencer = b.sequencer;
    let playing = b.playing;
    let nudge_us = b.nudge_us;
    let metrics = b.metrics;
    let scope_buffer = b.scope_buffer;
    let spectrum_buffer = b.spectrum_buffer;
    let audio_sample_pos = b.audio_sample_pos;
    let sample_rate_shared = b.sample_rate_shared;
    let mut _stream = b.stream;
    let mut _analysis_handle = b.analysis_handle;
    let mut midi_rx = b.midi_rx;
    let mut stream_error_rx = b.stream_error_rx;

    enable_raw_mode()?;
    io::stdout().execute(EnableBracketedPaste)?;
    io::stdout().execute(EnableMouseCapture)?;
    io::stdout().execute(EnterAlternateScreen)?;
    let backend = CrosstermBackend::new(io::stdout());
    let mut terminal = Terminal::new(backend)?;
    terminal.clear()?;

    let mut last_frame = Instant::now();

    loop {
        if app.audio.restart_pending {
            app.audio.restart_pending = false;
            _stream = None;
            _analysis_handle = None;

            let new_audio_rx = sequencer.swap_audio_channel();
            midi_rx = sequencer.swap_midi_channel();

            let new_config = AudioStreamConfig {
                output_device: app.audio.config.output_device.clone(),
                channels: app.audio.config.channels,
                buffer_size: app.audio.config.buffer_size,
                max_voices: app.audio.config.max_voices,
            };

            let (new_error_tx, new_error_rx) = crossbeam_channel::bounded(16);
            stream_error_rx = new_error_rx;

            let mut restart_samples = Vec::new();
            app.audio.config.sample_counts.clear();
            for path in &app.audio.config.sample_paths {
                let index = doux::sampling::scan_samples_dir(path);
                app.audio.config.sample_counts.push(index.len());
                restart_samples.extend(index);
            }

            audio_sample_pos.store(0, Ordering::Relaxed);

            let preload_entries: Vec<(String, std::path::PathBuf)> = restart_samples
                .iter()
                .map(|e| (e.name.clone(), e.path.clone()))
                .collect();

            match build_stream(
                &new_config,
                new_audio_rx,
                Arc::clone(&scope_buffer),
                Arc::clone(&spectrum_buffer),
                Arc::clone(&metrics),
                restart_samples,
                Arc::clone(&audio_sample_pos),
                new_error_tx,
                &app.audio.config.sample_paths,
            ) {
                Ok((new_stream, info, new_analysis, registry)) => {
                    _stream = Some(new_stream);
                    _analysis_handle = Some(new_analysis);
                    app.audio.config.sample_rate = info.sample_rate;
                    app.audio.config.host_name = info.host_name;
                    app.audio.config.channels = info.channels;
                    sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed);
                    app.audio.error = None;
                    app.audio.sample_registry = Some(Arc::clone(&registry));
                    app.ui.set_status("Audio restarted".to_string());

                    if !preload_entries.is_empty() {
                        let sr = info.sample_rate;
                        std::thread::Builder::new()
                            .name("sample-preload".into())
                            .spawn(move || {
                                engine::preload_sample_heads(preload_entries, sr, &registry);
                            })
                            .expect("failed to spawn preload thread");
                    }
                }
                Err(e) => {
                    app.audio.error = Some(e.clone());
                    app.ui.set_status(format!("Audio failed: {e}"));
                }
            }
        }

        while let Ok(err) = stream_error_rx.try_recv() {
            app.ui.flash(&err, 3000, state::FlashKind::Error);
        }

        #[cfg(unix)]
        if let Some(ref mut pipe) = stderr_pipe {
            use std::io::Read;
            let max_len = terminal.size().map(|s| s.width as usize).unwrap_or(80).saturating_sub(16);
            let mut buf = [0u8; 1024];
            while let Ok(n) = pipe.read(&mut buf) {
                if n == 0 {
                    break;
                }
                let text = String::from_utf8_lossy(&buf[..n]);
                for line in text.lines() {
                    let line = line.trim();
                    if !line.is_empty() {
                        let capped = if line.len() > max_len { &line[..max_len] } else { line };
                        app.ui.flash(capped, 5000, state::FlashKind::Error);
                    }
                }
            }
        }

        app.playback.playing = playing.load(Ordering::Relaxed);

        while let Ok(midi_cmd) = midi_rx.try_recv() {
            match midi_cmd {
                engine::MidiCommand::NoteOn {
                    device,
                    channel,
                    note,
                    velocity,
                } => {
                    app.midi.send_note_on(device, channel, note, velocity);
                }
                engine::MidiCommand::NoteOff {
                    device,
                    channel,
                    note,
                } => {
                    app.midi.send_note_off(device, channel, note);
                }
                engine::MidiCommand::CC {
                    device,
                    channel,
                    cc,
                    value,
                } => {
                    app.midi.send_cc(device, channel, cc, value);
                }
                engine::MidiCommand::PitchBend {
                    device,
                    channel,
                    value,
                } => {
                    app.midi.send_pitch_bend(device, channel, value);
                }
                engine::MidiCommand::Pressure {
                    device,
                    channel,
                    value,
                } => {
                    app.midi.send_pressure(device, channel, value);
                }
                engine::MidiCommand::ProgramChange {
                    device,
                    channel,
                    program,
                } => {
                    app.midi.send_program_change(device, channel, program);
                }
                engine::MidiCommand::Clock { device } => app.midi.send_realtime(device, 0xF8),
                engine::MidiCommand::Start { device } => app.midi.send_realtime(device, 0xFA),
                engine::MidiCommand::Stop { device } => app.midi.send_realtime(device, 0xFC),
                engine::MidiCommand::Continue { device } => app.midi.send_realtime(device, 0xFB),
            }
        }

        {
            app.metrics.active_voices = metrics.active_voices.load(Ordering::Relaxed) as usize;
            app.metrics.peak_voices = app.metrics.peak_voices.max(app.metrics.active_voices);
            app.metrics.cpu_load = metrics.load.get_load();
            app.metrics.schedule_depth = metrics.schedule_depth.load(Ordering::Relaxed) as usize;
            app.metrics.scope = scope_buffer.read();
            app.metrics.scope_right = scope_buffer.read_right();
            (app.metrics.peak_left, app.metrics.peak_right) = scope_buffer.peaks();
            app.metrics.spectrum = spectrum_buffer.read();
            app.metrics.nudge_ms = nudge_us.load(Ordering::Relaxed) as f64 / 1000.0;
        }

        app.flush_dirty_patterns(&sequencer.cmd_tx);
        app.flush_dirty_script(&sequencer.cmd_tx);
        app.flush_queued_changes(&sequencer.cmd_tx);

        let had_event = event::poll(Duration::from_millis(
            app.audio.config.refresh_rate.millis(),
        ))?;

        let seq_snapshot = sequencer.snapshot();
        app.metrics.event_count = seq_snapshot.event_count;

        if had_event {
            match event::read()? {
                Event::Key(key) => {
                    let mut ctx = InputContext {
                        app: &mut app,
                        link: &link,
                        snapshot: &seq_snapshot,
                        playing: &playing,
                        audio_tx: &sequencer.audio_tx,
                        seq_cmd_tx: &sequencer.cmd_tx,
                        nudge_us: &nudge_us,
                    };

                    if let InputResult::Quit = handle_key(&mut ctx, key) {
                        break;
                    }
                }
                Event::Mouse(mouse) => {
                    let mut ctx = InputContext {
                        app: &mut app,
                        link: &link,
                        snapshot: &seq_snapshot,
                        playing: &playing,
                        audio_tx: &sequencer.audio_tx,
                        seq_cmd_tx: &sequencer.cmd_tx,
                        nudge_us: &nudge_us,
                    };
                    handle_mouse(&mut ctx, mouse, terminal.get_frame().area());
                }
                Event::Paste(text) => {
                    if matches!(app.ui.modal, state::Modal::Editor) {
                        app.editor_ctx.editor.insert_str(&text);
                        if app.editor_ctx.show_stack {
                            services::stack_preview::update_cache(&app.editor_ctx);
                        }
                    } else if app.page == page::Page::Script && app.script_editor.focused {
                        app.script_editor.editor.insert_str(&text);
                    }
                }
                _ => {}
            }
        }

        state::effects::tick_effects(&mut app.ui, app.page);

        let elapsed = last_frame.elapsed();
        last_frame = Instant::now();

        let effects_active = app.ui.effects.borrow().is_running()
            || app.ui.modal_fx.borrow().is_some()
            || app.ui.title_fx.borrow().is_some()
            || app.ui.nav_fx.borrow().is_some();
        if app.playback.playing || had_event || app.ui.show_title || effects_active || app.ui.show_minimap() {
            if app.ui.show_title {
                app.ui.sparkles.tick(terminal.get_frame().area());
            }
            terminal.draw(|frame| views::render(frame, &app, &link, &seq_snapshot, elapsed))?;
        }
    }

    disable_raw_mode()?;
    io::stdout().execute(DisableMouseCapture)?;
    io::stdout().execute(DisableBracketedPaste)?;
    io::stdout().execute(LeaveAlternateScreen)?;

    sequencer.shutdown();

    Ok(())
}
