use std::borrow::Cow;

use ratatui::{
    Frame,
    layout::{Constraint, Direction, Layout, Rect},
    text::Text,
    widgets::{Row, Table},
};

use crate::{
    app::{App, AppConfigFields, AxisScaling},
    canvas::{
        Painter,
        components::time_series::{AxisBound, ChartScaling, GraphData, LegendConstraints},
        drawing_utils::{should_hide_x_label, widget_block},
        widgets::{PacketInfo, calculate_packet_info},
    },
    components::time_series::GraphDrawCtx,
    utils::{
        data_units::*,
        general::{saturating_log2, saturating_log10},
    },
};

impl Painter {
    pub fn draw_network(
        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
    ) {
        if app_state.app_config_fields.use_old_network_legend {
            const LEGEND_HEIGHT: u16 = 4;
            let network_chunk = Layout::default()
                .direction(Direction::Vertical)
                .margin(0)
                .constraints([
                    Constraint::Length(draw_loc.height.saturating_sub(LEGEND_HEIGHT)),
                    Constraint::Length(LEGEND_HEIGHT),
                ])
                .split(draw_loc);

            self.draw_network_graph(f, app_state, network_chunk[0], widget_id);
            self.draw_old_network_labels(f, app_state, network_chunk[1], widget_id);
        } else {
            self.draw_network_graph(f, app_state, draw_loc, widget_id);
        }

        if app_state.should_get_widget_bounds() {
            // Update draw loc in widget map
            // Note that in both cases, we always go to the same widget id so it's fine to
            // do it like this lol.
            if let Some(network_widget) = app_state.widget_map.get_mut(&widget_id) {
                network_widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
                network_widget.bottom_right_corner =
                    Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
            }
        }
    }

    pub fn draw_network_graph(
        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
    ) {
        if let Some(network_widget_state) =
            app_state.states.net_state.widget_states.get_mut(&widget_id)
        {
            let shared_data = app_state.data_store.get_data();
            let network_latest_data = &(shared_data.network_harvest);
            let rx_points = &(shared_data.time_series_data.rx);
            let tx_points = &(shared_data.time_series_data.tx);
            let times = &(shared_data.time_series_data.time);

            let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
            let hide_x_labels = should_hide_x_label(
                app_state.app_config_fields.hide_time,
                app_state.app_config_fields.autohide_time,
                network_widget_state.graph.state_mut().autohide_timer_mut(),
                draw_loc,
            );

            let y_max = network_widget_state
                .graph
                .y_max([rx_points, tx_points].into_iter(), times);
            let (adjusted_y_max, y_labels) =
                adjust_network_data_point(y_max, &app_state.app_config_fields);
            let y_bounds = AxisBound::Max(adjusted_y_max);

            let use_old_network_legend = app_state.app_config_fields.use_old_network_legend;
            let legend_constraints = if use_old_network_legend {
                // Always hide it. Note that I could pass in `None` to the position as well but
                // eh this works.
                LegendConstraints {
                    width: Constraint::Length(0),
                    height: Constraint::Length(0),
                }
            } else {
                // Hide the legend if the width is 90% of the total widget width
                // or the height is greater than 75% of the total widget height.
                LegendConstraints {
                    width: Constraint::Ratio(9, 10),
                    height: Constraint::Ratio(3, 4),
                }
            };

            // TODO: Add support for clicking on legend to only show that value on chart.
            let use_binary_prefix = app_state.app_config_fields.network_use_binary_prefix;
            let unit_type = app_state.app_config_fields.network_unit_type;
            let unit = match unit_type {
                DataUnit::Byte => "B/s",
                DataUnit::Bit => "b/s",
            };

            let rx = get_unit_prefix(network_latest_data.rx, use_binary_prefix);
            let tx = get_unit_prefix(network_latest_data.tx, use_binary_prefix);
            let total_rx = convert_bits(network_latest_data.total_rx, use_binary_prefix);
            let total_tx = convert_bits(network_latest_data.total_tx, use_binary_prefix);

            let graph_data = if use_old_network_legend {
                let mut graph_data = vec![
                    GraphData::default()
                        .time(times)
                        .values(rx_points)
                        .style(self.styles.rx_style),
                    GraphData::default()
                        .time(times)
                        .values(tx_points)
                        .style(self.styles.tx_style),
                ];

                graph_data.extend([
                    GraphData::default().style(self.styles.total_rx_style),
                    GraphData::default().style(self.styles.total_tx_style),
                ]);

                graph_data
            } else {
                let rx_label = format!("{:.1}{}{}", rx.0, rx.1, unit);
                let tx_label = format!("{:.1}{}{}", tx.0, tx.1, unit);
                let total_rx_label = format!("{:.1}{}", total_rx.0, total_rx.1);
                let total_tx_label = format!("{:.1}{}", total_tx.0, total_tx.1);

                // Add packets information if enabled and there's enough room.
                const MAX_LEGEND_WIDTH: u16 = 70;
                let approx_legend_width = draw_loc.width * 3 / 4;

                // FIXME: I'm not really a huge fan of this - I think it may be better to just
                // not support this and allow for more easily spawning a
                // separate legend table (basically old legend).
                if app_state.app_config_fields.network_show_packets
                    && approx_legend_width > MAX_LEGEND_WIDTH
                {
                    let PacketInfo {
                        rx_packet_rate,
                        tx_packet_rate,
                        avg_rx_packet_size,
                        avg_tx_packet_size,
                    } = calculate_packet_info(network_latest_data, use_binary_prefix);

                    let avg_rx_packet_size_label =
                        format!("{:.1}{}", avg_rx_packet_size.0, avg_rx_packet_size.1);
                    let avg_tx_packet_size_label =
                        format!("{:.1}{}", avg_tx_packet_size.0, avg_tx_packet_size.1);

                    vec![
                            GraphData::default()
                                .name(format!("RX: {rx_label:<10} All: {total_rx_label:<8} Packets: {rx_packet_rate:>8}pkt/s Avg: {avg_rx_packet_size_label}").into())
                                .time(times)
                                .values(rx_points)
                                .style(self.styles.rx_style),
                            GraphData::default()
                                .name(format!("TX: {tx_label:<10} All: {total_tx_label:<8} Packets: {tx_packet_rate:>8}pkt/s Avg: {avg_tx_packet_size_label}").into())
                                .time(times)
                                .values(tx_points)
                                .style(self.styles.tx_style),
                        ]
                } else {
                    vec![
                        GraphData::default()
                            .name(format!("RX: {rx_label:<10} All: {total_rx_label}").into())
                            .time(times)
                            .values(rx_points)
                            .style(self.styles.rx_style),
                        GraphData::default()
                            .name(format!("TX: {tx_label:<10} All: {total_tx_label}").into())
                            .time(times)
                            .values(tx_points)
                            .style(self.styles.tx_style),
                    ]
                }
            };

            let marker = self.get_marker(app_state.app_config_fields.use_dot);

            let scaling = match app_state.app_config_fields.network_scale_type {
                AxisScaling::Log => {
                    // TODO: I might change this behaviour later.
                    if app_state.app_config_fields.network_use_binary_prefix {
                        ChartScaling::Log2
                    } else {
                        ChartScaling::Log10
                    }
                }
                AxisScaling::Linear => ChartScaling::Linear,
            };

            let y_labels: Vec<Cow<'_, str>> = y_labels.into_iter().map(Into::into).collect();

            network_widget_state.graph.draw(
                f,
                draw_loc,
                GraphDrawCtx {
                    title: " Network ".into(),
                    border_style,
                    title_style: self.styles.widget_title_style,
                    graph_style: self.styles.graph_style,
                    general_widget_style: self.styles.general_widget_style,
                    border_type: self.styles.border_type,
                    marker,
                    hide_x_labels,
                    is_selected: app_state.current_widget.widget_id == widget_id,
                    is_expanded: app_state.is_expanded,
                    legend_position: app_state.app_config_fields.network_legend_position,
                    legend_constraints: Some(legend_constraints),
                },
                y_bounds,
                &y_labels,
                scaling,
                graph_data,
            );
        }
    }

    fn draw_old_network_labels(
        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
    ) {
        let network_latest_data = &(app_state.data_store.get_data().network_harvest);
        let use_binary_prefix = app_state.app_config_fields.network_use_binary_prefix;
        let unit_type = app_state.app_config_fields.network_unit_type;
        let unit = match unit_type {
            DataUnit::Byte => "B/s",
            DataUnit::Bit => "b/s",
        };

        let rx = get_unit_prefix(network_latest_data.rx, use_binary_prefix);
        let tx = get_unit_prefix(network_latest_data.tx, use_binary_prefix);

        let rx_label = format!("{:.1}{}{}", rx.0, rx.1, unit);
        let tx_label = format!("{:.1}{}{}", tx.0, tx.1, unit);

        let total_rx = convert_bits(network_latest_data.total_rx, use_binary_prefix);
        let total_tx = convert_bits(network_latest_data.total_tx, use_binary_prefix);
        let total_rx_label = format!("{:.1}{}", total_rx.0, total_rx.1);
        let total_tx_label = format!("{:.1}{}", total_tx.0, total_tx.1);

        let total_network = if app_state.app_config_fields.network_show_packets {
            let PacketInfo {
                rx_packet_rate,
                tx_packet_rate,
                avg_rx_packet_size,
                avg_tx_packet_size,
            } = calculate_packet_info(network_latest_data, use_binary_prefix);

            let avg_rx_packet_size_label =
                format!("{:.1}{}", avg_rx_packet_size.0, avg_rx_packet_size.1);
            let avg_tx_packet_size_label =
                format!("{:.1}{}", avg_tx_packet_size.0, avg_tx_packet_size.1);

            vec![Row::new([
                Text::styled(rx_label, self.styles.rx_style),
                Text::styled(tx_label, self.styles.tx_style),
                Text::styled(total_rx_label, self.styles.total_rx_style),
                Text::styled(total_tx_label, self.styles.total_tx_style),
                Text::styled(format!("{rx_packet_rate}pkt/s"), self.styles.rx_style),
                Text::styled(format!("{tx_packet_rate}pkt/s"), self.styles.tx_style),
                Text::styled(avg_rx_packet_size_label, self.styles.rx_style),
                Text::styled(avg_tx_packet_size_label, self.styles.tx_style),
            ])]
        } else {
            vec![Row::new([
                Text::styled(rx_label, self.styles.rx_style),
                Text::styled(tx_label, self.styles.tx_style),
                Text::styled(total_rx_label, self.styles.total_rx_style),
                Text::styled(total_tx_label, self.styles.total_tx_style),
            ])]
        };

        let headers = if app_state.app_config_fields.network_show_packets {
            vec![
                "RX", "TX", "Total RX", "Total TX", "RX Pkts", "TX Pkts", "Avg RX", "Avg TX",
            ]
        } else {
            vec!["RX", "TX", "Total RX", "Total TX"]
        };
        let num_columns = headers.len();

        let column_width = draw_loc.width.saturating_sub(2) / num_columns as u16;

        // Draw
        let current_border_style = if app_state.current_widget.widget_id == widget_id {
            self.styles.highlighted_border_style
        } else {
            self.styles.border_style
        };
        let block = widget_block(
            app_state.app_config_fields.use_basic_mode,
            app_state.current_widget.widget_id == widget_id,
            self.styles.border_type,
            self.styles.general_widget_style,
        )
        .border_style(current_border_style);

        f.render_widget(
            Table::new(
                total_network,
                &((std::iter::repeat_n(column_width, num_columns))
                    .map(Constraint::Length)
                    .collect::<Vec<_>>()),
            )
            .header(Row::new(headers).style(self.styles.table_header_style))
            .block(block)
            .style(self.styles.text_style),
            draw_loc,
        );
    }
}

/// Returns the required labels.
///
/// TODO: This is _really_ ugly... also there might be a bug with certain
/// heights and too many labels. We may need to take draw height into account,
/// either here, or in the time graph itself.
fn adjust_network_data_point(max_entry: f64, config: &AppConfigFields) -> (f64, Vec<String>) {
    // So, we're going with an approach like this for linear data:
    // - Main goal is to maximize the amount of information displayed given a
    //   specific height. We don't want to drown out some data if the ranges are too
    //   far though!  Nor do we want to filter out too much data...
    // - Change the y-axis unit (kilo/kibi, mega/mebi...) dynamically based on max
    //   load.
    //
    // The idea is we take the top value, build our scale such that each "point" is
    // a scaled version of that. So for example, let's say I use 390 Mb/s.  If I
    // drew 4 segments, it would be 97.5, 195, 292.5, 390, and
    // probably something like 438.75?
    //
    // So, how do we do this in ratatui?  Well, if we are using intervals that tie
    // in perfectly to the max value we want... then it's actually not that
    // hard. Since ratatui accepts a vector as labels and will properly space
    // them all out... we just work with that and space it out properly.
    //
    // Dynamic chart idea based off of FreeNAS's chart design.
    //
    // ---
    //
    // For log data, we just use the old method of log intervals
    // (kilo/mega/giga/etc.). Keep it nice and simple.

    // Now just check the largest unit we correspond to... then proceed to build
    // some entries from there!

    let scale_type = config.network_scale_type;
    let use_binary_prefix = config.network_use_binary_prefix;
    let network_unit_type = config.network_unit_type;

    let unit_char = match network_unit_type {
        DataUnit::Byte => "B",
        DataUnit::Bit => "b",
    };

    match scale_type {
        AxisScaling::Linear => {
            let (k_limit, m_limit, g_limit, t_limit) = if use_binary_prefix {
                (
                    KIBI_LIMIT_F64,
                    MEBI_LIMIT_F64,
                    GIBI_LIMIT_F64,
                    TEBI_LIMIT_F64,
                )
            } else {
                (
                    KILO_LIMIT_F64,
                    MEGA_LIMIT_F64,
                    GIGA_LIMIT_F64,
                    TERA_LIMIT_F64,
                )
            };

            let max_entry_upper = if max_entry == 0.0 {
                // If it's 0, then just use a very low value so the labels aren't just "0.0" 4
                // times. This _also_ prevents the y-axis height range ever
                // being 0.
                1.0
            } else {
                max_entry * 1.5 // We use the bumped up version to calculate our unit type.
            };

            let (max_value_scaled, unit_prefix, unit_type): (f64, &str, &str) = {
                if max_entry_upper < k_limit {
                    (max_entry, "", unit_char)
                } else if max_entry_upper < m_limit {
                    (
                        max_entry / k_limit,
                        if use_binary_prefix { "Ki" } else { "K" },
                        unit_char,
                    )
                } else if max_entry_upper < g_limit {
                    (
                        max_entry / m_limit,
                        if use_binary_prefix { "Mi" } else { "M" },
                        unit_char,
                    )
                } else if max_entry_upper < t_limit {
                    (
                        max_entry / g_limit,
                        if use_binary_prefix { "Gi" } else { "G" },
                        unit_char,
                    )
                } else {
                    (
                        max_entry / t_limit,
                        if use_binary_prefix { "Ti" } else { "T" },
                        unit_char,
                    )
                }
            };

            // Finally, build an acceptable range starting from there, using the given
            // height! Note we try to put more of a weight on the bottom section
            // vs. the top, since the top has less data.
            let base_unit = max_value_scaled;
            let labels: Vec<String> = vec![
                format!("0{unit_prefix}{unit_type}"),
                format!("{:.1}", base_unit * 0.5),
                format!("{:.1}", base_unit),
                format!("{:.1}", base_unit * 1.5),
            ]
            .into_iter()
            .map(|s| {
                // Pull 5 as the longest legend value is generally going to be 5 digits (if they
                // somehow hit over 5 terabits per second)
                format!("{s:>5}")
            })
            .collect();

            (max_entry_upper, labels)
        }
        AxisScaling::Log => {
            let (m_limit, g_limit, t_limit) = if use_binary_prefix {
                (LOG_MEBI_LIMIT, LOG_GIBI_LIMIT, LOG_TEBI_LIMIT)
            } else {
                (LOG_MEGA_LIMIT, LOG_GIGA_LIMIT, LOG_TERA_LIMIT)
            };

            // Remember to do saturating log checks as otherwise 0.0 becomes inf, and you
            // get gaps!
            let max_entry = if use_binary_prefix {
                saturating_log2(max_entry)
            } else {
                saturating_log10(max_entry)
            };

            fn get_zero(network_use_binary_prefix: bool, unit_char: &str) -> String {
                format!(
                    "{}0{}",
                    if network_use_binary_prefix { "  " } else { " " },
                    unit_char
                )
            }

            fn get_k(network_use_binary_prefix: bool, unit_char: &str) -> String {
                format!(
                    "1{}{}",
                    if network_use_binary_prefix { "Ki" } else { "K" },
                    unit_char
                )
            }

            fn get_m(network_use_binary_prefix: bool, unit_char: &str) -> String {
                format!(
                    "1{}{}",
                    if network_use_binary_prefix { "Mi" } else { "M" },
                    unit_char
                )
            }

            fn get_g(network_use_binary_prefix: bool, unit_char: &str) -> String {
                format!(
                    "1{}{}",
                    if network_use_binary_prefix { "Gi" } else { "G" },
                    unit_char
                )
            }

            fn get_t(network_use_binary_prefix: bool, unit_char: &str) -> String {
                format!(
                    "1{}{}",
                    if network_use_binary_prefix { "Ti" } else { "T" },
                    unit_char
                )
            }

            fn get_p(network_use_binary_prefix: bool, unit_char: &str) -> String {
                format!(
                    "1{}{}",
                    if network_use_binary_prefix { "Pi" } else { "P" },
                    unit_char
                )
            }

            if max_entry < m_limit {
                (
                    m_limit,
                    vec![
                        get_zero(use_binary_prefix, unit_char),
                        get_k(use_binary_prefix, unit_char),
                        get_m(use_binary_prefix, unit_char),
                    ],
                )
            } else if max_entry < g_limit {
                (
                    g_limit,
                    vec![
                        get_zero(use_binary_prefix, unit_char),
                        get_k(use_binary_prefix, unit_char),
                        get_m(use_binary_prefix, unit_char),
                        get_g(use_binary_prefix, unit_char),
                    ],
                )
            } else if max_entry < t_limit {
                (
                    t_limit,
                    vec![
                        get_zero(use_binary_prefix, unit_char),
                        get_k(use_binary_prefix, unit_char),
                        get_m(use_binary_prefix, unit_char),
                        get_g(use_binary_prefix, unit_char),
                        get_t(use_binary_prefix, unit_char),
                    ],
                )
            } else {
                // I really doubt anyone's transferring beyond petabyte speeds...
                (
                    if use_binary_prefix {
                        LOG_PEBI_LIMIT
                    } else {
                        LOG_PETA_LIMIT
                    },
                    vec![
                        get_zero(use_binary_prefix, unit_char),
                        get_k(use_binary_prefix, unit_char),
                        get_m(use_binary_prefix, unit_char),
                        get_g(use_binary_prefix, unit_char),
                        get_t(use_binary_prefix, unit_char),
                        get_p(use_binary_prefix, unit_char),
                    ],
                )
            }
        }
    }
}
