//! Generates graphs.html for a replay job: swim-lane timeline + system resource charts.

use crate::system_monitor::{MonitorData, SeriesData, TraceEvent};
use plotters::prelude::*;
use std::fmt::Write as _;

struct ChartSeries {
    label: String,
    color: String,
    points: SeriesData<f64>,
}

impl ChartSeries {
    fn new(label: &str, color: &str, points: SeriesData<f64>) -> Self {
        ChartSeries {
            label: label.into(),
            color: color.into(),
            points,
        }
    }
}

/// The time at which a trace begins occupying a lane.
///
/// When the download takes at least a second, we use `download_start_ms` so
/// that a trace whose download overlaps another trace's replay gets its own
/// lane (preventing download bars from visually obscuring replay bars in the
/// same row).  For instant cache hits (`--traces-db`) the download duration is
/// near zero, so we fall back to `replay_start_ms` to keep sequential traces
/// packed into a single lane.
fn lane_start(e: &TraceEvent) -> u64 {
    let dl_ms = e.download_end_ms.saturating_sub(e.download_start_ms);
    if dl_ms >= 1000 {
        e.download_start_ms
    } else {
        e.replay_start_ms
    }
}

/// Assign each trace event to a swim lane using interval packing.
/// Returns a Vec of (lane_index, event) pairs, and the total number of lanes.
fn assign_lanes(events: &[TraceEvent]) -> (Vec<(usize, &TraceEvent)>, usize) {
    let mut sorted: Vec<&TraceEvent> = events.iter().collect();
    sorted.sort_by_key(|e| lane_start(e));

    // lane_end[i] = replay_end_ms of the last trace placed in lane i.
    let mut lane_end: Vec<u64> = Vec::new();
    let mut assigned: Vec<(usize, &TraceEvent)> = Vec::new();

    for event in sorted {
        let start = lane_start(event);
        let lane = lane_end
            .iter()
            .position(|&end| end <= start)
            .unwrap_or_else(|| {
                lane_end.push(0);
                lane_end.len() - 1
            });
        lane_end[lane] = event.replay_end_ms;
        assigned.push((lane, event));
    }

    let num_lanes = lane_end.len().max(1);
    (assigned, num_lanes)
}

/// Build an inline SVG string for the swim-lane replay timeline.
fn build_swimlane_svg(events: &[TraceEvent], total_duration_ms: u64) -> String {
    if events.is_empty() {
        return String::new();
    }

    let (assigned, num_lanes) = assign_lanes(events);

    let row_h: u32 = 24;
    let axis_h: u32 = 30;
    let top_padding: u32 = 10;
    // Match the plotters line charts exactly: same total width, same left/right margins.
    // plotters uses margin(10) + y_label_area_size(50) on the left, margin(10) on the right.
    let total_w: u32 = 1000;
    let left_offset: u32 = 60; // 10 outer margin + 50 y-label area
    let right_margin: u32 = 10;
    let chart_w: u32 = total_w - left_offset - right_margin;
    let chart_h: u32 = num_lanes as u32 * row_h + axis_h + top_padding;

    let total_ms = total_duration_ms.max(1) as f64;

    let x_of = |ms: u64| -> f64 { left_offset as f64 + (ms as f64 / total_ms) * chart_w as f64 };
    let y_of = |lane: usize| -> f64 { top_padding as f64 + lane as f64 * row_h as f64 };
    let mut svg = String::new();
    let _ = write!(
        svg,
        "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{total_w}\" height=\"{chart_h}\" \
         style=\"display:block;max-width:100%\">"
    );

    // Background
    let _ = write!(
        svg,
        "<rect width=\"100%\" height=\"100%\" fill=\"#f8f9fa\"/>"
    );

    // Axis ticks and labels
    let tick_count = 10u32;
    for i in 0..=tick_count {
        let ms = total_duration_ms * i as u64 / tick_count as u64;
        let x = x_of(ms) as u32;
        let y0 = top_padding;
        let y1 = chart_h - axis_h;
        let yt = chart_h - axis_h + 4 + 12;
        let s = ms as f64 / 1000.0;
        let _ = write!(
            svg,
            "<line x1=\"{x}\" y1=\"{y0}\" x2=\"{x}\" y2=\"{y1}\" \
             stroke=\"#ccc\" stroke-width=\"1\"/>"
        );
        let _ = write!(
            svg,
            "<text x=\"{x}\" y=\"{yt}\" text-anchor=\"middle\" \
             font-size=\"10\" fill=\"#666\">{s:.0}s</text>"
        );
    }

    // Bars
    for (lane, event) in &assigned {
        let dl_x = x_of(event.download_start_ms);
        let dl_w = (x_of(event.download_end_ms) - x_of(event.download_start_ms)).max(1.0);
        let rp_x = x_of(event.replay_start_ms);
        let rp_w = (x_of(event.replay_end_ms) - x_of(event.replay_start_ms)).max(1.0);
        let y = y_of(*lane) + 1.0;
        let bar_h = (row_h - 2) as f64;

        let replay_color = if event.passed { "#4c8c4a" } else { "#c0392b" };
        let basename = event
            .trace_name
            .rsplit('/')
            .next()
            .unwrap_or(&event.trace_name);
        let escaped_name = html_escape::encode_text(&event.trace_name);
        let escaped_base = html_escape::encode_text(basename);

        // Download bar (blue)
        let _ = write!(
            svg,
            "<rect x=\"{dl_x:.1}\" y=\"{y:.1}\" width=\"{dl_w:.1}\" height=\"{bar_h:.1}\" \
             fill=\"#5b9bd5\" opacity=\"0.7\" stroke=\"black\" stroke-width=\"1\" \
             data-label=\"{escaped_name} (download)\">\
             <title>{escaped_name} (download)</title></rect>"
        );

        // Replay bar (green/red)
        let _ = write!(
            svg,
            "<rect x=\"{rp_x:.1}\" y=\"{y:.1}\" width=\"{rp_w:.1}\" height=\"{bar_h:.1}\" \
             fill=\"{replay_color}\" stroke=\"black\" stroke-width=\"1\" \
             data-label=\"{escaped_name}\">\
             <title>{escaped_name}</title></rect>"
        );

        // Label clipped to replay bar
        let clip_id = format!("c{lane}_{rp_x:.0}");
        let tx = rp_x + 3.0;
        let ty = y + bar_h / 2.0;
        let _ = write!(
            svg,
            "<clipPath id=\"{clip_id}\"><rect x=\"{rp_x:.1}\" y=\"{y:.1}\" \
             width=\"{rp_w:.1}\" height=\"{bar_h:.1}\"/></clipPath>"
        );
        let _ = write!(
            svg,
            "<text x=\"{tx:.1}\" y=\"{ty:.1}\" font-size=\"10\" fill=\"white\" \
             clip-path=\"url(#{clip_id})\" dominant-baseline=\"middle\">{escaped_base}</text>"
        );
    }

    svg.push_str("</svg>");
    svg
}

/// Build a plotters SVG line chart. Returns the SVG string.
fn build_line_chart_svg(
    title: &str,
    series: &[ChartSeries],
    y_label: &str,
    y_max: f64,
    width: u32,
    height: u32,
) -> Result<String, Box<dyn std::error::Error>> {
    let mut svg_str = String::new();
    {
        let root = SVGBackend::with_string(&mut svg_str, (width, height)).into_drawing_area();
        root.fill(&WHITE)?;

        let x_max = series
            .iter()
            .flat_map(|s| s.points.iter_secs().map(|(t, _)| t))
            .fold(0.0f64, f64::max);

        let mut chart = ChartBuilder::on(&root)
            .caption(title, ("sans-serif", 14))
            .margin(10)
            .x_label_area_size(30)
            .y_label_area_size(50)
            .build_cartesian_2d(0.0f64..x_max.max(1.0), 0.0f64..y_max)?;

        chart
            .configure_mesh()
            .x_desc("Time (s)")
            .y_desc(y_label)
            .draw()?;

        for s in series {
            let color = parse_hex_color(&s.color).unwrap_or(RED);
            chart
                .draw_series(LineSeries::new(s.points.iter_secs(), color.stroke_width(2)))?
                .label(&s.label)
                .legend(move |(x, y)| {
                    PathElement::new(vec![(x, y), (x + 20, y)], color.stroke_width(2))
                });
        }

        chart
            .configure_series_labels()
            .background_style(WHITE.mix(0.8))
            .border_style(BLACK)
            .draw()?;

        root.present()?;
    }
    Ok(svg_str)
}

/// Build a combined GPU frequency (left Y, MHz) + temperature (right Y, °C) chart.
///
/// Renders a dual-axis chart when both kinds of data are present, or falls back
/// to a single-axis chart when only one kind is available.
fn build_freq_temp_svg(
    data: &MonitorData,
    width: u32,
    height: u32,
) -> Result<String, Box<dyn std::error::Error>> {
    let has_freq = !data.gpu_freq_mhz.is_empty();
    let has_mem_freq = !data.gpu_mem_freq_mhz.is_empty();
    let has_cpu_temp = !data.cpu_temp_c.is_empty();
    let has_gpu_temp = !data.gpu_temp_c.is_empty();
    let has_temp = has_cpu_temp || has_gpu_temp;

    let freq_max = (data.gpu_freq_max_mhz.unwrap_or(0) as f64)
        .max(data.gpu_freq_mhz.max_value())
        .max(data.gpu_mem_freq_mhz.max_value())
        .max(1000.0);

    // Single-axis fallbacks.
    if (has_freq || has_mem_freq) && !has_temp {
        let mut series: Vec<ChartSeries> = Vec::new();
        if has_freq {
            series.push(ChartSeries::new(
                "GFX MHz",
                "#2980b9",
                data.gpu_freq_mhz.clone(),
            ));
        }
        if has_mem_freq {
            series.push(ChartSeries::new(
                "Mem MHz",
                "#27ae60",
                data.gpu_mem_freq_mhz.clone(),
            ));
        }
        return build_line_chart_svg(
            "GPU Frequency",
            &series,
            "MHz",
            freq_max * 1.05,
            width,
            height,
        );
    }
    if !has_freq && has_temp {
        let temp_max = data.cpu_temp_c.max_value().max(data.gpu_temp_c.max_value());
        let mut series: Vec<ChartSeries> = Vec::new();
        if has_cpu_temp {
            series.push(ChartSeries::new(
                "CPU \u{00b0}C",
                "#e74c3c",
                data.cpu_temp_c.clone(),
            ));
        }
        if has_gpu_temp {
            series.push(ChartSeries::new(
                "GPU \u{00b0}C",
                "#8e44ad",
                data.gpu_temp_c.clone(),
            ));
        }
        return build_line_chart_svg(
            "Temperature",
            &series,
            "\u{00b0}C",
            (temp_max * 1.1).max(50.0),
            width,
            height,
        );
    }

    // Dual-axis: MHz on left, °C on right.
    let mut svg_str = String::new();
    {
        let root = SVGBackend::with_string(&mut svg_str, (width, height)).into_drawing_area();
        root.fill(&WHITE)?;

        let x_max = [
            &data.gpu_freq_mhz,
            &data.gpu_mem_freq_mhz,
            &data.cpu_temp_c,
            &data.gpu_temp_c,
        ]
        .iter()
        .flat_map(|s| s.iter_secs().map(|(t, _)| t))
        .fold(0.0f64, f64::max)
        .max(1.0);
        let temp_max =
            (data.cpu_temp_c.max_value().max(data.gpu_temp_c.max_value()) * 1.1).max(50.0);

        let chart = ChartBuilder::on(&root)
            .caption("GPU Frequency & Temperature", ("sans-serif", 14))
            .margin(10)
            .x_label_area_size(30)
            .y_label_area_size(50)
            .right_y_label_area_size(60)
            .build_cartesian_2d(0.0f64..x_max, 0.0f64..freq_max * 1.05)?;
        let mut chart = chart.set_secondary_coord(0.0f64..x_max, 0.0f64..temp_max);

        chart
            .configure_mesh()
            .x_desc("Time (s)")
            .y_desc("MHz")
            .draw()?;
        chart
            .configure_secondary_axes()
            .y_desc("\u{00b0}C")
            .draw()?;

        if has_freq {
            let freq_color = parse_hex_color("#2980b9").unwrap_or(BLUE);
            chart
                .draw_series(LineSeries::new(
                    data.gpu_freq_mhz.iter_secs(),
                    freq_color.stroke_width(2),
                ))?
                .label("GFX MHz")
                .legend(move |(x, y)| {
                    PathElement::new(vec![(x, y), (x + 20, y)], freq_color.stroke_width(2))
                });
        }
        if has_mem_freq {
            let mem_color = parse_hex_color("#27ae60").unwrap_or(GREEN);
            chart
                .draw_series(LineSeries::new(
                    data.gpu_mem_freq_mhz.iter_secs(),
                    mem_color.stroke_width(2),
                ))?
                .label("Mem MHz")
                .legend(move |(x, y)| {
                    PathElement::new(vec![(x, y), (x + 20, y)], mem_color.stroke_width(2))
                });
        }
        if has_cpu_temp {
            let color = parse_hex_color("#e74c3c").unwrap_or(RED);
            chart
                .draw_secondary_series(LineSeries::new(
                    data.cpu_temp_c.iter_secs(),
                    color.stroke_width(2),
                ))?
                .label("CPU \u{00b0}C")
                .legend(move |(x, y)| {
                    PathElement::new(vec![(x, y), (x + 20, y)], color.stroke_width(2))
                });
        }
        if has_gpu_temp {
            let color = parse_hex_color("#8e44ad").unwrap_or(RED);
            chart
                .draw_secondary_series(LineSeries::new(
                    data.gpu_temp_c.iter_secs(),
                    color.stroke_width(2),
                ))?
                .label("GPU \u{00b0}C")
                .legend(move |(x, y)| {
                    PathElement::new(vec![(x, y), (x + 20, y)], color.stroke_width(2))
                });
        }

        chart
            .configure_series_labels()
            .background_style(WHITE.mix(0.8))
            .border_style(BLACK)
            .draw()?;
        root.present()?;
    }
    Ok(svg_str)
}

fn parse_hex_color(hex: &str) -> Option<RGBColor> {
    let hex = hex.trim_start_matches('#');
    if hex.len() != 6 {
        return None;
    }
    let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
    let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
    let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
    Some(RGBColor(r, g, b))
}

/// Build the full graphs.html string.
pub fn build_graphs_html(data: &MonitorData, events: &[TraceEvent]) -> anyhow::Result<String> {
    let swimlane_svg = build_swimlane_svg(events, data.duration_ms());

    // System chart: CPU%, GPU busy%, active replays, active downloads
    let max_active = data
        .active_replays
        .max_value()
        .max(data.active_downloads.max_value());

    let mut system_series: Vec<ChartSeries> = vec![
        ChartSeries::new("CPU %", "#e74c3c", data.cpu_pct.clone()),
        ChartSeries::new("Active replays", "#2ecc71", data.active_replays.clone()),
        ChartSeries::new("Active downloads", "#3498db", data.active_downloads.clone()),
    ];
    if !data.gpu_busy_pct.is_empty() {
        system_series.push(ChartSeries::new(
            "GPU %",
            "#f39c12",
            data.gpu_busy_pct.clone(),
        ));
    }

    let system_svg = build_line_chart_svg(
        "System Resources",
        &system_series,
        "Value",
        100.0f64.max(max_active + 1.0),
        1000,
        300,
    )
    .unwrap_or_default();

    // Memory chart — system + GPU (unified pool on tested hardware)
    let mem_max = data.mem_available_mb.max_value().max(1.0);
    let swap_max = data.swap_used_mb.max_value();
    let gpu_mem_max = data.gpu_mem_mb.max_value();

    let mut mem_series: Vec<ChartSeries> = vec![
        ChartSeries::new("Available MiB", "#27ae60", data.mem_available_mb.clone()),
        ChartSeries::new("Swap used MiB", "#e67e22", data.swap_used_mb.clone()),
    ];
    if !data.gpu_mem_mb.is_empty() {
        mem_series.push(ChartSeries::new(
            "GPU used MiB",
            "#8e44ad",
            data.gpu_mem_mb.clone(),
        ));
    }

    let mem_svg = build_line_chart_svg(
        "Memory",
        &mem_series,
        "MiB",
        (mem_max + swap_max + gpu_mem_max + 1.0) * 1.05,
        1000,
        300,
    )
    .unwrap_or_default();

    // GPU frequency + temperature chart (optional, shown if either kind of data is present)
    let has_gpu_freq = !data.gpu_freq_mhz.is_empty() || !data.gpu_mem_freq_mhz.is_empty();
    let has_any_temp = !data.cpu_temp_c.is_empty() || !data.gpu_temp_c.is_empty();
    let freq_temp_svg = if has_gpu_freq || has_any_temp {
        build_freq_temp_svg(data, 1000, 300).unwrap_or_default()
    } else {
        String::new()
    };
    let freq_temp_section_title = match (has_gpu_freq, has_any_temp) {
        (true, true) => "GPU Frequency &amp; Temperature",
        (true, false) => "GPU Frequency",
        _ => "Temperature",
    };

    let html = format!(
        r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Replay Job Graphs</title>
<style>
body {{ font-family: sans-serif; max-width: 1100px; margin: 0 auto; padding: 1em; }}
h1 {{ font-size: 1.4em; }}
h2 {{ font-size: 1.1em; margin-top: 2em; }}
.chart {{ width: 100%; overflow-x: auto; margin-bottom: 1em; }}
.legend {{ font-size: 0.85em; color: #555; margin-bottom: 0.5em; }}
.legend span {{ display: inline-block; width: 14px; height: 14px; border-radius: 2px; vertical-align: middle; margin-right: 4px; }}
#svg-tooltip {{ position: fixed; background: rgba(0,0,0,0.75); color: #fff; padding: 3px 7px;
  border-radius: 4px; font-size: 0.8em; pointer-events: none; white-space: nowrap;
  display: none; z-index: 100; }}
</style>
</head>
<body>
<h1>Replay Job Graphs</h1>

<h2>Trace Timeline</h2>
<div class="legend">
  <span style="background:#5b9bd5"></span>Download&nbsp;&nbsp;
  <span style="background:#4c8c4a"></span>Replay (pass)&nbsp;&nbsp;
  <span style="background:#c0392b"></span>Replay (fail)
</div>
<div class="chart">{swimlane}</div>

<h2>System Resources</h2>
<div class="chart">{system}</div>

<h2>Memory</h2>
<div class="chart">{mem}</div>

{freq_temp_section}
<div id="svg-tooltip"></div>
<script>
(function() {{
  var tip = document.getElementById('svg-tooltip');
  document.addEventListener('mouseover', function(e) {{
    var el = e.target.closest('[data-label]');
    if (el) {{ tip.textContent = el.dataset.label; tip.style.display = 'block'; }}
  }});
  document.addEventListener('mousemove', function(e) {{
    tip.style.left = (e.clientX + 12) + 'px';
    tip.style.top  = (e.clientY + 12) + 'px';
  }});
  document.addEventListener('mouseout', function(e) {{
    if (!e.target.closest('[data-label]')) tip.style.display = 'none';
  }});
}})();
</script>
</body>
</html>"#,
        swimlane = swimlane_svg,
        system = system_svg,
        mem = mem_svg,
        freq_temp_section = if has_gpu_freq || has_any_temp {
            format!("<h2>{freq_temp_section_title}</h2><div class=\"chart\">{freq_temp_svg}</div>")
        } else {
            String::new()
        },
    );

    Ok(html)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::system_monitor::{MonitorData, TraceEvent};

    fn make_event(name: &str, dl_s: u64, dl_e: u64, rp_s: u64, rp_e: u64) -> TraceEvent {
        TraceEvent {
            trace_name: name.to_string(),
            download_start_ms: dl_s * 1000,
            download_end_ms: dl_e * 1000,
            replay_start_ms: rp_s * 1000,
            replay_end_ms: rp_e * 1000,
            passed: true,
        }
    }

    #[test]
    fn test_assign_lanes_sequential() {
        // B's replay starts after A's replay ends: they share one lane.
        let events = vec![make_event("a", 0, 0, 0, 3), make_event("b", 0, 0, 4, 7)];
        let (assigned, num_lanes) = assign_lanes(&events);
        assert_eq!(num_lanes, 1);
        assert!(assigned.iter().all(|(lane, _)| *lane == 0));
    }

    #[test]
    fn test_assign_lanes_traces_db() {
        // Simulates --traces-db: instant downloads (dl_start == dl_end == 0),
        // replay semaphore limits to 1 job, traces run one after another.
        let events = vec![
            make_event("a", 0, 0, 0, 10),
            make_event("b", 0, 0, 10, 20),
            make_event("c", 0, 0, 20, 30),
        ];
        let (_, num_lanes) = assign_lanes(&events);
        assert_eq!(num_lanes, 1);
    }

    #[test]
    fn test_assign_lanes_parallel() {
        // Two replays that overlap need two lanes.
        let events = vec![make_event("a", 0, 0, 0, 10), make_event("b", 0, 0, 0, 10)];
        let (_, num_lanes) = assign_lanes(&events);
        assert_eq!(num_lanes, 2);
    }

    #[test]
    fn test_assign_lanes_pipelined_download() {
        // B starts a real (>1s) download while A is still downloading/replaying.
        // They must get separate lanes so B's download bar doesn't obscure A's replay bar.
        let events = vec![
            make_event("a", 0, 30, 30, 33), // 30s download, short replay
            make_event("b", 5, 7, 35, 60),  // 2s download starts during A's download
        ];
        let (_, num_lanes) = assign_lanes(&events);
        assert_eq!(num_lanes, 2);
    }

    #[test]
    fn test_assign_lanes_sequential_after_real_download() {
        // C starts its download after A has fully finished — should reuse A's lane.
        let events = vec![
            make_event("a", 0, 10, 10, 20),  // finishes at t=20
            make_event("c", 25, 27, 27, 40), // starts downloading at t=25, after A done
        ];
        let (_, num_lanes) = assign_lanes(&events);
        assert_eq!(num_lanes, 1);
    }

    #[test]
    fn test_build_graphs_html_empty() {
        // Should not panic on empty data.
        let html = build_graphs_html(&MonitorData::new(None), &[]).unwrap();
        assert!(html.contains("<title>Replay Job Graphs</title>"));
    }

    #[test]
    fn test_build_graphs_html_with_data() {
        let events = vec![
            make_event("game/trace-a.gfxr", 0, 2, 2, 8),
            make_event("game/trace-b.gfxr", 0, 3, 3, 6),
        ];
        let html = build_graphs_html(&MonitorData::new(None), &events).unwrap();
        assert!(html.contains("trace-a.gfxr"));
        assert!(html.contains("trace-b.gfxr"));
    }
}
