// This file is part of o2.
//
// Copyright (c) 2026  René Coignard <contact@renecoignard.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

//! Entry point and main event loop.
//!
//! [`main`] initialises the crossterm raw-mode terminal, creates an [`EditorState`],
//! and drives the event loop. MIDI Beat Clock timing and note dispatch are
//! handled entirely by the dedicated `midi-clock` thread (see
//! [`o2_rs::core::io::clock`]); the main loop only drives the engine at the
//! correct frame rate and renders the UI.

use anyhow::Result;
use clap::Parser;
use crossterm::{
    event::{
        self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
        Event,
    },
    execute,
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use o2_rs::{
    core::oxygen::{EditorState, PopupType},
    editor::input,
    ui::render,
};
use ratatui::{Terminal, backend::CrosstermBackend};
use std::{
    io::{self, Stdout},
    path::PathBuf,
    time::{Duration, Instant},
};

#[derive(Parser, Debug)]
#[command(
    name = "o2",
    version,
    override_usage = "o2 [options] [file]",
    disable_help_flag = true,
    disable_version_flag = true,
    help_template = "Usage: {usage}\n\n{all-args}"
)]
struct Cli {
    /// Set the maximum number of undo steps.
    /// If you plan to work with large files,
    /// set this to a low number.
    /// Default: 100
    #[arg(
        long,
        default_value_t = 100,
        hide_default_value = true,
        value_name = "number",
        help_heading = "General options",
        verbatim_doc_comment
    )]
    undo_limit: usize,

    /// When creating a new grid file, use these
    /// starting dimensions.
    #[arg(
        long,
        value_parser = parse_size,
        value_name = "nxn",
        help_heading = "General options",
        verbatim_doc_comment
    )]
    initial_size: Option<(usize, usize)>,

    /// Set the tempo (beats per minute).
    /// Default: 120
    #[arg(
        long,
        default_value_t = 120,
        hide_default_value = true,
        value_name = "number",
        help_heading = "General options",
        verbatim_doc_comment
    )]
    bpm: usize,

    /// Set the seed for the random function.
    /// Default: 1
    #[arg(
        long,
        default_value_t = 1,
        hide_default_value = true,
        value_name = "number",
        help_heading = "General options",
        verbatim_doc_comment
    )]
    seed: u64,

    /// Print this message and exit.
    #[arg(
        short = 'h',
        long = "help",
        action = clap::ArgAction::Help,
        help_heading = "General options"
    )]
    help: Option<bool>,

    /// Print version information and exit.
    #[arg(
        short = 'V',
        long = "version",
        action = clap::ArgAction::Version,
        help_heading = "General options"
    )]
    version: Option<bool>,

    /// Set MIDI to be sent via OSC formatted for Plogue Bidule.
    /// The path argument is the path of the Plogue OSC MIDI device.
    /// Example: /OSC_MIDI_0/MIDI
    #[arg(long, help_heading = "OSC/MIDI options", verbatim_doc_comment)]
    osc_midi_bidule: Option<String>,

    /// Use only black and white instead of the full
    /// colour palette.
    #[arg(
        long,
        help_heading = "Display options",
        conflicts_with = "contrast",
        verbatim_doc_comment
    )]
    bw: bool,

    /// Use colours only where they aid usability: editing
    /// elements and menus stay coloured; grid elements are
    /// rendered in white.
    #[arg(
        long,
        help_heading = "Display options",
        conflicts_with = "bw",
        verbatim_doc_comment
    )]
    contrast: bool,

    #[arg(value_name = "file", hide = true)]
    file: Option<PathBuf>,
}

struct TerminalGuard;

impl Drop for TerminalGuard {
    fn drop(&mut self) {
        restore_terminal();
    }
}

fn parse_size(s: &str) -> Result<(usize, usize), String> {
    let (ws, hs) = s
        .split_once('x')
        .ok_or_else(|| "Expected format NxM (e.g. 57x25)".to_string())?;
    let w = ws.parse().map_err(|_| "Invalid width".to_string())?;
    let h = hs.parse().map_err(|_| "Invalid height".to_string())?;
    Ok((w, h))
}

fn restore_terminal() {
    let _ = disable_raw_mode();
    let _ = execute!(
        io::stdout(),
        crossterm::style::ResetColor,
        LeaveAlternateScreen,
        DisableMouseCapture,
        DisableBracketedPaste,
        crossterm::cursor::Show
    );
}

fn emergency_save(app: &EditorState) {
    let save_path = if let Some(path) = &app.current_file {
        let mut os_string = path.as_os_str().to_os_string();
        os_string.push(".save");
        PathBuf::from(os_string)
    } else {
        PathBuf::from(format!("patch-{}.o2.save", input::arvelie_neralie()))
    };

    let content = app.to_grid_string();
    if std::fs::write(&save_path, content.trim_end()).is_ok() {
        eprintln!(
            "\n[o2] Application panicked! Emergency save created at: {}",
            save_path.display()
        );
    }
}

fn run_app(
    app: &mut EditorState,
    terminal: &mut Terminal<CrosstermBackend<Stdout>>,
    _cli: &Cli,
) -> Result<()> {
    let mut next_frame_tick = Instant::now();
    let mut needs_draw = true;

    loop {
        app.midi.set_shared(app.bpm, app.paused, app.midi_bclock);

        match app.midi.poll_transport_event() {
            0xFA => {
                app.paused = false;
                app.o2.f = 0;
                needs_draw = true;
            }
            0xFB => {
                app.paused = false;
                needs_draw = true;
            }
            0xFC => {
                app.paused = true;
                app.midi.silence();
                app.midi.send_clock_stop();
                needs_draw = true;
            }
            _ => {}
        }

        if needs_draw {
            let size = terminal.size()?;
            let viewport_w = size.width as usize;
            let viewport_h = size.height.saturating_sub(2) as usize;
            app.update_scroll(viewport_w, viewport_h);

            terminal.draw(|f| render::draw(f, app))?;
            needs_draw = false;
        }

        let tick_rate = Duration::from_millis(if app.paused {
            100
        } else {
            60000 / app.bpm.max(1) as u64 / 4
        });

        let now = Instant::now();
        let mut timeout = next_frame_tick.saturating_duration_since(now);

        if app.midi.is_puppet() {
            timeout = timeout.min(tick_rate / 4);
        }

        if app
            .popup
            .iter()
            .any(|p| matches!(p, PopupType::About { .. }))
        {
            timeout = timeout.min(Duration::from_millis(33));
            needs_draw = true;
        }

        if event::poll(timeout)? {
            match event::read()? {
                Event::Resize(cols, rows) => {
                    let new_w = (cols as usize).max(app.o2.w);
                    let new_h = (rows.saturating_sub(2) as usize).max(app.o2.h);
                    app.resize(new_w, new_h);
                    needs_draw = true;
                }
                Event::Mouse(mouse_event) => {
                    input::handle_mouse(app, mouse_event);
                    needs_draw = true;
                }
                Event::Key(key) if input::handle_key(app, key) => {
                    needs_draw = true;
                }
                Event::Paste(ref text) => {
                    input::handle_paste(app, text);
                    needs_draw = true;
                }
                _ => {}
            }
        }

        let now = Instant::now();
        let puppet_tick = app.midi.poll_puppet_tick();
        let timer_tick = !app.midi.is_puppet() && now >= next_frame_tick;

        if puppet_tick || timer_tick {
            if !app.paused {
                app.operate();
                app.midi.flush();
                app.o2.f += 1;
                needs_draw = true;
            }
            if timer_tick {
                next_frame_tick += tick_rate;

                // Ant mill: reset if we fall more than two frames behind.
                let now = Instant::now();
                if now.duration_since(next_frame_tick) > tick_rate * 2 {
                    next_frame_tick = now + tick_rate;
                }
            }
        }

        if !app.running {
            app.midi.silence();
            app.midi.send_clock_stop();
            break;
        }
    }

    Ok(())
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    let original_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |panic_info| {
        restore_terminal();
        original_hook(panic_info);
    }));

    enable_raw_mode()?;
    execute!(
        io::stdout(),
        EnterAlternateScreen,
        EnableMouseCapture,
        EnableBracketedPaste,
        crossterm::cursor::Hide
    )?;

    let _guard = TerminalGuard;

    let backend = CrosstermBackend::new(io::stdout());
    let mut terminal = Terminal::new(backend)?;

    let size = terminal.size()?;
    let mut term_w = size.width.max(1) as usize;
    let mut term_h = (size.height.saturating_sub(2)).max(1) as usize;

    if let Some((w, h)) = cli.initial_size {
        term_w = w;
        term_h = h;
    }

    let mut app = EditorState::new(term_w, term_h, cli.seed, cli.undo_limit);
    app.set_bpm(cli.bpm);
    app.midi.osc_midi_bidule = cli.osc_midi_bidule.clone();
    app.bw = cli.bw;
    app.contrast = cli.contrast;

    if let Some(path) = &cli.file
        && let Ok(content) = std::fs::read_to_string(path)
    {
        app.load(&content, Some(path.clone()));
        app.resize(term_w.max(app.o2.w), term_h.max(app.o2.h));
        app.history.saved_absolute_index = Some(app.history.offset + app.history.index);
    }

    let loop_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        run_app(&mut app, &mut terminal, &cli)
    }));

    match loop_result {
        Ok(result) => result,
        Err(err) => {
            emergency_save(&app);
            std::panic::resume_unwind(err);
        }
    }
}
