//! PDF Chart Generation
//!
//! Generates PDF content streams for common chart types: bar, line, pie, and scatter.

use crate::fitz::error::Result;
use std::fmt::Write;

/// Chart type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChartType {
    Bar,
    Line,
    Pie,
    Scatter,
}

/// Legend position
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LegendPosition {
    TopLeft,
    TopRight,
    BottomLeft,
    BottomRight,
}

/// Chart configuration
#[derive(Debug, Clone)]
pub struct ChartConfig {
    pub width: f64,
    pub height: f64,
    pub title: String,
    pub x_label: String,
    pub y_label: String,
    pub colors: Vec<(f64, f64, f64)>,
    pub font_size: f64,
    pub grid: bool,
    pub legend: bool,
}

impl Default for ChartConfig {
    fn default() -> Self {
        Self {
            width: 400.0,
            height: 300.0,
            title: String::new(),
            x_label: String::new(),
            y_label: String::new(),
            colors: vec![
                (0.2, 0.4, 0.8),
                (0.8, 0.2, 0.2),
                (0.2, 0.7, 0.3),
                (0.9, 0.6, 0.1),
                (0.6, 0.2, 0.8),
            ],
            font_size: 10.0,
            grid: true,
            legend: true,
        }
    }
}

/// Data series for charts
#[derive(Debug, Clone)]
pub struct DataSeries {
    pub label: String,
    pub values: Vec<f64>,
    pub color: Option<(f64, f64, f64)>,
}

impl DataSeries {
    pub fn new(label: impl Into<String>, values: Vec<f64>) -> Self {
        Self {
            label: label.into(),
            values,
            color: None,
        }
    }

    pub fn with_color(mut self, r: f64, g: f64, b: f64) -> Self {
        self.color = Some((r, g, b));
        self
    }
}

/// Chart definition
#[derive(Debug, Clone)]
pub struct Chart {
    pub chart_type: ChartType,
    pub config: ChartConfig,
    pub series: Vec<DataSeries>,
    pub categories: Vec<String>,
}

impl Chart {
    /// Generate PDF content stream bytes
    pub fn generate_chart_content_stream(&self) -> Result<Vec<u8>> {
        let mut out = String::new();

        out.push_str("q\n");

        let margin_left = 50.0;
        let margin_right = 30.0;
        let margin_top = 40.0;
        let margin_bottom = 50.0;

        let plot_left = margin_left;
        let plot_right = self.config.width - margin_right;
        let plot_bottom = margin_bottom;
        let plot_top = self.config.height - margin_top;

        let plot_w = plot_right - plot_left;
        let plot_h = plot_top - plot_bottom;

        // Title
        if !self.config.title.is_empty() {
            let _ = writeln!(
                out,
                "BT /Helv {} Tf {} {} Td ({}) Tj ET",
                self.config.font_size + 2.0,
                plot_left,
                self.config.height - 15.0,
                escape_pdf_string(&self.config.title)
            );
        }

        match self.chart_type {
            ChartType::Bar => {
                self.render_bar_chart(&mut out, plot_left, plot_bottom, plot_w, plot_h)?
            }
            ChartType::Line => {
                self.render_line_chart(&mut out, plot_left, plot_bottom, plot_w, plot_h)?
            }
            ChartType::Pie => {
                self.render_pie_chart(&mut out, plot_left, plot_bottom, plot_w, plot_h)?
            }
            ChartType::Scatter => {
                self.render_scatter_chart(&mut out, plot_left, plot_bottom, plot_w, plot_h)?
            }
        }

        // Legend
        if self.config.legend && !self.series.is_empty() {
            self.render_legend(&mut out, plot_right, plot_top)?;
        }

        out.push_str("Q\n");
        Ok(out.into_bytes())
    }

    fn get_color(&self, series_idx: usize, value_idx: usize) -> (f64, f64, f64) {
        self.series
            .get(series_idx)
            .and_then(|s| s.color)
            .unwrap_or_else(|| {
                self.config.colors[(series_idx + value_idx) % self.config.colors.len().max(1)]
            })
    }

    fn render_bar_chart(&self, out: &mut String, ox: f64, oy: f64, w: f64, h: f64) -> Result<()> {
        let total_bars: usize = self
            .series
            .iter()
            .map(|s| s.values.len())
            .max()
            .unwrap_or(0);
        if total_bars == 0 {
            return Ok(());
        }

        let (y_min, y_max, _ticks) = compute_axis_range(
            self.series
                .iter()
                .flat_map(|s| s.values.iter().copied())
                .collect::<Vec<_>>()
                .as_slice(),
        );
        let range = (y_max - y_min).max(0.0001);

        let bar_group_w = w / total_bars as f64;
        let num_series = self.series.len().max(1);
        let bar_w = bar_group_w * 0.7 / num_series as f64;
        let gap = bar_group_w * 0.15;

        // Grid
        if self.config.grid {
            let _ = writeln!(out, "0.9 0.9 0.9 RG 0.5 w");
            for i in 0..5 {
                let y = oy + (i as f64 / 4.0) * h;
                let _ = writeln!(out, "{} {} m {} {} l S", ox, y, ox + w, y);
            }
            for i in 0..=total_bars {
                let x = ox + (i as f64 / total_bars as f64) * w;
                let _ = writeln!(out, "{} {} m {} {} l S", x, oy, x, oy + h);
            }
        }

        // Bars
        for (si, series) in self.series.iter().enumerate() {
            let (cr, cg, cb) = self.get_color(si, 0);
            let _ = writeln!(out, "{:.2} {:.2} {:.2} rg", cr, cg, cb);
            for (i, &val) in series.values.iter().enumerate() {
                let norm = ((val - y_min) / range).clamp(0.0, 1.0);
                let bh = norm * h;
                let bx = ox + (i as f64 + 0.5) * bar_group_w - (num_series as f64 * bar_w) / 2.0
                    + si as f64 * bar_w
                    + gap / 2.0;
                let _ = writeln!(out, "{:.2} {:.2} {:.2} {:.2} re f", bx, oy, bar_w, bh);
            }
        }

        // Axis
        let _ = writeln!(out, "0 0 0 RG 1 w");
        let _ = writeln!(out, "{} {} m {} {} l S", ox, oy, ox, oy + h);
        let _ = writeln!(out, "{} {} m {} {} l S", ox, oy, ox + w, oy);

        // Axis labels
        if !self.config.y_label.is_empty() {
            let _ = writeln!(
                out,
                "BT /Helv {} Tf {} {} Td ({}) Tj ET",
                self.config.font_size - 2.0,
                ox - 45.0,
                oy + h / 2.0 - 20.0,
                escape_pdf_string(&self.config.y_label)
            );
        }
        if !self.config.x_label.is_empty() {
            let _ = writeln!(
                out,
                "BT /Helv {} Tf {} {} Td ({}) Tj ET",
                self.config.font_size - 2.0,
                ox + w / 2.0 - 20.0,
                oy - 25.0,
                escape_pdf_string(&self.config.x_label)
            );
        }

        Ok(())
    }

    fn render_line_chart(&self, out: &mut String, ox: f64, oy: f64, w: f64, h: f64) -> Result<()> {
        let all_vals: Vec<f64> = self
            .series
            .iter()
            .flat_map(|s| s.values.iter().copied())
            .collect();
        if all_vals.is_empty() {
            return Ok(());
        }

        let (y_min, y_max, _ticks) = compute_axis_range(&all_vals);
        let range = (y_max - y_min).max(0.0001);
        let max_points = self
            .series
            .iter()
            .map(|s| s.values.len())
            .max()
            .unwrap_or(1);

        // Grid
        if self.config.grid {
            let _ = writeln!(out, "0.9 0.9 0.9 RG 0.5 w");
            for i in 0..5 {
                let y = oy + (i as f64 / 4.0) * h;
                let _ = writeln!(out, "{} {} m {} {} l S", ox, y, ox + w, y);
            }
            for i in 0..=5 {
                let x = ox + (i as f64 / 5.0) * w;
                let _ = writeln!(out, "{} {} m {} {} l S", x, oy, x, oy + h);
            }
        }

        let step_x = if max_points > 1 {
            w / (max_points - 1) as f64
        } else {
            w
        };

        for (si, series) in self.series.iter().enumerate() {
            if series.values.is_empty() {
                continue;
            }
            let (cr, cg, cb) = self.get_color(si, 0);
            let _ = writeln!(out, "{:.2} {:.2} {:.2} RG 1.5 w", cr, cg, cb);

            let points: Vec<(f64, f64)> = series
                .values
                .iter()
                .enumerate()
                .map(|(i, &v)| {
                    let px = ox + i as f64 * step_x;
                    let py = oy + ((v - y_min) / range).clamp(0.0, 1.0) * h;
                    (px, py)
                })
                .collect();

            // Smooth curve with cubic Bezier
            if points.len() >= 2 {
                let _ = write!(out, "{:.2} {:.2} m\n", points[0].0, points[0].1);
                for i in 1..points.len() {
                    let (x0, y0) = points[i - 1];
                    let (x1, y1) = points[i];
                    let (x_prev, y_prev) = if i >= 2 { points[i - 2] } else { points[i - 1] };
                    let (x_next, y_next) = if i + 1 < points.len() {
                        points[i + 1]
                    } else {
                        points[i]
                    };
                    let cpx1 = x0 + (x1 - x_prev) / 6.0;
                    let cpy1 = y0 + (y1 - y_prev) / 6.0;
                    let cpx2 = x1 - (x_next - x0) / 6.0;
                    let cpy2 = y1 - (y_next - y0) / 6.0;
                    let _ = writeln!(
                        out,
                        "{:.2} {:.2} {:.2} {:.2} {:.2} {:.2} c",
                        cpx1, cpy1, cpx2, cpy2, x1, y1
                    );
                }
                let _ = writeln!(out, "S");
            } else if points.len() == 1 {
                let (px, py) = points[0];
                let _ = writeln!(out, "{:.2} {:.2} 2 0 360 arc f", px, py);
            }

            // Markers
            for (px, py) in &points {
                let _ = writeln!(out, "{:.2} {:.2} {:.2} rg", cr, cg, cb);
                let _ = writeln!(out, "{:.2} {:.2} 3 0 360 arc f", px, py);
            }
        }

        // Axis
        let _ = writeln!(out, "0 0 0 RG 1 w");
        let _ = writeln!(out, "{} {} m {} {} l S", ox, oy, ox, oy + h);
        let _ = writeln!(out, "{} {} m {} {} l S", ox, oy, ox + w, oy);

        Ok(())
    }

    fn render_pie_chart(&self, out: &mut String, ox: f64, oy: f64, w: f64, h: f64) -> Result<()> {
        let series = match self.series.first() {
            Some(s) if !s.values.is_empty() => s,
            _ => return Ok(()),
        };

        let total: f64 = series.values.iter().sum();
        if total <= 0.0 {
            return Ok(());
        }

        let cx = ox + w / 2.0;
        let cy = oy + h / 2.0;
        let radius = w.min(h) / 2.0 * 0.85;

        let mut start_angle = 0.0f64;

        for (i, &val) in series.values.iter().enumerate() {
            let pct = val / total;
            let sweep = pct * 360.0;
            let (cr, cg, cb) = self.get_color(0, i);
            let _ = writeln!(out, "{:.2} {:.2} {:.2} rg", cr, cg, cb);

            let start_rad = start_angle.to_radians();
            let _ = write!(out, "{:.2} {:.2} m\n", cx, cy);
            let _ = writeln!(
                out,
                "{:.2} {:.2} l",
                cx + radius * start_rad.cos(),
                cy + radius * start_rad.sin()
            );

            let steps = (sweep / 2.0).ceil().max(1.0) as usize;
            let angle_step = sweep / steps as f64;
            for step in 1..=steps {
                let a = (start_angle + step as f64 * angle_step).to_radians();
                let _ = writeln!(
                    out,
                    "{:.2} {:.2} l",
                    cx + radius * a.cos(),
                    cy + radius * a.sin()
                );
            }
            let _ = writeln!(out, "f");

            start_angle += sweep;
        }

        // Labels with percentages
        start_angle = 0.0;
        for (i, &val) in series.values.iter().enumerate() {
            let pct = val / total;
            let sweep = pct * 360.0;
            let mid_angle = (start_angle + sweep / 2.0).to_radians();
            let label_r = radius * 0.7;
            let lx = cx + label_r * mid_angle.cos();
            let ly = cy + label_r * mid_angle.sin();
            let label = if i < self.categories.len() {
                format!("{} {:.0}%", self.categories[i], pct * 100.0)
            } else {
                format!("{:.0}%", pct * 100.0)
            };
            let _ = writeln!(
                out,
                "BT /Helv {} Tf {} {} Td ({}) Tj ET",
                self.config.font_size - 1.0,
                lx - 15.0,
                ly - 4.0,
                escape_pdf_string(&label)
            );
            start_angle += sweep;
        }

        Ok(())
    }

    fn render_scatter_chart(
        &self,
        out: &mut String,
        ox: f64,
        oy: f64,
        w: f64,
        h: f64,
    ) -> Result<()> {
        let mut all_x: Vec<f64> = Vec::new();
        let mut all_y: Vec<f64> = Vec::new();
        for s in &self.series {
            if s.values.len() >= 2 {
                for chunk in s.values.chunks(2) {
                    if chunk.len() == 2 {
                        all_x.push(chunk[0]);
                        all_y.push(chunk[1]);
                    }
                }
            }
        }
        if all_x.is_empty() || all_y.is_empty() {
            return Ok(());
        }

        let (x_min, x_max, _) = compute_axis_range(&all_x);
        let (y_min, y_max, _) = compute_axis_range(&all_y);
        let x_range = (x_max - x_min).max(0.0001);
        let y_range = (y_max - y_min).max(0.0001);

        let to_x = |v: f64| ox + ((v - x_min) / x_range).clamp(0.0, 1.0) * w;
        let to_y = |v: f64| oy + ((v - y_min) / y_range).clamp(0.0, 1.0) * h;

        // Grid
        if self.config.grid {
            let _ = writeln!(out, "0.9 0.9 0.9 RG 0.5 w");
            for i in 0..5 {
                let x = ox + (i as f64 / 4.0) * w;
                let _ = writeln!(out, "{} {} m {} {} l S", x, oy, x, oy + h);
            }
            for i in 0..5 {
                let y = oy + (i as f64 / 4.0) * h;
                let _ = writeln!(out, "{} {} m {} {} l S", ox, y, ox + w, y);
            }
        }

        for (si, series) in self.series.iter().enumerate() {
            let points: Vec<(f64, f64)> = series
                .values
                .chunks(2)
                .filter_map(|c| {
                    if c.len() == 2 {
                        Some((to_x(c[0]), to_y(c[1])))
                    } else {
                        None
                    }
                })
                .collect();

            if points.is_empty() {
                continue;
            }

            let (cr, cg, cb) = self.get_color(si, 0);

            // Trend line (linear regression)
            if series.values.len() >= 4 {
                let (px, py): (Vec<f64>, Vec<f64>) = series
                    .values
                    .chunks(2)
                    .filter_map(|c| {
                        if c.len() == 2 {
                            Some((c[0], c[1]))
                        } else {
                            None
                        }
                    })
                    .unzip();
                if px.len() >= 2 {
                    let n = px.len() as f64;
                    let sum_x: f64 = px.iter().sum();
                    let sum_y: f64 = py.iter().sum();
                    let sum_xy: f64 = px.iter().zip(py.iter()).map(|(a, b)| a * b).sum();
                    let sum_x2: f64 = px.iter().map(|x| x * x).sum();
                    let slope =
                        (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x).max(1e-10);
                    let intercept = (sum_y - slope * sum_x) / n;
                    let y1 = to_y(slope * x_min + intercept);
                    let y2 = to_y(slope * x_max + intercept);
                    let _ = writeln!(out, "0.7 0.7 0.7 RG 0.5 w");
                    let _ = writeln!(out, "{} {} m {} {} l S", ox, y1, ox + w, y2);
                }
            }

            // Points
            let _ = writeln!(out, "{:.2} {:.2} {:.2} rg", cr, cg, cb);
            for (px, py) in &points {
                let _ = writeln!(out, "{:.2} {:.2} 4 0 360 arc f", px, py);
            }
        }

        // Axis
        let _ = writeln!(out, "0 0 0 RG 1 w");
        let _ = writeln!(out, "{} {} m {} {} l S", ox, oy, ox, oy + h);
        let _ = writeln!(out, "{} {} m {} {} l S", ox, oy, ox + w, oy);

        Ok(())
    }

    fn render_legend(&self, out: &mut String, right: f64, top: f64) -> Result<()> {
        let leg_x = right + 5.0;
        let mut leg_y = top - 10.0;
        let line_h = self.config.font_size + 4.0;

        for (i, s) in self.series.iter().enumerate() {
            let (cr, cg, cb) = self.get_color(i, 0);
            let _ = writeln!(out, "{:.2} {:.2} {:.2} rg", cr, cg, cb);
            let _ = writeln!(
                out,
                "{:.2} {:.2} {:.2} {:.2} re f",
                leg_x,
                leg_y - 3.0,
                12.0,
                8.0
            );
            let _ = writeln!(
                out,
                "BT /Helv {} Tf {:.2} {:.2} Td ({}) Tj ET",
                self.config.font_size - 1.0,
                leg_x + 15.0,
                leg_y - 2.0,
                escape_pdf_string(&s.label)
            );
            leg_y -= line_h;
        }
        Ok(())
    }
}

fn compute_axis_range(values: &[f64]) -> (f64, f64, Vec<f64>) {
    if values.is_empty() {
        return (0.0, 1.0, vec![0.0, 1.0]);
    }
    let min_val = values.iter().cloned().fold(f64::INFINITY, f64::min);
    let max_val = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
    if min_val >= max_val {
        let m = min_val;
        return (m - 0.5, m + 0.5, vec![m - 0.5, m, m + 0.5]);
    }
    let range = max_val - min_val;
    let magnitude = 10.0_f64.powf(range.log10().floor());
    let norm_range = range / magnitude;
    let tick_step = if norm_range <= 1.0 {
        0.2 * magnitude
    } else if norm_range <= 2.0 {
        0.5 * magnitude
    } else if norm_range <= 5.0 {
        1.0 * magnitude
    } else {
        2.0 * magnitude
    };
    let nice_min = (min_val / tick_step).floor() * tick_step;
    let nice_max = (max_val / tick_step).ceil() * tick_step;
    let mut ticks = Vec::new();
    let mut t = nice_min;
    while t <= nice_max {
        ticks.push(t);
        t += tick_step;
    }
    (nice_min, nice_max, ticks)
}

fn escape_pdf_string(s: &str) -> String {
    s.chars()
        .map(|c| match c {
            '\\' => "\\\\".to_string(),
            '(' => "\\(".to_string(),
            ')' => "\\)".to_string(),
            _ => c.to_string(),
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_bar_chart_single_series() {
        let chart = Chart {
            chart_type: ChartType::Bar,
            config: ChartConfig::default(),
            series: vec![DataSeries::new("A", vec![10.0, 20.0, 30.0])],
            categories: vec!["X".into(), "Y".into(), "Z".into()],
        };
        let stream = chart.generate_chart_content_stream().unwrap();
        assert!(!stream.is_empty());
        assert!(String::from_utf8_lossy(&stream).contains("re f"));
    }

    #[test]
    fn test_bar_chart_multiple_series() {
        let chart = Chart {
            chart_type: ChartType::Bar,
            config: ChartConfig::default(),
            series: vec![
                DataSeries::new("A", vec![10.0, 20.0]),
                DataSeries::new("B", vec![15.0, 25.0]),
            ],
            categories: vec!["Q1".into(), "Q2".into()],
        };
        let stream = chart.generate_chart_content_stream().unwrap();
        assert!(!stream.is_empty());
    }

    #[test]
    fn test_line_chart_data_points() {
        let chart = Chart {
            chart_type: ChartType::Line,
            config: ChartConfig::default(),
            series: vec![DataSeries::new("Sales", vec![1.0, 4.0, 2.0, 8.0, 5.0])],
            categories: vec![],
        };
        let stream = chart.generate_chart_content_stream().unwrap();
        assert!(!stream.is_empty());
        let s = String::from_utf8_lossy(&stream);
        assert!(s.contains("m\n") && s.contains("c"));
    }

    #[test]
    fn test_pie_chart_percentage() {
        let chart = Chart {
            chart_type: ChartType::Pie,
            config: ChartConfig::default(),
            series: vec![DataSeries::new("Slices", vec![25.0, 50.0, 25.0])],
            categories: vec!["A".into(), "B".into(), "C".into()],
        };
        let stream = chart.generate_chart_content_stream().unwrap();
        assert!(!stream.is_empty());
        let s = String::from_utf8_lossy(&stream);
        assert!(s.contains("%"));
    }

    #[test]
    fn test_scatter_plot_coordinate_mapping() {
        let chart = Chart {
            chart_type: ChartType::Scatter,
            config: ChartConfig::default(),
            series: vec![DataSeries::new(
                "Points",
                vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
            )],
            categories: vec![],
        };
        let stream = chart.generate_chart_content_stream().unwrap();
        assert!(!stream.is_empty());
        let s = String::from_utf8_lossy(&stream);
        assert!(s.contains("arc f"));
    }

    #[test]
    fn test_empty_data() {
        let chart = Chart {
            chart_type: ChartType::Bar,
            config: ChartConfig::default(),
            series: vec![DataSeries::new("Empty", vec![])],
            categories: vec![],
        };
        let stream = chart.generate_chart_content_stream().unwrap();
        assert!(!stream.is_empty());
    }

    #[test]
    fn test_single_point() {
        let chart = Chart {
            chart_type: ChartType::Line,
            config: ChartConfig::default(),
            series: vec![DataSeries::new("One", vec![42.0])],
            categories: vec![],
        };
        let stream = chart.generate_chart_content_stream().unwrap();
        assert!(!stream.is_empty());
    }

    #[test]
    fn test_negative_values() {
        let chart = Chart {
            chart_type: ChartType::Bar,
            config: ChartConfig::default(),
            series: vec![DataSeries::new("Neg", vec![-10.0, 0.0, 10.0])],
            categories: vec!["A".into(), "B".into(), "C".into()],
        };
        let stream = chart.generate_chart_content_stream().unwrap();
        assert!(!stream.is_empty());
    }

    #[test]
    fn test_axis_range_nice_ticks() {
        let (min, max, ticks) = compute_axis_range(&[7.0, 23.0, 15.0]);
        assert!(min <= 7.0);
        assert!(max >= 23.0);
        assert!(!ticks.is_empty());
    }

    #[test]
    fn test_escape_pdf_string() {
        let s = escape_pdf_string("test (parens)");
        assert!(s.contains("\\("));
        assert!(s.contains("\\)"));
    }
}
