//! Keyboard and mouse input handling — dispatches events to page-specific or modal handlers.

pub(crate) mod engine_page;
mod help_page;
mod main_page;
mod modal;
mod mouse;
pub(crate) mod options_page;
mod panel;
mod patterns_page;
mod script_page;

use arc_swap::ArcSwap;
use crossbeam_channel::Sender;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent};
use ratatui::layout::Rect;
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};

use crate::app::App;
use crate::commands::AppCommand;
use crate::engine::{AudioCommand, LinkState, SeqCommand, SequencerSnapshot};
use crate::page::Page;
use crate::state::{MinimapMode, Modal, PanelFocus};

pub enum InputResult {
    Continue,
    Quit,
}

pub struct InputContext<'a> {
    pub app: &'a mut App,
    pub link: &'a LinkState,
    pub snapshot: &'a SequencerSnapshot,
    pub playing: &'a Arc<AtomicBool>,
    pub audio_tx: &'a ArcSwap<Sender<AudioCommand>>,
    pub seq_cmd_tx: &'a Sender<SeqCommand>,
    pub nudge_us: &'a Arc<AtomicI64>,
}

impl<'a> InputContext<'a> {
    fn dispatch(&mut self, cmd: AppCommand) {
        self.app.dispatch(cmd, self.link, self.snapshot);
    }
}

pub fn handle_mouse(ctx: &mut InputContext, mouse: MouseEvent, term: Rect) {
    mouse::handle_mouse(ctx, mouse, term);
}

pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
    if handle_live_keys(ctx, &key) {
        return InputResult::Continue;
    }

    if key.kind == KeyEventKind::Release {
        return InputResult::Continue;
    }

    let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
    let is_arrow = matches!(
        key.code,
        KeyCode::Left | KeyCode::Right | KeyCode::Up | KeyCode::Down
    );
    if !(matches!(ctx.app.ui.minimap, MinimapMode::Hidden) || ctrl && is_arrow) {
        ctx.dispatch(AppCommand::ClearMinimap);
    }

    if ctx.app.ui.show_title {
        ctx.dispatch(AppCommand::HideTitle);
        return InputResult::Continue;
    }

    ctx.dispatch(AppCommand::ClearStatus);

    if matches!(ctx.app.ui.modal, Modal::None) {
        handle_normal_input(ctx, key)
    } else {
        modal::handle_modal_input(ctx, key)
    }
}

fn handle_live_keys(ctx: &mut InputContext, key: &KeyEvent) -> bool {
    match (key.code, key.kind) {
        _ if !matches!(ctx.app.ui.modal, Modal::None) => false,
        _ if ctx.app.page == Page::Script && ctx.app.script_editor.focused => false,
        (KeyCode::Char('f'), KeyEventKind::Press) if !key.modifiers.contains(KeyModifiers::ALT) => {
            ctx.dispatch(AppCommand::ToggleLiveKeysFill);
            true
        }
        _ => false,
    }
}

fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
    let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
    let alt = key.modifiers.contains(KeyModifiers::ALT);
    if key.code == KeyCode::F(12) && !ctx.app.plugin_mode {
        if !ctx.app.playback.playing {
            ctx.dispatch(AppCommand::TogglePlaying);
            ctx.playing.store(true, Ordering::Relaxed);
        }
        let _ = ctx.seq_cmd_tx.send(SeqCommand::RestartAll);
        return InputResult::Continue;
    }

    if ctx.app.panel.visible && ctx.app.panel.focus == PanelFocus::Side {
        return panel::handle_panel_input(ctx, key);
    }

    if alt {
        match key.code {
            KeyCode::Up => {
                ctx.dispatch(AppCommand::PrevPattern);
                return InputResult::Continue;
            }
            KeyCode::Down => {
                ctx.dispatch(AppCommand::NextPattern);
                return InputResult::Continue;
            }
            KeyCode::Left | KeyCode::Char('b') => {
                ctx.dispatch(AppCommand::PrevBank);
                return InputResult::Continue;
            }
            KeyCode::Right | KeyCode::Char('f') => {
                ctx.dispatch(AppCommand::NextBank);
                return InputResult::Continue;
            }
            _ => {}
        }
    }

    if ctrl {
        let minimap_timed = MinimapMode::Timed(Instant::now() + Duration::from_millis(1000));
        match key.code {
            KeyCode::Left => {
                ctx.app.ui.minimap = minimap_timed;
                ctx.dispatch(AppCommand::PageLeft);
                return InputResult::Continue;
            }
            KeyCode::Right => {
                ctx.app.ui.minimap = minimap_timed;
                ctx.dispatch(AppCommand::PageRight);
                return InputResult::Continue;
            }
            KeyCode::Up => {
                ctx.app.ui.minimap = minimap_timed;
                ctx.dispatch(AppCommand::PageUp);
                return InputResult::Continue;
            }
            KeyCode::Down => {
                ctx.app.ui.minimap = minimap_timed;
                ctx.dispatch(AppCommand::PageDown);
                return InputResult::Continue;
            }
            _ => {}
        }
    }

    if let Some(page) = match key.code {
        KeyCode::F(1) => Some(Page::Dict),
        KeyCode::F(2) => Some(Page::Patterns),
        KeyCode::F(3) => Some(Page::Options),
        KeyCode::F(4) => Some(Page::Help),
        KeyCode::F(5) => Some(Page::Main),
        KeyCode::F(6) => Some(Page::Engine),
        KeyCode::F(7) => Some(Page::Script),
        _ => None,
    } {
        ctx.app.ui.minimap = MinimapMode::Timed(Instant::now() + Duration::from_millis(1000));
        ctx.dispatch(AppCommand::GoToPage(page));
        return InputResult::Continue;
    }

    match ctx.app.page {
        Page::Main => main_page::handle_main_page(ctx, key, ctrl),
        Page::Patterns => patterns_page::handle_patterns_page(ctx, key),
        Page::Engine => engine_page::handle_engine_page(ctx, key),
        Page::Options => options_page::handle_options_page(ctx, key),
        Page::Help => help_page::handle_help_page(ctx, key),
        Page::Dict => help_page::handle_dict_page(ctx, key),
        Page::Script => script_page::handle_script_page(ctx, key),
    }
}

fn open_save(ctx: &mut InputContext) {
    use crate::state::file_browser::FileBrowserState;
    let initial = ctx
        .app
        .project_state
        .file_path
        .as_ref()
        .map(|p| p.display().to_string())
        .unwrap_or_default();
    let state = FileBrowserState::new_save(initial);
    ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(Box::new(state))));
}

fn open_load(ctx: &mut InputContext) {
    use crate::state::file_browser::FileBrowserState;
    let default_dir = ctx
        .app
        .project_state
        .file_path
        .as_ref()
        .and_then(|p| p.parent())
        .map(|p| {
            let mut s = p.display().to_string();
            if !s.ends_with('/') {
                s.push('/');
            }
            s
        })
        .unwrap_or_default();
    let state = FileBrowserState::new_load(default_dir);
    ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(Box::new(state))));
}

fn load_project_samples(ctx: &mut InputContext) {
    let paths = ctx.app.project_state.project.sample_paths.clone();
    if paths.is_empty() {
        return;
    }

    let mut total_count = 0;
    let mut counts = Vec::new();
    let mut all_preload_entries = Vec::new();
    for path in &paths {
        if path.is_dir() {
            let index = doux::sampling::scan_samples_dir(path);
            let count = index.len();
            total_count += count;
            counts.push(count);
            for e in &index {
                all_preload_entries.push((e.name.clone(), e.path.clone()));
            }
            let _ = ctx.audio_tx.load().send(AudioCommand::LoadSamples(index));
        } else {
            counts.push(0);
        }
    }

    ctx.app.audio.config.sample_paths = paths;
    ctx.app.audio.config.sample_counts = counts;

    for path in &ctx.app.audio.config.sample_paths {
        if let Some(sf2_path) = doux::soundfont::find_sf2_file(path) {
            let _ = ctx.audio_tx.load().send(AudioCommand::LoadSoundfont(sf2_path));
            break;
        }
    }

    if total_count > 0 {
        if let Some(registry) = ctx.app.audio.sample_registry.clone() {
            let sr = ctx.app.audio.config.sample_rate;
            std::thread::Builder::new()
                .name("sample-preload".into())
                .spawn(move || {
                    crate::engine::preload_sample_heads(all_preload_entries, sr, &registry);
                })
                .expect("failed to spawn preload thread");
        }
        ctx.dispatch(AppCommand::SetStatus(format!(
            "Loaded {total_count} samples from project"
        )));
    }
}
