use crate::internal_telemetry::is_allocation_tracking_enabled;
use crossterm::{
    cursor::Show,
    event::{DisableMouseCapture, EnableMouseCapture, KeyCode},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
    tty::IsTty,
    ExecutableCommand,
};
use num_format::{Locale, ToFormattedString};
use number_prefix::NumberPrefix;
use ratatui::{
    backend::CrosstermBackend,
    layout::{Alignment, Constraint, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Cell, Paragraph, Row, Table, Wrap},
    Frame, Terminal,
};
use std::io::stdout;
use tokio::sync::oneshot;
use super::{
    events::capture_key_press,
    state::{self, ConnectionStatus},
};
trait ThousandsFormatter {
    fn thousands_format(&self) -> String;
}
impl ThousandsFormatter for u32 {
    fn thousands_format(&self) -> String {
        match self {
            0 => "--".into(),
            _ => self.to_formatted_string(&Locale::en),
        }
    }
}
impl ThousandsFormatter for u64 {
    fn thousands_format(&self) -> String {
        match self {
            0 => "--".into(),
            _ => self.to_formatted_string(&Locale::en),
        }
    }
}
impl ThousandsFormatter for i64 {
    fn thousands_format(&self) -> String {
        match self {
            0 => "--".into(),
            _ => self.to_formatted_string(&Locale::en),
        }
    }
}
trait HumanFormatter {
    fn human_format(&self) -> String;
    fn human_format_bytes(&self) -> String;
}
impl HumanFormatter for i64 {
    fn human_format(&self) -> String {
        match self {
            0 => "--".into(),
            n => match NumberPrefix::decimal(*n as f64) {
                NumberPrefix::Standalone(n) => n.to_string(),
                NumberPrefix::Prefixed(p, n) => format!("{:.2} {}", n, p),
            },
        }
    }
    fn human_format_bytes(&self) -> String {
        match self {
            0 => "--".into(),
            n => match NumberPrefix::binary(*n as f64) {
                NumberPrefix::Standalone(n) => n.to_string(),
                NumberPrefix::Prefixed(p, n) => format!("{:.2} {}B", n, p),
            },
        }
    }
}
fn format_metric(total: i64, throughput: i64, human_metrics: bool) -> String {
    match total {
        0 => "N/A".to_string(),
        v => format!(
            "{} ({}/s)",
            if human_metrics {
                v.human_format()
            } else {
                v.thousands_format()
            },
            throughput.human_format()
        ),
    }
}
fn format_metric_bytes(total: i64, throughput: i64, human_metrics: bool) -> String {
    match total {
        0 => "N/A".to_string(),
        v => format!(
            "{} ({}/s)",
            if human_metrics {
                v.human_format_bytes()
            } else {
                v.thousands_format()
            },
            throughput.human_format_bytes()
        ),
    }
}
const NUM_COLUMNS: usize = if is_allocation_tracking_enabled() {
    10
} else {
    9
};
static HEADER: [&str; NUM_COLUMNS] = [
    "ID",
    "Output",
    "Kind",
    "Type",
    "Events In",
    "Bytes In",
    "Events Out",
    "Bytes Out",
    "Errors",
    #[cfg(feature = "allocation-tracing")]
    "Memory Used",
];
struct Widgets<'a> {
    constraints: Vec<Constraint>,
    url_string: &'a str,
    opts: &'a super::Opts,
    title: &'a str,
}
impl<'a> Widgets<'a> {
    pub fn new(title: &'a str, url_string: &'a str, opts: &'a super::Opts) -> Self {
        let constraints = vec![
            Constraint::Length(3),
            Constraint::Max(90),
            Constraint::Length(3),
        ];
        Self {
            constraints,
            url_string,
            opts,
            title,
        }
    }
    fn title(&'a self, f: &mut Frame, area: Rect, connection_status: &ConnectionStatus) {
        let mut text = vec![
            Span::from(self.url_string),
            Span::styled(
                format!(" | Sampling @ {}ms", self.opts.interval.thousands_format()),
                Style::default().fg(Color::Gray),
            ),
            Span::from(" | "),
        ];
        text.extend(connection_status.as_ui_spans());
        let text = vec![Line::from(text)];
        let block = Block::default().borders(Borders::ALL).title(Span::styled(
            self.title,
            Style::default()
                .fg(Color::Green)
                .add_modifier(Modifier::BOLD),
        ));
        let w = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
        f.render_widget(w, area);
    }
    fn components_table(&self, f: &mut Frame, state: &state::State, area: Rect) {
        let header = HEADER
            .iter()
            .map(|s| Cell::from(*s).style(Style::default().add_modifier(Modifier::BOLD)))
            .collect::<Vec<_>>();
        let mut items = Vec::new();
        for (_, r) in state.components.iter() {
            let mut data = vec![
                r.key.id().to_string(),
                (!r.has_displayable_outputs())
                    .then_some("--")
                    .unwrap_or_default()
                    .to_string(),
                r.kind.clone(),
                r.component_type.clone(),
            ];
            let formatted_metrics = [
                format_metric(
                    r.received_events_total,
                    r.received_events_throughput_sec,
                    self.opts.human_metrics,
                ),
                format_metric_bytes(
                    r.received_bytes_total,
                    r.received_bytes_throughput_sec,
                    self.opts.human_metrics,
                ),
                format_metric(
                    r.sent_events_total,
                    r.sent_events_throughput_sec,
                    self.opts.human_metrics,
                ),
                format_metric_bytes(
                    r.sent_bytes_total,
                    r.sent_bytes_throughput_sec,
                    self.opts.human_metrics,
                ),
                if self.opts.human_metrics {
                    r.errors.human_format()
                } else {
                    r.errors.thousands_format()
                },
                #[cfg(feature = "allocation-tracing")]
                r.allocated_bytes.human_format_bytes(),
            ];
            data.extend_from_slice(&formatted_metrics);
            items.push(Row::new(data).style(Style::default()));
            if r.has_displayable_outputs() {
                for (id, output) in r.outputs.iter() {
                    let sent_events_metric = format_metric(
                        output.sent_events_total,
                        output.sent_events_throughput_sec,
                        self.opts.human_metrics,
                    );
                    let mut data = [""; NUM_COLUMNS]
                        .into_iter()
                        .map(Cell::from)
                        .collect::<Vec<_>>();
                    data[1] = Cell::from(id.as_str());
                    data[5] = Cell::from(sent_events_metric);
                    items.push(Row::new(data).style(Style::default()));
                }
            }
        }
        let widths: &[Constraint] = if is_allocation_tracking_enabled() {
            &[
                Constraint::Percentage(13), Constraint::Percentage(8),  Constraint::Percentage(4),  Constraint::Percentage(9),  Constraint::Percentage(10), Constraint::Percentage(12), Constraint::Percentage(10), Constraint::Percentage(12), Constraint::Percentage(8),  Constraint::Percentage(14), ]
        } else {
            &[
                Constraint::Percentage(13), Constraint::Percentage(12), Constraint::Percentage(9),  Constraint::Percentage(6),  Constraint::Percentage(12), Constraint::Percentage(14), Constraint::Percentage(12), Constraint::Percentage(14), Constraint::Percentage(8),  ]
        };
        let w = Table::new(items, widths)
            .header(Row::new(header).bottom_margin(1))
            .block(Block::default().borders(Borders::ALL).title("Components"))
            .column_spacing(2);
        f.render_widget(w, area);
    }
    fn components_resize_window(&self, f: &mut Frame, area: Rect) {
        let block = Block::default().borders(Borders::ALL).title("Components");
        let w = Paragraph::new("Expand the window to > 80 chars to view metrics")
            .block(block)
            .wrap(Wrap { trim: true });
        f.render_widget(w, area);
    }
    fn quit_box(&self, f: &mut Frame, area: Rect) {
        let text = vec![Line::from("To quit, press ESC or 'q'")];
        let block = Block::default()
            .borders(Borders::ALL)
            .border_style(Style::default().fg(Color::Gray));
        let w = Paragraph::new(text)
            .block(block)
            .style(Style::default().fg(Color::Gray))
            .alignment(Alignment::Left);
        f.render_widget(w, area);
    }
    fn draw(&self, f: &mut Frame, state: state::State) {
        let size = f.area();
        let rects = Layout::default()
            .constraints(self.constraints.clone())
            .split(size);
        self.title(f, rects[0], &state.connection_status);
        if size.width >= 80 {
            self.components_table(f, &state, rects[1]);
        } else {
            self.components_resize_window(f, rects[1]);
        }
        self.quit_box(f, rects[2]);
    }
}
pub fn is_tty() -> bool {
    stdout().is_tty()
}
pub async fn init_dashboard<'a>(
    title: &'a str,
    url: &'a str,
    opts: &'a super::Opts,
    mut state_rx: state::StateRx,
    mut shutdown_rx: oneshot::Receiver<()>,
) -> Result<(), Box<dyn std::error::Error>> {
    let (mut key_press_rx, key_press_kill_tx) = capture_key_press();
    let mut stdout = stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    enable_raw_mode()?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;
    terminal.clear()?;
    let widgets = Widgets::new(title, url, opts);
    loop {
        tokio::select! {
            Some(state) = state_rx.recv() => {
                terminal.draw(|f| widgets.draw(f, state))?;
            },
            k = key_press_rx.recv() => {
                if let KeyCode::Esc | KeyCode::Char('q') = k.unwrap() {
                    _ = key_press_kill_tx.send(());
                    break
                }
            }
            _ = &mut shutdown_rx => {
                _ = key_press_kill_tx.send(());
                break
            }
        }
    }
    terminal.backend_mut().execute(DisableMouseCapture)?;
    terminal.backend_mut().execute(LeaveAlternateScreen)?;
    terminal.backend_mut().execute(Show)?;
    disable_raw_mode()?;
    Ok(())
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn format_zero() {
        const N: i64 = 0;
        assert_eq!(N.thousands_format(), "--");
        assert_eq!(N.human_format(), "--");
    }
    #[test]
    fn format_hundred() {
        const N: i64 = 100;
        assert_eq!(N.thousands_format(), "100");
        assert_eq!(N.human_format(), "100");
    }
    #[test]
    fn format_thousands() {
        const N: i64 = 1_000;
        assert_eq!(N.thousands_format(), "1,000");
        assert_eq!(N.human_format(), "1.00 k");
    }
    #[test]
    fn format_thousands_no_rounding() {
        const N: i64 = 1_500;
        assert_eq!(N.thousands_format(), "1,500");
        assert_eq!(N.human_format(), "1.50 k");
    }
    #[test]
    fn format_thousands_round_down() {
        const N: i64 = 1_514;
        assert_eq!(N.thousands_format(), "1,514");
        assert_eq!(N.human_format(), "1.51 k");
    }
    #[test]
    fn format_thousands_round_up() {
        const N: i64 = 1_999;
        assert_eq!(N.thousands_format(), "1,999");
        assert_eq!(N.human_format(), "2.00 k");
    }
    #[test]
    fn format_millions() {
        const N: i64 = 1_000_000;
        assert_eq!(N.thousands_format(), "1,000,000");
        assert_eq!(N.human_format(), "1.00 M");
    }
    #[test]
    fn format_billions() {
        const N: i64 = 1_000_000_000;
        assert_eq!(N.thousands_format(), "1,000,000,000");
        assert_eq!(N.human_format(), "1.00 G");
    }
    #[test]
    fn format_trillions() {
        const N: i64 = 1_100_000_000_000;
        assert_eq!(N.thousands_format(), "1,100,000,000,000");
        assert_eq!(N.human_format(), "1.10 T");
    }
    #[test]
    fn format_bytes() {
        const N: i64 = 1024;
        assert_eq!(N.human_format_bytes(), "1.00 KiB");
        assert_eq!((N * N).human_format_bytes(), "1.00 MiB");
        assert_eq!((N * (N * N)).human_format_bytes(), "1.00 GiB");
        assert_eq!((N * (N * (N * N))).human_format_bytes(), "1.00 TiB");
        assert_eq!((N * (N * (N * (N * N)))).human_format_bytes(), "1.00 PiB");
    }
}