use ab_glyph::{point, Font, FontRef, PxScale, ScaleFont};
use anyhow::{Context, Result};
use image::{ImageBuffer, Rgba, RgbaImage};
use imageproc::drawing::{draw_line_segment_mut, draw_text_mut};
use std::path::Path;

use crate::color::RgbColor;

const FONT_DATA: &[u8] = include_bytes!("../assets/fonts/DejaVuSans.ttf");
const LINE_GAP_PADDING: f32 = 8.0;
const GAP_EPS_PX: f32 = 0.5;
const FILL_RATIO: f32 = 0.5;
const MARGIN_RATIO: f32 = 0.95;

struct TextMetrics {
    draw_x: i32,
    draw_y: i32,
    min_x: f32,
    min_y: f32,
    max_x: f32,
    max_y: f32,
}

pub fn generate_png(
    width: u32,
    height: u32,
    text: &str,
    background: RgbColor,
    path: &Path,
) -> Result<()> {
    let font = FontRef::try_from_slice(FONT_DATA).context("Failed to load embedded font")?;
    let foreground = background.inverse();
    let fg_rgba = foreground.to_rgba();

    let scale = fit_text_scale(&font, text, width, height);
    let px_scale = PxScale::from(scale);
    let metrics = text_metrics(&font, text, px_scale, width, height);
    let gap_pad = LINE_GAP_PADDING + scale * 0.1;

    let mut img: RgbaImage =
        ImageBuffer::from_pixel(width, height, background.to_rgba());

    let w1 = (width.saturating_sub(1)) as f32;
    let h1 = (height.saturating_sub(1)) as f32;

    let left = (metrics.draw_x as f32 + metrics.min_x - gap_pad).max(0.0);
    let top = (metrics.draw_y as f32 + metrics.min_y - gap_pad).max(0.0);
    let right = (metrics.draw_x as f32 + metrics.max_x + gap_pad).min(w1);
    let bottom = (metrics.draw_y as f32 + metrics.max_y + gap_pad).min(h1);

    draw_line_with_gap(&mut img, (0.0, 0.0), (w1, h1), left, top, right, bottom, fg_rgba);
    draw_line_with_gap(&mut img, (w1, 0.0), (0.0, h1), left, top, right, bottom, fg_rgba);

    draw_text_mut(
        &mut img,
        fg_rgba,
        metrics.draw_x,
        metrics.draw_y,
        px_scale,
        &font,
        text,
    );

    img.save(path)
        .with_context(|| format!("Failed to save image to {}", path.display()))?;

    Ok(())
}

fn text_metrics(
    font: &FontRef,
    text: &str,
    scale: PxScale,
    width: u32,
    height: u32,
) -> TextMetrics {
    let (text_w, text_h, min_x, min_y, max_x, max_y) = measure_text(font, text, scale);
    let draw_x = ((width as f32 - text_w) / 2.0 - min_x).round() as i32;
    let draw_y = ((height as f32 - text_h) / 2.0 - min_y).round() as i32;

    TextMetrics {
        draw_x,
        draw_y,
        min_x,
        min_y,
        max_x,
        max_y,
    }
}

fn measure_text(font: &FontRef, text: &str, scale: PxScale) -> (f32, f32, f32, f32, f32, f32) {
    let scaled = font.as_scaled(scale);
    let mut min_x = f32::MAX;
    let mut min_y = f32::MAX;
    let mut max_x = f32::MIN;
    let mut max_y = f32::MIN;
    let mut x = 0.0f32;
    let mut last: Option<ab_glyph::GlyphId> = None;

    for ch in text.chars() {
        let glyph_id = scaled.glyph_id(ch);
        if let Some(prev) = last {
            x += scaled.kern(glyph_id, prev);
        }
        let glyph = glyph_id.with_scale_and_position(scale, point(x, scaled.ascent()));
        x += scaled.h_advance(glyph_id);
        last = Some(glyph_id);
        if let Some(outlined) = scaled.outline_glyph(glyph) {
            let bb = outlined.px_bounds();
            min_x = min_x.min(bb.min.x);
            min_y = min_y.min(bb.min.y);
            max_x = max_x.max(bb.max.x);
            max_y = max_y.max(bb.max.y);
        }
    }

    if min_x == f32::MAX {
        let s = scale.y;
        let w = text.len() as f32 * s * 0.6;
        return (w, s, 0.0, 0.0, w, s);
    }

    (
        max_x - min_x,
        max_y - min_y,
        min_x.floor(),
        min_y.floor(),
        max_x.ceil(),
        max_y.ceil(),
    )
}

fn fit_text_scale(font: &FontRef, text: &str, width: u32, height: u32) -> f32 {
    let target_area = width as f32 * height as f32 * FILL_RATIO;
    let max_w = width as f32 * MARGIN_RATIO;
    let max_h = height as f32 * MARGIN_RATIO;
    let mut lo = 1.0f32;
    let mut hi = (width.max(height) as f32) * 4.0;
    let mut best_fit = lo;

    while hi - lo > 0.5 {
        let mid = (lo + hi) / 2.0;
        let scale = PxScale::from(mid);
        let (w, h, _, _, _, _) = measure_text(font, text, scale);
        let area = w * h;
        let fits = w <= max_w && h <= max_h;

        if fits {
            best_fit = mid;
            if area >= target_area {
                lo = mid;
            } else {
                lo = mid;
            }
        } else {
            hi = mid;
        }
    }

    best_fit
}

fn draw_line_with_gap(
    img: &mut RgbaImage,
    start: (f32, f32),
    end: (f32, f32),
    left: f32,
    top: f32,
    right: f32,
    bottom: f32,
    color: Rgba<u8>,
) {
    let dx = end.0 - start.0;
    let dy = end.1 - start.1;
    let len = dx.hypot(dy);

    if len < f32::EPSILON {
        return;
    }

    let Some((t_in, t_out)) = rect_interior_ts(start, end, left, top, right, bottom) else {
        draw_line_segment_mut(img, start, end, color);
        return;
    };

    let eps_t = GAP_EPS_PX / len;

    if t_in > eps_t {
        let t1 = (t_in - eps_t).max(0.0);
        draw_line_segment_mut(
            img,
            start,
            (start.0 + dx * t1, start.1 + dy * t1),
            color,
        );
    }

    if t_out < 1.0 - eps_t {
        let t2 = (t_out + eps_t).min(1.0);
        draw_line_segment_mut(
            img,
            (start.0 + dx * t2, start.1 + dy * t2),
            end,
            color,
        );
    }
}

fn rect_interior_ts(
    start: (f32, f32),
    end: (f32, f32),
    left: f32,
    top: f32,
    right: f32,
    bottom: f32,
) -> Option<(f32, f32)> {
    let dx = end.0 - start.0;
    let dy = end.1 - start.1;

    let mut t_enter = 0.0f32;
    let mut t_exit = 1.0f32;

    let edges = [
        (-dx, start.0 - left),
        (dx, right - start.0),
        (-dy, start.1 - top),
        (dy, bottom - start.1),
    ];

    for (p, q) in edges {
        if p.abs() < f32::EPSILON {
            if q < 0.0 {
                return None;
            }
            continue;
        }

        let t = q / p;
        if p < 0.0 {
            t_enter = t_enter.max(t);
        } else {
            t_exit = t_exit.min(t);
        }
    }

    if t_enter > t_exit {
        return None;
    }

    let t_in = t_enter.clamp(0.0, 1.0);
    let t_out = t_exit.clamp(0.0, 1.0);

    if t_in >= t_out {
        return None;
    }

    Some((t_in, t_out))
}
