use termichart_core::{ChartConfig, Color, Point, Result, Style, Viewport};
use termichart_render::CellGrid;

/// Upper half block — top half rendered in foreground color.
const UPPER_HALF: char = '\u{2580}';
/// Lower half block — bottom half rendered in foreground color.
const LOWER_HALF: char = '\u{2584}';
/// Full block — entire cell rendered in foreground color.
const FULL_BLOCK: char = '\u{2588}';

/// Renders a line chart using half-block characters with smooth area fill.
///
/// Each character cell provides 2 vertical sub-pixels via `▀` / `▄` / `█`,
/// giving a solid, clean appearance. The area below the line is filled with
/// a color gradient that fades toward the bottom.
pub struct LineChart {
    points: Vec<Point>,
    overlays: Vec<(String, Vec<Point>)>,
}

impl LineChart {
    /// Creates a new line chart from the given data points.
    pub fn new(points: Vec<Point>) -> Self {
        Self {
            points,
            overlays: Vec::new(),
        }
    }

    /// Adds overlay indicator points (pre-computed).
    pub fn add_overlay(&mut self, name: String, points: Vec<Point>) {
        self.overlays.push((name, points));
    }

    /// Renders the line chart onto the given CellGrid within the chart area.
    pub fn render(
        &self,
        grid: &mut CellGrid,
        viewport: &Viewport,
        config: &ChartConfig,
        offset_x: u16,
        offset_y: u16,
    ) -> Result<()> {
        if self.points.len() < 2 {
            return Ok(());
        }

        let w = viewport.width as usize;
        let h = viewport.height as usize;
        if w == 0 || h == 0 {
            return Ok(());
        }
        let sub_h = h * 2;
        let color = config.theme.line_color();

        // Compute sub-pixel y for each column using Catmull-Rom interpolation.
        let line_sub_y = compute_sub_y_values(&self.points, viewport, w, sub_h);

        // Draw main line with area fill.
        draw_line_area(grid, &line_sub_y, w, h, offset_x, offset_y, color, true);

        // Draw overlays (line only, no fill).
        for (idx, (_name, overlay_pts)) in self.overlays.iter().enumerate() {
            if overlay_pts.len() < 2 {
                continue;
            }
            let overlay_color = config.theme.overlay_colors(idx);
            let overlay_sub_y = compute_sub_y_values(overlay_pts, viewport, w, sub_h);
            draw_line_area(
                grid,
                &overlay_sub_y,
                w,
                h,
                offset_x,
                offset_y,
                overlay_color,
                false,
            );
        }

        Ok(())
    }
}

/// Computes the sub-pixel y coordinate for each column using Catmull-Rom
/// interpolation through the data points.
fn compute_sub_y_values(
    points: &[Point],
    viewport: &Viewport,
    w: usize,
    sub_h: usize,
) -> Vec<usize> {
    let mut sub_ys = Vec::with_capacity(w);
    for col in 0..w {
        // Map column center to data-space x.
        let x_frac = (col as f64 + 0.5) / w as f64;
        let x_data = viewport.x_range.min + x_frac * viewport.x_range.span();

        // Interpolate y using Catmull-Rom spline.
        let y_data = catmull_rom_y(points, x_data);

        // Map to screen sub-pixel y (0 = top, sub_h-1 = bottom).
        let screen_y = viewport.map_y(y_data);
        let sub_y = (screen_y * 2.0)
            .round()
            .clamp(0.0, (sub_h.saturating_sub(1)) as f64) as usize;
        sub_ys.push(sub_y);
    }
    sub_ys
}

/// Draws a line (and optional filled area below it) onto the grid using
/// half-block characters for 2× vertical resolution.
fn draw_line_area(
    grid: &mut CellGrid,
    line_sub_y: &[usize],
    w: usize,
    h: usize,
    offset_x: u16,
    offset_y: u16,
    color: Color,
    fill: bool,
) {
    for col in 0..w {
        let sub_y = line_sub_y[col];
        let grid_col = col as u16 + offset_x;

        // Connect to previous column for vertical continuity.
        let prev_sub_y = if col > 0 { line_sub_y[col - 1] } else { sub_y };
        let line_min = sub_y.min(prev_sub_y);
        let line_max = sub_y.max(prev_sub_y);

        let line_style = Style {
            fg: color,
            bold: true,
            ..Default::default()
        };

        for row in 0..h {
            let top_sub = row * 2;
            let bot_sub = row * 2 + 1;
            let grid_row = row as u16 + offset_y;

            let top_line = top_sub >= line_min && top_sub <= line_max;
            let bot_line = bot_sub >= line_min && bot_sub <= line_max;
            let top_fill = fill && top_sub > line_max;
            let bot_fill = fill && bot_sub > line_max;

            match (top_line, bot_line, top_fill, bot_fill) {
                // Both halves are part of the line.
                (true, true, _, _) => {
                    grid.set(grid_col, grid_row, FULL_BLOCK, line_style);
                }
                // Top half is line, bottom half is fill.
                (true, false, _, true) => {
                    let fc = gradient_color(color, row, line_max / 2, h);
                    let style = Style {
                        fg: color,
                        bg: fc,
                        bold: true,
                        ..Default::default()
                    };
                    grid.set(grid_col, grid_row, UPPER_HALF, style);
                }
                // Top half is line, bottom half is empty.
                (true, false, _, false) => {
                    grid.set(grid_col, grid_row, UPPER_HALF, line_style);
                }
                // Bottom half is line, top half is empty.
                (false, true, _, _) => {
                    grid.set(grid_col, grid_row, LOWER_HALF, line_style);
                }
                // Both halves are fill.
                (false, false, true, true) => {
                    let fc = gradient_color(color, row, line_max / 2, h);
                    let style = Style {
                        fg: fc,
                        ..Default::default()
                    };
                    grid.set(grid_col, grid_row, FULL_BLOCK, style);
                }
                // Only bottom half is fill (transition row).
                (false, false, false, true) => {
                    let fc = gradient_color(color, row, line_max / 2, h);
                    let style = Style {
                        fg: fc,
                        ..Default::default()
                    };
                    grid.set(grid_col, grid_row, LOWER_HALF, style);
                }
                // Empty — nothing to draw.
                _ => {}
            }
        }
    }
}

/// Computes a darkened version of a color for the area-fill gradient.
///
/// Intensity fades from ~35 % at the line down to ~3 % at the chart bottom,
/// creating a smooth gradient beneath the line.
fn gradient_color(base: Color, row: usize, line_row: usize, total_rows: usize) -> Color {
    match base {
        Color::Rgb(r, g, b) => {
            let max_dist = total_rows.saturating_sub(line_row).max(1) as f64;
            let dist = row.saturating_sub(line_row) as f64;
            let factor = ((1.0 - dist / max_dist) * 0.35).max(0.03);
            Color::Rgb(
                (r as f64 * factor) as u8,
                (g as f64 * factor) as u8,
                (b as f64 * factor) as u8,
            )
        }
        other => other,
    }
}

/// Interpolates y at the given x using Catmull-Rom spline through the data
/// points.
///
/// Points are assumed to be sorted by x. Out-of-range x values clamp to the
/// nearest endpoint's y value.
fn catmull_rom_y(points: &[Point], x: f64) -> f64 {
    if points.is_empty() {
        return 0.0;
    }
    let n = points.len();
    if n == 1 {
        return points[0].y;
    }
    if x <= points[0].x {
        return points[0].y;
    }
    if x >= points[n - 1].x {
        return points[n - 1].y;
    }

    // Find the segment [i, i+1] containing x.
    let mut i = 0;
    for j in 0..n - 1 {
        if x <= points[j + 1].x {
            i = j;
            break;
        }
    }

    let dx = points[i + 1].x - points[i].x;
    let t = if dx > 0.0 {
        (x - points[i].x) / dx
    } else {
        0.0
    };

    // Control points (clamp at endpoints).
    let p0 = if i > 0 { points[i - 1].y } else { points[0].y };
    let p1 = points[i].y;
    let p2 = points[i + 1].y;
    let p3 = if i + 2 < n {
        points[i + 2].y
    } else {
        points[n - 1].y
    };

    let t2 = t * t;
    let t3 = t2 * t;

    // Catmull-Rom spline formula.
    0.5 * ((2.0 * p1)
        + (-p0 + p2) * t
        + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
        + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3)
}
