#![windows_subsystem = "windows"]

use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;

use cagire::block_renderer::BlockCharBackend;
use clap::Parser;
use doux::EngineMetrics;
use eframe::NativeOptions;
use egui_ratatui::RataguiBackend;
use ratatui::Terminal;
use soft_ratatui::embedded_graphics_unicodefonts::{
    mono_10x20_atlas, mono_6x13_atlas, mono_6x13_bold_atlas, mono_6x13_italic_atlas,
    mono_7x13_atlas, mono_7x13_bold_atlas, mono_7x13_italic_atlas, mono_8x13_atlas,
    mono_8x13_bold_atlas, mono_8x13_italic_atlas, mono_9x15_atlas, mono_9x15_bold_atlas,
    mono_9x18_atlas, mono_9x18_bold_atlas,
};
use soft_ratatui::{EmbeddedGraphics, SoftBackend};

use cagire::engine::{
    build_stream, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand, ScopeBuffer,
    SequencerHandle, SpectrumBuffer,
};
use cagire::init::{init, InitArgs};
use cagire::input::{handle_key, handle_mouse, InputContext, InputResult};
use cagire::input_egui::{convert_egui_events, convert_egui_mouse, EguiMouseState};
use cagire::settings::Settings;
use cagire::views;
use crossbeam_channel::Receiver;

#[derive(Parser)]
#[command(name = "cagire-desktop", about = "Cagire desktop application")]
struct Args {
    #[arg(short, long)]
    samples: Vec<std::path::PathBuf>,

    #[arg(short, long)]
    output: Option<String>,

    #[arg(short, long)]
    input: Option<String>,

    #[arg(short, long)]
    channels: Option<u16>,

    #[arg(short, long)]
    buffer: Option<u32>,
}

#[derive(Clone, Copy, PartialEq)]
enum FontChoice {
    Size6x13,
    Size7x13,
    Size8x13,
    Size9x15,
    Size9x18,
    Size10x20,
}

impl FontChoice {
    fn from_setting(s: &str) -> Self {
        match s {
            "6x13" => Self::Size6x13,
            "7x13" => Self::Size7x13,
            "9x15" => Self::Size9x15,
            "9x18" => Self::Size9x18,
            "10x20" => Self::Size10x20,
            _ => Self::Size8x13,
        }
    }

    fn to_setting(self) -> &'static str {
        match self {
            Self::Size6x13 => "6x13",
            Self::Size7x13 => "7x13",
            Self::Size8x13 => "8x13",
            Self::Size9x15 => "9x15",
            Self::Size9x18 => "9x18",
            Self::Size10x20 => "10x20",
        }
    }

    fn label(self) -> &'static str {
        match self {
            Self::Size6x13 => "6x13 (Compact)",
            Self::Size7x13 => "7x13",
            Self::Size8x13 => "8x13 (Default)",
            Self::Size9x15 => "9x15",
            Self::Size9x18 => "9x18",
            Self::Size10x20 => "10x20 (Large)",
        }
    }

    const ALL: [Self; 6] = [
        Self::Size6x13,
        Self::Size7x13,
        Self::Size8x13,
        Self::Size9x15,
        Self::Size9x18,
        Self::Size10x20,
    ];
}

type TerminalType = Terminal<RataguiBackend<BlockCharBackend>>;

fn create_terminal(font: FontChoice) -> TerminalType {
    let (regular, bold, italic) = match font {
        FontChoice::Size6x13 => (
            mono_6x13_atlas(),
            Some(mono_6x13_bold_atlas()),
            Some(mono_6x13_italic_atlas()),
        ),
        FontChoice::Size7x13 => (
            mono_7x13_atlas(),
            Some(mono_7x13_bold_atlas()),
            Some(mono_7x13_italic_atlas()),
        ),
        FontChoice::Size8x13 => (
            mono_8x13_atlas(),
            Some(mono_8x13_bold_atlas()),
            Some(mono_8x13_italic_atlas()),
        ),
        FontChoice::Size9x15 => (mono_9x15_atlas(), Some(mono_9x15_bold_atlas()), None),
        FontChoice::Size9x18 => (mono_9x18_atlas(), Some(mono_9x18_bold_atlas()), None),
        FontChoice::Size10x20 => (mono_10x20_atlas(), None, None),
    };

    let eg = SoftBackend::<EmbeddedGraphics>::new(80, 24, regular, bold, italic);
    let soft = SoftBackend {
        buffer: eg.buffer,
        cursor: eg.cursor,
        cursor_pos: eg.cursor_pos,
        char_width: eg.char_width,
        char_height: eg.char_height,
        blink_counter: eg.blink_counter,
        blinking_fast: eg.blinking_fast,
        blinking_slow: eg.blinking_slow,
        rgb_pixmap: eg.rgb_pixmap,
        always_redraw_list: eg.always_redraw_list,
        raster_backend: BlockCharBackend {
            inner: eg.raster_backend,
        },
    };
    Terminal::new(RataguiBackend::new("cagire", soft)).expect("terminal")
}

struct CagireDesktop {
    app: cagire::app::App,
    terminal: TerminalType,
    link: Arc<LinkState>,
    sequencer: Option<SequencerHandle>,
    playing: Arc<AtomicBool>,
    nudge_us: Arc<AtomicI64>,
    metrics: Arc<EngineMetrics>,
    scope_buffer: Arc<ScopeBuffer>,
    spectrum_buffer: Arc<SpectrumBuffer>,
    audio_sample_pos: Arc<AtomicU64>,
    sample_rate_shared: Arc<AtomicU32>,
    _stream: Option<cpal::Stream>,
    _analysis_handle: Option<AnalysisHandle>,
    midi_rx: Receiver<MidiCommand>,
    stream_error_rx: crossbeam_channel::Receiver<String>,
    current_font: FontChoice,
    zoom_factor: f32,
    fullscreen: bool,
    decorations: bool,
    always_on_top: bool,
    mouse_x: Arc<AtomicU32>,
    mouse_y: Arc<AtomicU32>,
    mouse_down: Arc<AtomicU32>,
    last_frame: std::time::Instant,
    egui_mouse: EguiMouseState,
}

impl CagireDesktop {
    fn new(cc: &eframe::CreationContext<'_>, args: Args) -> Self {
        let b = init(InitArgs {
            samples: args.samples,
            output: args.output,
            input: args.input,
            channels: args.channels,
            buffer: args.buffer,
        });

        let current_font = FontChoice::from_setting(&b.settings.display.font);
        let terminal = create_terminal(current_font);
        let zoom_factor = b.settings.display.zoom_factor;

        cc.egui_ctx.set_visuals(egui::Visuals::dark());
        cc.egui_ctx.set_zoom_factor(zoom_factor);

        Self {
            app: b.app,
            terminal,
            link: b.link,
            sequencer: Some(b.sequencer),
            playing: b.playing,
            nudge_us: b.nudge_us,
            metrics: b.metrics,
            scope_buffer: b.scope_buffer,
            spectrum_buffer: b.spectrum_buffer,
            audio_sample_pos: b.audio_sample_pos,
            sample_rate_shared: b.sample_rate_shared,
            _stream: b.stream,
            _analysis_handle: b.analysis_handle,
            midi_rx: b.midi_rx,
            stream_error_rx: b.stream_error_rx,
            current_font,
            zoom_factor,
            fullscreen: false,
            decorations: true,
            always_on_top: false,
            mouse_x: b.mouse_x,
            mouse_y: b.mouse_y,
            mouse_down: b.mouse_down,
            last_frame: std::time::Instant::now(),
            egui_mouse: EguiMouseState::default(),
        }
    }

    fn handle_audio_restart(&mut self) {
        if !self.app.audio.restart_pending {
            return;
        }

        self.app.audio.restart_pending = false;
        self._stream = None;
        self._analysis_handle = None;

        let Some(ref sequencer) = self.sequencer else {
            return;
        };
        let new_audio_rx = sequencer.swap_audio_channel();
        self.midi_rx = sequencer.swap_midi_channel();

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

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

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

        self.audio_sample_pos.store(0, Ordering::Release);

        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(&self.scope_buffer),
            Arc::clone(&self.spectrum_buffer),
            Arc::clone(&self.metrics),
            restart_samples,
            Arc::clone(&self.audio_sample_pos),
            new_error_tx,
            &self.app.audio.config.sample_paths,
        ) {
            Ok((new_stream, info, new_analysis, registry)) => {
                self._stream = Some(new_stream);
                self._analysis_handle = Some(new_analysis);
                self.app.audio.config.sample_rate = info.sample_rate;
                self.app.audio.config.host_name = info.host_name;
                self.app.audio.config.channels = info.channels;
                self.sample_rate_shared
                    .store(info.sample_rate as u32, Ordering::Relaxed);
                self.app.audio.error = None;
                self.app.audio.sample_registry = Some(std::sync::Arc::clone(&registry));
                self.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 || {
                            cagire::engine::preload_sample_heads(preload_entries, sr, &registry);
                        })
                        .expect("failed to spawn preload thread");
                }
            }
            Err(e) => {
                self.app.audio.error = Some(e.clone());
                self.app.ui.set_status(format!("Audio failed: {e}"));
            }
        }
    }

    fn update_metrics(&mut self) {
        self.app.playback.playing = self.playing.load(Ordering::Relaxed);

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

    fn handle_input(&mut self, ctx: &egui::Context) -> bool {
        let Some(ref sequencer) = self.sequencer else {
            return false;
        };
        let seq_snapshot = sequencer.snapshot();

        let term = self.terminal.get_frame().area();
        let widget_rect = ctx.content_rect();
        for mouse in convert_egui_mouse(ctx, widget_rect, term, &mut self.egui_mouse) {
            let mut input_ctx = InputContext {
                app: &mut self.app,
                link: &self.link,
                snapshot: &seq_snapshot,
                playing: &self.playing,
                audio_tx: &sequencer.audio_tx,
                seq_cmd_tx: &sequencer.cmd_tx,
                nudge_us: &self.nudge_us,
            };
            handle_mouse(&mut input_ctx, mouse, term);
        }

        for key in convert_egui_events(ctx) {
            let mut input_ctx = InputContext {
                app: &mut self.app,
                link: &self.link,
                snapshot: &seq_snapshot,
                playing: &self.playing,
                audio_tx: &sequencer.audio_tx,
                seq_cmd_tx: &sequencer.cmd_tx,
                nudge_us: &self.nudge_us,
            };

            if let InputResult::Quit = handle_key(&mut input_ctx, key) {
                return true;
            }
        }

        false
    }
}

impl eframe::App for CagireDesktop {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        self.handle_audio_restart();

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

        self.update_metrics();

        ctx.input(|i| {
            if let Some(pos) = i.pointer.latest_pos() {
                let screen = i.viewport_rect();
                let nx = (pos.x / screen.width()).clamp(0.0, 1.0);
                let ny = (pos.y / screen.height()).clamp(0.0, 1.0);
                self.mouse_x.store(nx.to_bits(), Ordering::Relaxed);
                self.mouse_y.store(ny.to_bits(), Ordering::Relaxed);
            }
            let down = if i.pointer.primary_down() {
                1.0_f32
            } else {
                0.0_f32
            };
            self.mouse_down.store(down.to_bits(), Ordering::Relaxed);
        });

        let Some(ref sequencer) = self.sequencer else {
            return;
        };
        let seq_snapshot = sequencer.snapshot();

        self.app.metrics.event_count = seq_snapshot.event_count;

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

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

        let should_quit = self.handle_input(ctx);
        if should_quit {
            ctx.send_viewport_cmd(egui::ViewportCommand::Close);
            return;
        }

        let current_font = self.current_font;
        let current_zoom = self.zoom_factor;
        let current_fullscreen = self.fullscreen;
        let current_decorations = self.decorations;
        let current_always_on_top = self.always_on_top;
        let mut new_font = None;
        let mut new_zoom = None;
        let mut toggle_fullscreen = false;
        let mut toggle_decorations = false;
        let mut toggle_always_on_top = false;

        egui::CentralPanel::default()
            .frame(egui::Frame::NONE.fill(egui::Color32::BLACK))
            .show(ctx, |ui| {
                if self.app.ui.show_title {
                    self.app.ui.sparkles.tick(self.terminal.get_frame().area());
                }

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

                let elapsed = self.last_frame.elapsed();
                self.last_frame = std::time::Instant::now();

                let link = &self.link;
                let app = &self.app;
                self.terminal
                    .draw(|frame| views::render(frame, app, link, &seq_snapshot, elapsed))
                    .expect("Failed to draw");

                ui.add(self.terminal.backend_mut());

                let response = ui.interact(
                    ui.max_rect(),
                    egui::Id::new("terminal_context"),
                    egui::Sense::click(),
                );
                response.context_menu(|ui| {
                    ui.menu_button("Font", |ui| {
                        for choice in FontChoice::ALL {
                            let selected = current_font == choice;
                            if ui.selectable_label(selected, choice.label()).clicked() {
                                new_font = Some(choice);
                                ui.close();
                            }
                        }
                    });
                    ui.menu_button("Zoom", |ui| {
                        for &level in &[0.5_f32, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] {
                            let selected = (current_zoom - level).abs() < 0.01;
                            let label = format!("{:.0}%", level * 100.0);
                            if ui.selectable_label(selected, label).clicked() {
                                new_zoom = Some(level);
                                ui.close();
                            }
                        }
                    });
                    ui.separator();
                    if ui
                        .selectable_label(current_fullscreen, "Fullscreen")
                        .clicked()
                    {
                        toggle_fullscreen = true;
                        ui.close();
                    }
                    if ui
                        .selectable_label(current_always_on_top, "Always On Top")
                        .clicked()
                    {
                        toggle_always_on_top = true;
                        ui.close();
                    }
                    if ui
                        .selectable_label(!current_decorations, "Borderless")
                        .clicked()
                    {
                        toggle_decorations = true;
                        ui.close();
                    }
                });
            });

        if let Some(font) = new_font {
            self.terminal = create_terminal(font);
            self.current_font = font;
            let mut settings = Settings::load();
            settings.display.font = font.to_setting().to_string();
            settings.save();
        }
        if let Some(zoom) = new_zoom {
            self.zoom_factor = zoom;
            ctx.set_zoom_factor(zoom);
            let mut settings = Settings::load();
            settings.display.zoom_factor = zoom;
            settings.save();
        }
        if toggle_fullscreen {
            self.fullscreen = !self.fullscreen;
            ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(self.fullscreen));
        }
        if toggle_always_on_top {
            self.always_on_top = !self.always_on_top;
            let level = if self.always_on_top {
                egui::WindowLevel::AlwaysOnTop
            } else {
                egui::WindowLevel::Normal
            };
            ctx.send_viewport_cmd(egui::ViewportCommand::WindowLevel(level));
        }
        if toggle_decorations {
            self.decorations = !self.decorations;
            ctx.send_viewport_cmd(egui::ViewportCommand::Decorations(self.decorations));
        }

        ctx.request_repaint_after(Duration::from_millis(
            self.app.audio.config.refresh_rate.millis(),
        ));
    }

    fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
        if let Some(sequencer) = self.sequencer.take() {
            sequencer.shutdown();
        }
    }
}

fn load_icon() -> egui::IconData {
    const ICON_BYTES: &[u8] = include_bytes!("../../../assets/Cagire.png");

    let img = image::load_from_memory(ICON_BYTES)
        .expect("Failed to load embedded icon")
        .resize(64, 64, image::imageops::FilterType::Lanczos3)
        .into_rgba8();

    let (width, height) = img.dimensions();

    egui::IconData {
        rgba: img.into_raw(),
        width,
        height,
    }
}

fn main() -> eframe::Result<()> {
    #[cfg(unix)]
    cagire::engine::realtime::lock_memory();

    let args = Args::parse();

    let options = NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_title("Cagire")
            .with_inner_size([1200.0, 800.0])
            .with_icon(std::sync::Arc::new(load_icon())),
        ..Default::default()
    };

    eframe::run_native(
        "Cagire",
        options,
        Box::new(move |cc| Ok(Box::new(CagireDesktop::new(cc, args)))),
    )
}
