pub mod shell;

use std::io;
use std::time::Duration;

use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::ExecutableCommand;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use tokio::sync::mpsc;
use tracing::warn;

use self::shell::Shell;

pub enum OutputMsg {
    Register { shell_id: usize, name: String },
    Line { shell_id: usize, content: Vec<u8> },
}

struct App {
    shells: Vec<Shell>,
    active_idx: usize,
    rx: mpsc::UnboundedReceiver<OutputMsg>,
}

impl App {
    fn new(rx: mpsc::UnboundedReceiver<OutputMsg>) -> Self {
        Self {
            shells: Vec::new(),
            active_idx: 0,
            rx,
        }
    }

    fn active_shell(&self) -> Option<&Shell> {
        self.shells.get(self.active_idx)
    }

    fn active_shell_mut(&mut self) -> Option<&mut Shell> {
        self.shells.get_mut(self.active_idx)
    }

    fn shell_count(&self) -> usize {
        self.shells.len()
    }

    fn active_index(&self) -> usize {
        self.active_idx
    }

    fn next_shell(&mut self) {
        if !self.shells.is_empty() {
            self.active_idx = (self.active_idx + 1) % self.shells.len();
        }
    }

    fn prev_shell(&mut self) {
        if !self.shells.is_empty() {
            self.active_idx = (self.active_idx + self.shells.len() - 1) % self.shells.len();
        }
    }

    fn process_messages(&mut self) {
        while let Ok(msg) = self.rx.try_recv() {
            match msg {
                OutputMsg::Register { shell_id, name } => {
                    self.shells.push(Shell::new(shell_id, name));
                }
                OutputMsg::Line { shell_id, content } => {
                    if let Some(shell) = self.shells.iter_mut().find(|s| s.id() == shell_id) {
                        shell.push_raw_line(&content);
                    }
                }
            }
        }
    }

    fn render(&self, frame: &mut Frame) {
        let area = frame.area();

        let (title, nav_hint, border_color) = if let Some(shell) = self.active_shell() {
            let title = format!(" {} ", shell.name());
            let nav = format!(" \u{25c4} {}/{} \u{25ba} ", self.active_index() + 1, self.shell_count());
            (title, nav, shell.color())
        } else {
            let title = " donion \u{2014} waiting for shells ".to_string();
            let nav = String::new();
            (title, nav, Color::White)
        };

        let block = Block::default()
            .borders(Borders::ALL)
            .border_style(Style::default().fg(border_color))
            .title(title)
            .title_bottom(Line::from(nav_hint).centered());

        let inner = block.inner(area);
        frame.render_widget(block, area);

        if let Some(shell) = self.active_shell() {
            let viewport_height = inner.height as usize;
            let lines: Vec<Line<'static>> = shell
                .visible_lines(viewport_height)
                .cloned()
                .collect();
            let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });
            frame.render_widget(paragraph, inner);
        }
    }
}

/// Handle a key event. Returns `true` if the app should quit.
fn handle_key(app: &mut App, key: KeyEvent) -> bool {
    match (key.modifiers, key.code) {
        (KeyModifiers::CONTROL, KeyCode::Char('c')) => return true,
        (KeyModifiers::CONTROL, KeyCode::Up) => {
            if let Some(shell) = app.active_shell_mut() {
                shell.scroll_to_top();
            }
        }
        (KeyModifiers::CONTROL, KeyCode::Down) => {
            if let Some(shell) = app.active_shell_mut() {
                shell.scroll_to_bottom();
            }
        }
        (_, KeyCode::Left) => app.prev_shell(),
        (_, KeyCode::Right) => app.next_shell(),
        (_, KeyCode::Up) => {
            if let Some(shell) = app.active_shell_mut() {
                shell.scroll_up(1);
            }
        }
        (_, KeyCode::Down) => {
            if let Some(shell) = app.active_shell_mut() {
                shell.scroll_down(1);
            }
        }
        _ => {}
    }
    false
}

pub async fn run(rx: mpsc::UnboundedReceiver<OutputMsg>) -> Result<()> {
    terminal::enable_raw_mode()?;
    io::stdout().execute(EnterAlternateScreen)?;

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

    let mut app = App::new(rx);

    loop {
        app.process_messages();

        if let Err(e) = terminal.draw(|f| app.render(f)) {
            warn!("draw error: {e}");
        }

        if event::poll(Duration::from_millis(50))? {
            if let Event::Key(key) = event::read()? {
                if handle_key(&mut app, key) {
                    break;
                }
            }
        }
    }

    terminal::disable_raw_mode()?;
    io::stdout().execute(LeaveAlternateScreen)?;
    Ok(())
}
