use itertools::Itertools;
use ratatui::{
    buffer::Buffer,
    layout::{Alignment, Constraint, Layout, Margin, Rect},
    style::{Styled, Stylize},
    symbols::Marker,
    widgets::{
        canvas::{self, Canvas, Map, MapResolution, Points},
        Block, BorderType, Clear, Padding, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
        Sparkline, StatefulWidget, Table, TableState, Widget,
    },
};

use crate::{RgbSwatch, THEME};

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TracerouteTab {
    row_index: usize,
}

impl TracerouteTab {
    /// Select the previous row (with wrap around).
    pub fn prev_row(&mut self) {
        self.row_index = self.row_index.saturating_add(HOPS.len() - 1) % HOPS.len();
    }

    /// Select the next row (with wrap around).
    pub fn next_row(&mut self) {
        self.row_index = self.row_index.saturating_add(1) % HOPS.len();
    }
}

impl Widget for TracerouteTab {
    fn render(self, area: Rect, buf: &mut Buffer) {
        RgbSwatch.render(area, buf);
        let area = area.inner(Margin {
            vertical: 1,
            horizontal: 2,
        });
        Clear.render(area, buf);
        Block::new().style(THEME.content).render(area, buf);
        let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
        let vertical = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]);
        let [left, map] = horizontal.areas(area);
        let [hops, pings] = vertical.areas(left);

        render_hops(self.row_index, hops, buf);
        render_ping(self.row_index, pings, buf);
        render_map(self.row_index, map, buf);
    }
}

fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
    let mut state = TableState::default().with_selected(Some(selected_row));
    let rows = HOPS
        .iter()
        .map(|hop| Row::new(vec![hop.host, hop.address]))
        .collect_vec();
    let block = Block::new()
        .padding(Padding::new(1, 1, 1, 1))
        .title_alignment(Alignment::Center)
        .title("Traceroute bad.horse".bold().white());
    StatefulWidget::render(
        Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
            .header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
            .highlight_style(THEME.traceroute.selected)
            .block(block),
        area,
        buf,
        &mut state,
    );
    let mut scrollbar_state = ScrollbarState::default()
        .content_length(HOPS.len())
        .position(selected_row);
    let area = Rect {
        width: area.width + 1,
        y: area.y + 3,
        height: area.height - 4,
        ..area
    };
    Scrollbar::default()
        .orientation(ScrollbarOrientation::VerticalLeft)
        .begin_symbol(None)
        .end_symbol(None)
        .track_symbol(None)
        .thumb_symbol("▌")
        .render(area, buf, &mut scrollbar_state);
}

pub fn render_ping(progress: usize, area: Rect, buf: &mut Buffer) {
    let mut data = [
        8, 8, 8, 8, 7, 7, 7, 6, 6, 5, 4, 3, 3, 2, 2, 1, 1, 1, 2, 2, 3, 4, 5, 6, 7, 7, 8, 8, 8, 7,
        7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 2, 4, 6, 7, 8, 8, 8, 8, 6, 4, 2, 1, 1, 1, 1, 2, 2, 2, 3,
        3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7,
    ];
    let mid = progress % data.len();
    data.rotate_left(mid);
    Sparkline::default()
        .block(
            Block::new()
                .title("Ping")
                .title_alignment(Alignment::Center)
                .border_type(BorderType::Thick),
        )
        .data(&data)
        .style(THEME.traceroute.ping)
        .render(area, buf);
}

fn render_map(selected_row: usize, area: Rect, buf: &mut Buffer) {
    let theme = THEME.traceroute.map;
    let path: Option<(&Hop, &Hop)> = HOPS.iter().tuple_windows().nth(selected_row);
    let map = Map {
        resolution: MapResolution::High,
        color: theme.color,
    };
    Canvas::default()
        .background_color(theme.background_color)
        .block(
            Block::new()
                .padding(Padding::new(1, 0, 1, 0))
                .style(theme.style),
        )
        .marker(Marker::HalfBlock)
        // picked to show Australia for the demo as it's the most interesting part of the map
        // (and the only part with hops ;))
        .x_bounds([112.0, 155.0])
        .y_bounds([-46.0, -11.0])
        .paint(|context| {
            context.draw(&map);
            if let Some(path) = path {
                context.draw(&canvas::Line::new(
                    path.0.location.0,
                    path.0.location.1,
                    path.1.location.0,
                    path.1.location.1,
                    theme.path,
                ));
                context.draw(&Points {
                    color: theme.source,
                    coords: &[path.0.location], // sydney
                });
                context.draw(&Points {
                    color: theme.destination,
                    coords: &[path.1.location], // perth
                });
            }
        })
        .render(area, buf);
}

#[derive(Debug)]
struct Hop {
    host: &'static str,
    address: &'static str,
    location: (f64, f64),
}

impl Hop {
    const fn new(name: &'static str, address: &'static str, location: (f64, f64)) -> Self {
        Self {
            host: name,
            address,
            location,
        }
    }
}

const CANBERRA: (f64, f64) = (149.1, -35.3);
const SYDNEY: (f64, f64) = (151.1, -33.9);
const MELBOURNE: (f64, f64) = (144.9, -37.8);
const PERTH: (f64, f64) = (115.9, -31.9);
const DARWIN: (f64, f64) = (130.8, -12.4);
const BRISBANE: (f64, f64) = (153.0, -27.5);
const ADELAIDE: (f64, f64) = (138.6, -34.9);

// Go traceroute bad.horse some time, it's fun. these locations are made up and don't correspond
// to the actual IP addresses (which are in Toronto, Canada).
const HOPS: &[Hop] = &[
    Hop::new("home", "127.0.0.1", CANBERRA),
    Hop::new("bad.horse", "162.252.205.130", SYDNEY),
    Hop::new("bad.horse", "162.252.205.131", MELBOURNE),
    Hop::new("bad.horse", "162.252.205.132", BRISBANE),
    Hop::new("bad.horse", "162.252.205.133", SYDNEY),
    Hop::new("he.rides.across.the.nation", "162.252.205.134", PERTH),
    Hop::new("the.thoroughbred.of.sin", "162.252.205.135", DARWIN),
    Hop::new("he.got.the.application", "162.252.205.136", BRISBANE),
    Hop::new("that.you.just.sent.in", "162.252.205.137", ADELAIDE),
    Hop::new("it.needs.evaluation", "162.252.205.138", DARWIN),
    Hop::new("so.let.the.games.begin", "162.252.205.139", PERTH),
    Hop::new("a.heinous.crime", "162.252.205.140", BRISBANE),
    Hop::new("a.show.of.force", "162.252.205.141", CANBERRA),
    Hop::new("a.murder.would.be.nice.of.course", "162.252.205.142", PERTH),
    Hop::new("bad.horse", "162.252.205.143", MELBOURNE),
    Hop::new("bad.horse", "162.252.205.144", DARWIN),
    Hop::new("bad.horse", "162.252.205.145", MELBOURNE),
    Hop::new("he-s.bad", "162.252.205.146", PERTH),
    Hop::new("the.evil.league.of.evil", "162.252.205.147", BRISBANE),
    Hop::new("is.watching.so.beware", "162.252.205.148", DARWIN),
    Hop::new("the.grade.that.you.receive", "162.252.205.149", PERTH),
    Hop::new("will.be.your.last.we.swear", "162.252.205.150", ADELAIDE),
    Hop::new("so.make.the.bad.horse.gleeful", "162.252.205.151", SYDNEY),
    Hop::new("or.he-ll.make.you.his.mare", "162.252.205.152", MELBOURNE),
    Hop::new("o_o", "162.252.205.153", BRISBANE),
    Hop::new("you-re.saddled.up", "162.252.205.154", DARWIN),
    Hop::new("there-s.no.recourse", "162.252.205.155", PERTH),
    Hop::new("it-s.hi-ho.silver", "162.252.205.156", SYDNEY),
    Hop::new("signed.bad.horse", "162.252.205.157", CANBERRA),
];
