use termichart_core::{Range, Rect, Style, Theme, Viewport, XAxisFormat};

use crate::cell::CellGrid;

/// Box-drawing characters used for axis rendering.
const VERTICAL: char = '\u{2502}'; // │
const HORIZONTAL: char = '\u{2500}'; // ─
const TICK_LEFT: char = '\u{2524}'; // ┤  (tick mark pointing left from y-axis)
const TICK_DOWN: char = '\u{252C}'; // ┬  (tick mark pointing down from x-axis)
const CORNER_BL: char = '\u{2514}'; // └  (bottom-left corner)

/// Renders axis lines, tick marks, and labels onto a [`CellGrid`].
#[derive(Debug, Clone, Copy)]
pub struct AxisRenderer;

impl AxisRenderer {
    /// Creates a new `AxisRenderer`.
    pub fn new() -> Self {
        Self
    }

    /// Draws the Y axis (vertical) inside the given `area` rectangle.
    ///
    /// The axis line is drawn along the right edge of `area`, with tick marks
    /// and price labels extending to the left.
    pub fn draw_y_axis(
        &self,
        grid: &mut CellGrid,
        viewport: &Viewport,
        area: &Rect,
        theme: &Theme,
    ) {
        let axis_style = Style {
            fg: theme.axis_color(),
            dim: true,
            ..Style::default()
        };
        let label_style = Style {
            fg: theme.label_color(),
            ..Style::default()
        };

        // The axis line is at the right edge of the y-axis area.
        let axis_col = area.x + area.width.saturating_sub(1);

        // Draw the vertical line.
        for row in area.y..area.bottom() {
            grid.set(axis_col, row, VERTICAL, axis_style);
        }

        // Compute nice tick values and draw labels + tick marks.
        let ticks = nice_ticks(&viewport.y_range, area.height as usize);

        for tick_val in &ticks {
            // Map the data value to a screen row within the chart area
            // (which sits immediately to the right of the y-axis area).
            let mapped = viewport.map_y(*tick_val);
            let row = mapped.round() as i32;
            // The row must fall within the area's vertical span.
            if row < area.y as i32 || row >= area.bottom() as i32 {
                continue;
            }
            let row = row as u16;

            // Draw the tick mark on the axis line.
            grid.set(axis_col, row, TICK_LEFT, axis_style);

            // Format the label.
            let label = format_label(*tick_val);
            let label_len = label.len() as u16;

            // Place the label to the left of the tick mark.
            let label_start = if axis_col >= label_len + 1 {
                axis_col - label_len - 1
            } else {
                area.x
            };

            // Clamp so we don't write outside the area.
            let label_start = label_start.max(area.x);

            for (i, ch) in label.chars().enumerate() {
                let c = label_start + i as u16;
                if c < axis_col {
                    grid.set(c, row, ch, label_style);
                }
            }
        }
    }

    /// Draws the X axis (horizontal) inside the given `area` rectangle.
    ///
    /// The axis line is drawn along the top edge of `area`, with tick marks
    /// and labels extending downward. When `x_format` is [`XAxisFormat::Time`],
    /// tick values are interpreted as Unix-epoch seconds and rendered as
    /// `HH:MM:SS`.
    pub fn draw_x_axis(
        &self,
        grid: &mut CellGrid,
        viewport: &Viewport,
        area: &Rect,
        theme: &Theme,
        x_format: XAxisFormat,
    ) {
        let axis_style = Style {
            fg: theme.axis_color(),
            dim: true,
            ..Style::default()
        };
        let label_style = Style {
            fg: theme.label_color(),
            ..Style::default()
        };

        let axis_row = area.y;

        // Draw the horizontal line.
        for col in area.x..area.right() {
            grid.set(col, axis_row, HORIZONTAL, axis_style);
        }

        // Draw corner at the left edge (junction with y-axis).
        if area.x > 0 {
            grid.set(area.x.saturating_sub(1), axis_row, CORNER_BL, axis_style);
        }

        // Compute tick values along the x range.
        let ticks = match x_format {
            XAxisFormat::Time => nice_time_ticks(&viewport.x_range, area.width as usize),
            XAxisFormat::Numeric => nice_ticks(&viewport.x_range, area.width as usize),
        };

        // Time labels are wider (5-8 chars) so enforce minimum spacing.
        let min_label_gap: u16 = match x_format {
            XAxisFormat::Time => 10,
            XAxisFormat::Numeric => 0,
        };
        let mut last_label_end: u16 = 0;

        for tick_val in &ticks {
            let mapped = viewport.map_x(*tick_val);
            let col = mapped.round() as i32 + area.x as i32;
            if col < area.x as i32 || col >= area.right() as i32 {
                continue;
            }
            let col = col as u16;

            // Tick mark.
            grid.set(col, axis_row, TICK_DOWN, axis_style);

            // Label below the axis.
            let label = match x_format {
                XAxisFormat::Time => format_time_label(*tick_val),
                XAxisFormat::Numeric => format_label(*tick_val),
            };
            let half = (label.len() / 2) as u16;
            let label_start = col.saturating_sub(half);

            // Skip label if it would overlap the previous one.
            if label_start < last_label_end + min_label_gap && last_label_end > 0 {
                continue;
            }

            if axis_row + 1 < grid.height {
                for (i, ch) in label.chars().enumerate() {
                    let c = label_start + i as u16;
                    if c < area.right() {
                        grid.set(c, axis_row + 1, ch, label_style);
                    }
                }
                last_label_end = label_start + label.len() as u16;
            }
        }
    }
}

impl Default for AxisRenderer {
    fn default() -> Self {
        Self::new()
    }
}

/// Generates a set of evenly-spaced "nice" tick values within the given
/// [`Range`], aiming for at most `max_ticks` ticks.
///
/// The step size is rounded to a human-friendly value (1, 2, or 5 times a
/// power of ten).
pub fn nice_ticks(range: &Range, max_ticks: usize) -> Vec<f64> {
    if max_ticks < 2 {
        return vec![];
    }

    let span = range.span();
    if span <= 0.0 || !span.is_finite() {
        return vec![];
    }

    // Target step: divide the span by the desired tick count.
    let raw_step = span / (max_ticks as f64);

    // Round to a "nice" step: 1, 2, or 5 * 10^n.
    let magnitude = 10.0_f64.powf(raw_step.log10().floor());
    let residual = raw_step / magnitude;

    let nice_step = if residual <= 1.5 {
        magnitude
    } else if residual <= 3.5 {
        2.0 * magnitude
    } else if residual <= 7.5 {
        5.0 * magnitude
    } else {
        10.0 * magnitude
    };

    // Generate ticks starting at the first multiple of nice_step >= range.min.
    let mut ticks = Vec::new();
    let start = (range.min / nice_step).ceil() * nice_step;

    let mut val = start;
    while val <= range.max {
        ticks.push(val);
        val += nice_step;

        // Safety: avoid infinite loops from floating-point weirdness.
        if ticks.len() > max_ticks * 2 {
            break;
        }
    }

    ticks
}

/// Generates time-aligned ticks at nice minute/hour boundaries.
///
/// Picks the smallest step from `[1m, 2m, 5m, 10m, 15m, 30m, 1h]` that
/// produces at most `max_ticks` ticks, then generates values snapped to
/// those boundaries.
fn nice_time_ticks(range: &Range, max_ticks: usize) -> Vec<f64> {
    if max_ticks < 2 {
        return vec![];
    }
    let span = range.span();
    if span <= 0.0 || !span.is_finite() {
        return vec![];
    }

    // Candidate steps in seconds (1m → 4h).
    let steps: &[f64] = &[
        60.0, 120.0, 300.0, 600.0, 900.0, 1800.0, 3600.0, 7200.0, 14400.0,
    ];

    // Pick the smallest step that gives ≤ max_ticks ticks.
    // Use fewer ticks than max to leave room for labels.
    let target = (max_ticks / 10).max(3);
    let step = steps
        .iter()
        .copied()
        .find(|&s| (span / s) as usize <= target)
        .unwrap_or(14400.0);

    // Snap the starting value up to the next multiple of step.
    let start = ((range.min / step).ceil()) * step;

    let mut ticks = Vec::new();
    let mut val = start;
    while val <= range.max {
        ticks.push(val);
        val += step;
        if ticks.len() > max_ticks * 2 {
            break;
        }
    }
    ticks
}

/// Breaks a Unix-epoch timestamp into local `(hour, minute, second)`.
fn epoch_to_local_hms(epoch_secs: f64) -> (i32, i32, i32) {
    let secs = epoch_secs as i64;
    #[cfg(unix)]
    {
        extern "C" {
            fn localtime_r(timep: *const i64, result: *mut Tm) -> *mut Tm;
        }
        #[repr(C)]
        struct Tm {
            tm_sec: i32,
            tm_min: i32,
            tm_hour: i32,
            _rest: [i32; 6],
        }
        let mut tm = Tm { tm_sec: 0, tm_min: 0, tm_hour: 0, _rest: [0; 6] };
        unsafe { localtime_r(&secs, &mut tm); }
        (tm.tm_hour, tm.tm_min, tm.tm_sec)
    }
    #[cfg(not(unix))]
    {
        let s = secs.rem_euclid(86400);
        ((s / 3600) as i32, ((s % 3600) / 60) as i32, (s % 60) as i32)
    }
}

/// Formats a Unix-epoch timestamp as `HH:MM` (or `HH:MM:SS` when seconds
/// are non-zero) in local time.
fn format_time_label(epoch_secs: f64) -> String {
    let (h, m, s) = epoch_to_local_hms(epoch_secs);
    if s == 0 {
        format!("{:02}:{:02}", h, m)
    } else {
        format!("{:02}:{:02}:{:02}", h, m, s)
    }
}

/// Formats a numeric tick label with an appropriate number of decimal places.
fn format_label(value: f64) -> String {
    let abs = value.abs();
    if abs == 0.0 {
        "0".to_string()
    } else if abs >= 1000.0 {
        format!("{:.0}", value)
    } else if abs >= 100.0 {
        format!("{:.1}", value)
    } else if abs >= 10.0 {
        format!("{:.2}", value)
    } else if abs >= 1.0 {
        format!("{:.2}", value)
    } else {
        format!("{:.4}", value)
    }
}
