use std::io;

use color_eyre::Result;
use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind};
use ratatui::crossterm::execute;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style, Stylize};
use ratatui::text::Line;
use ratatui::widgets::{Block, Borders};
use ratatui::DefaultTerminal;
use tui_scrollbar::{
    ScrollBar, ScrollBarArrows, ScrollBarInteraction, ScrollCommand, ScrollLengths, ScrollMetrics,
    SUBCELL,
};

const KEY_STEP: usize = 1;
const TITLE_FG: Color = Color::Rgb(196, 206, 224);
const TITLE_BG: Color = Color::Rgb(32, 43, 64);
const BLOCK_FG: Color = Color::Rgb(196, 206, 224);
const BLOCK_BG: Color = Color::Rgb(13, 23, 38);

fn main() -> Result<()> {
    color_eyre::install()?;
    let mut terminal = ratatui::init();
    execute!(io::stdout(), event::EnableMouseCapture)?;
    let result = App::new().run(&mut terminal);
    execute!(io::stdout(), event::DisableMouseCapture)?;
    ratatui::restore();
    result
}

#[derive(Debug, Default)]
struct App {
    state: AppState,
    layout: Option<LayoutState>,
    vertical_offset: usize,
    horizontal_offset: usize,
    vertical_interaction: ScrollBarInteraction,
    horizontal_interaction: ScrollBarInteraction,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
    #[default]
    Running,
    Quit,
}

#[derive(Debug, Clone, Copy)]
struct LayoutState {
    content: Rect,
    vertical_bar: Rect,
    horizontal_bar: Rect,
}

impl App {
    fn new() -> Self {
        Self {
            state: AppState::Running,
            layout: None,
            vertical_offset: 0,
            horizontal_offset: 0,
            vertical_interaction: ScrollBarInteraction::new(),
            horizontal_interaction: ScrollBarInteraction::new(),
        }
    }

    fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
        while self.state == AppState::Running {
            terminal.draw(|frame| self.render(frame))?;
            self.handle_events()?;
        }
        Ok(())
    }

    fn render(&mut self, frame: &mut ratatui::Frame) {
        let area = frame.area();
        if area.width < 2 || area.height < 2 {
            return;
        }

        let title = "tui-scrollbar - mouse scroll demo";
        let block = Block::new()
            .borders(Borders::TOP)
            .border_style(Style::new().fg(TITLE_FG).bg(TITLE_BG))
            .style(Style::new().fg(BLOCK_FG).bg(BLOCK_BG))
            .title(
                Line::from(title)
                    .centered()
                    .fg(TITLE_FG)
                    .bg(TITLE_BG)
                    .bold(),
            );
        frame.render_widget(&block, area);

        let content_area = Rect {
            y: area.y.saturating_add(1),
            height: area.height.saturating_sub(1),
            ..area
        };

        // Split out the bottom row and right column for the scrollbars.
        let [content_row, bar_row] = content_area.layout(&Layout::vertical([
            Constraint::Fill(1),
            Constraint::Length(1),
        ]));
        let [content, vertical_bar] = content_row.layout(&Layout::horizontal([
            Constraint::Fill(1),
            Constraint::Length(1),
        ]));
        let [horizontal_bar, _corner] = bar_row.layout(&Layout::horizontal([
            Constraint::Fill(1),
            Constraint::Length(1),
        ]));

        self.layout = Some(LayoutState {
            content,
            vertical_bar,
            horizontal_bar,
        });

        // Keep offsets valid when the terminal is resized.
        let (h_metrics, v_metrics) = self.metrics_for_layout(content);
        self.horizontal_offset = self.horizontal_offset.min(h_metrics.max_offset());
        self.vertical_offset = self.vertical_offset.min(v_metrics.max_offset());

        let horizontal_lengths = ScrollLengths {
            content_len: h_metrics.content_len(),
            viewport_len: h_metrics.viewport_len(),
        };
        let horizontal = ScrollBar::horizontal(horizontal_lengths)
            .arrows(ScrollBarArrows::Both)
            .offset(self.horizontal_offset)
            .scroll_step(SUBCELL);
        let vertical_lengths = ScrollLengths {
            content_len: v_metrics.content_len(),
            viewport_len: v_metrics.viewport_len(),
        };
        let vertical = ScrollBar::vertical(vertical_lengths)
            .arrows(ScrollBarArrows::Both)
            .offset(self.vertical_offset)
            .scroll_step(SUBCELL);

        frame.render_widget(&horizontal, horizontal_bar);
        frame.render_widget(&vertical, vertical_bar);
    }

    fn handle_events(&mut self) -> Result<()> {
        match event::read()? {
            Event::Key(key) => {
                if key.kind == KeyEventKind::Press {
                    match key.code {
                        KeyCode::Char('q') | KeyCode::Esc => self.state = AppState::Quit,
                        KeyCode::Up => self.handle_key_scroll(0, -(KEY_STEP as isize)),
                        KeyCode::Down => self.handle_key_scroll(0, KEY_STEP as isize),
                        KeyCode::Left => self.handle_key_scroll(-(KEY_STEP as isize), 0),
                        KeyCode::Right => self.handle_key_scroll(KEY_STEP as isize, 0),
                        _ => {}
                    }
                }
            }
            Event::Mouse(event) => {
                if let Some(layout) = self.layout {
                    let (h_metrics, v_metrics) = self.metrics_for_layout(layout.content);
                    let horizontal_lengths = ScrollLengths {
                        content_len: h_metrics.content_len(),
                        viewport_len: h_metrics.viewport_len(),
                    };
                    let horizontal = ScrollBar::horizontal(horizontal_lengths)
                        .arrows(ScrollBarArrows::Both)
                        .offset(self.horizontal_offset)
                        .scroll_step(SUBCELL);
                    let vertical_lengths = ScrollLengths {
                        content_len: v_metrics.content_len(),
                        viewport_len: v_metrics.viewport_len(),
                    };
                    let vertical = ScrollBar::vertical(vertical_lengths)
                        .arrows(ScrollBarArrows::Both)
                        .offset(self.vertical_offset)
                        .scroll_step(SUBCELL);

                    if let Some(command) = horizontal.handle_mouse_event(
                        layout.horizontal_bar,
                        event,
                        &mut self.horizontal_interaction,
                    ) {
                        self.apply_command(command, true);
                    }
                    if let Some(command) = vertical.handle_mouse_event(
                        layout.vertical_bar,
                        event,
                        &mut self.vertical_interaction,
                    ) {
                        self.apply_command(command, false);
                    }
                }
            }
            _ => {}
        }
        Ok(())
    }

    fn handle_key_scroll(&mut self, dx: isize, dy: isize) {
        let Some(layout) = self.layout else {
            return;
        };
        let (h_metrics, v_metrics) = self.metrics_for_layout(layout.content);
        self.horizontal_offset =
            Self::apply_delta(self.horizontal_offset, dx, h_metrics.max_offset());
        self.vertical_offset = Self::apply_delta(self.vertical_offset, dy, v_metrics.max_offset());
    }

    fn apply_command(&mut self, command: ScrollCommand, is_horizontal: bool) {
        let ScrollCommand::SetOffset(offset) = command;
        if is_horizontal {
            self.horizontal_offset = offset;
        } else {
            self.vertical_offset = offset;
        }
    }

    fn metrics_for_layout(&self, content: Rect) -> (ScrollMetrics, ScrollMetrics) {
        // Use subcell units so wheel/drag updates line up with the fractional renderer.
        let h_cells = content.width.max(1) as usize;
        let v_cells = content.height.max(1) as usize;
        let h_content = h_cells.saturating_mul(SUBCELL).max(1);
        let v_content = v_cells.saturating_mul(SUBCELL).max(1);
        let h_viewport = h_content.saturating_sub(100).max(1);
        let v_viewport = v_content.saturating_sub(100).max(1);
        (
            ScrollMetrics::new(
                ScrollLengths {
                    content_len: h_content,
                    viewport_len: h_viewport,
                },
                self.horizontal_offset,
                content.width,
            ),
            ScrollMetrics::new(
                ScrollLengths {
                    content_len: v_content,
                    viewport_len: v_viewport,
                },
                self.vertical_offset,
                content.height,
            ),
        )
    }

    fn apply_delta(current: usize, delta: isize, max: usize) -> usize {
        if delta < 0 {
            current.saturating_sub(delta.unsigned_abs() as usize)
        } else {
            current.saturating_add(delta as usize).min(max)
        }
    }
}
