use crate::types::{BBox, Bounded, Char, Curve, Edge, Line, Page, Point, Word};
use crate::table::{Table, TableFinder, TableSettings};
use crate::Result;
use font8x8::UnicodeFonts;
use image::{ImageBuffer, ImageFormat, Rgba, RgbaImage};
use imageproc::drawing::{
    draw_filled_circle_mut, draw_filled_rect_mut, draw_hollow_circle_mut, draw_hollow_rect_mut,
    draw_line_segment_mut,
};
use imageproc::rect::Rect as ImageRect;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

const DEFAULT_RESOLUTION: f64 = 72.0;

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct RgbaColor {
    pub r: u8,
    pub g: u8,
    pub b: u8,
    pub a: u8,
}

impl RgbaColor {
    pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
        Self { r, g, b, a }
    }

    pub fn to_rgba(self) -> Rgba<u8> {
        Rgba([self.r, self.g, self.b, self.a])
    }
}

impl Default for RgbaColor {
    fn default() -> Self {
        Self::new(0, 0, 0, 255)
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub struct RenderOptions {
    pub resolution: Option<f64>,
    pub width: Option<f64>,
    pub height: Option<f64>,
    pub antialias: bool,
    pub force_mediabox: bool,
}

impl Default for RenderOptions {
    fn default() -> Self {
        Self {
            resolution: Some(DEFAULT_RESOLUTION),
            width: None,
            height: None,
            antialias: false,
            force_mediabox: false,
        }
    }
}

pub trait HasBBox {
    fn bbox(&self) -> BBox;
}

impl<T> HasBBox for T
where
    T: Bounded,
{
    fn bbox(&self) -> BBox {
        Bounded::bbox(self)
    }
}

impl HasBBox for BBox {
    fn bbox(&self) -> BBox {
        *self
    }
}

impl HasBBox for (f64, f64, f64, f64) {
    fn bbox(&self) -> BBox {
        BBox::new(self.0, self.1, self.2, self.3)
    }
}

impl HasBBox for Table {
    fn bbox(&self) -> BBox {
        self.bbox
    }
}

pub trait HasCenter {
    fn center(&self) -> Point;
}

impl<T> HasCenter for T
where
    T: HasBBox,
{
    fn center(&self) -> Point {
        self.bbox().center()
    }
}

pub trait HasLineSegments {
    fn line_segments(&self) -> Vec<(Point, Point)>;
}

impl HasLineSegments for Line {
    fn line_segments(&self) -> Vec<(Point, Point)> {
        self.pts
            .windows(2)
            .map(|pair| (pair[0], pair[1]))
            .collect::<Vec<_>>()
    }
}

impl HasLineSegments for Edge {
    fn line_segments(&self) -> Vec<(Point, Point)> {
        vec![(Point::new(self.x0, self.top), Point::new(self.x1, self.bottom))]
    }
}

impl HasLineSegments for Curve {
    fn line_segments(&self) -> Vec<(Point, Point)> {
        self.pts
            .windows(2)
            .map(|pair| (pair[0], pair[1]))
            .collect::<Vec<_>>()
    }
}

impl HasLineSegments for (Point, Point) {
    fn line_segments(&self) -> Vec<(Point, Point)> {
        vec![*self]
    }
}

impl HasLineSegments for ((f64, f64), (f64, f64)) {
    fn line_segments(&self) -> Vec<(Point, Point)> {
        vec![(Point::new((self.0).0, (self.0).1), Point::new((self.1).0, (self.1).1))]
    }
}

#[derive(Debug, Clone)]
pub struct PageImage {
    pub page: Page,
    pub resolution: f64,
    pub antialias: bool,
    pub force_mediabox: bool,
    pub bbox: BBox,
    pub original: RgbaImage,
    pub annotated: RgbaImage,
}

impl PageImage {
    pub fn new(page: &Page, options: RenderOptions) -> Result<Self> {
        let set_count = [options.resolution.is_some(), options.width.is_some(), options.height.is_some()]
            .into_iter()
            .filter(|item| *item)
            .count();
        if set_count > 1 {
            return Err(crate::Error::Message(
                "pass at most one of resolution, width, or height".to_string(),
            ));
        }

        let bbox = if page.bbox != page.mediabox {
            page.bbox
        } else if options.force_mediabox {
            page.mediabox
        } else {
            page.cropbox
        };

        let resolution = if let Some(resolution) = options.resolution {
            resolution
        } else if let Some(width) = options.width {
            DEFAULT_RESOLUTION * (width / bbox.width())
        } else if let Some(height) = options.height {
            DEFAULT_RESOLUTION * (height / bbox.height())
        } else {
            DEFAULT_RESOLUTION
        };

        let scale = resolution / DEFAULT_RESOLUTION;
        let width_px = ((bbox.width() * scale).round() as i64).max(1) as u32;
        let height_px = ((bbox.height() * scale).round() as i64).max(1) as u32;
        let mut image = ImageBuffer::from_pixel(width_px, height_px, Rgba([255, 255, 255, 255]));

        let mut page_image = Self {
            page: page.clone(),
            resolution,
            antialias: options.antialias,
            force_mediabox: options.force_mediabox,
            bbox,
            original: image.clone(),
            annotated: image.clone(),
        };
        page_image.render_page_content(&mut image);
        page_image.original = image.clone();
        page_image.annotated = image;
        Ok(page_image)
    }

    pub fn width(&self) -> u32 {
        self.annotated.width()
    }

    pub fn height(&self) -> u32 {
        self.annotated.height()
    }

    pub fn reset(&mut self) -> &mut Self {
        self.annotated = self.original.clone();
        self
    }

    pub fn copy(&self) -> Self {
        self.clone()
    }

    pub fn save<P: AsRef<Path>>(
        &self,
        dest: P,
        format: Option<ImageFormat>,
        _quantize: bool,
        _colors: u16,
        _bits: u8,
    ) -> Result<()> {
        if let Some(format) = format {
            self.annotated.save_with_format(dest, format)?;
        } else {
            self.annotated.save(dest)?;
        }
        Ok(())
    }

    pub fn show(&self) -> Result<PathBuf> {
        let millis = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map_err(|err| crate::Error::Message(err.to_string()))?
            .as_millis();
        let path = std::env::temp_dir().join(format!("pdfsink-rs-page-{millis}.png"));
        self.save(&path, Some(ImageFormat::Png), false, 256, 8)?;

        #[cfg(target_os = "macos")]
        let _ = Command::new("open").arg(&path).spawn();
        #[cfg(target_os = "linux")]
        let _ = Command::new("xdg-open").arg(&path).spawn();
        #[cfg(target_os = "windows")]
        let _ = Command::new("cmd").args(["/C", "start", path.to_string_lossy().as_ref()]).spawn();

        Ok(path)
    }

    pub fn draw_line<T: HasLineSegments>(&mut self, item: &T, stroke: RgbaColor, stroke_width: u32) -> &mut Self {
        let color = stroke.to_rgba();
        for (start, end) in item.line_segments() {
            let (x0, y0) = self.project_point(start);
            let (x1, y1) = self.project_point(end);
            let offset = (stroke_width.max(1) as i32 - 1) / 2;
            for dx in -offset..=offset {
                for dy in -offset..=offset {
                    draw_line_segment_mut(
                        &mut self.annotated,
                        (x0 + dx as f32, y0 + dy as f32),
                        (x1 + dx as f32, y1 + dy as f32),
                        color,
                    );
                }
            }
        }
        self
    }

    pub fn draw_lines<T: HasLineSegments>(&mut self, items: &[T], stroke: RgbaColor, stroke_width: u32) -> &mut Self {
        for item in items {
            self.draw_line(item, stroke, stroke_width);
        }
        self
    }

    pub fn draw_vline(&mut self, location: f64, stroke: RgbaColor, stroke_width: u32) -> &mut Self {
        self.draw_line(
            &(
                Point::new(location, self.bbox.top),
                Point::new(location, self.bbox.bottom),
            ),
            stroke,
            stroke_width,
        )
    }

    pub fn draw_vlines(&mut self, locations: &[f64], stroke: RgbaColor, stroke_width: u32) -> &mut Self {
        for location in locations {
            self.draw_vline(*location, stroke, stroke_width);
        }
        self
    }

    pub fn draw_hline(&mut self, location: f64, stroke: RgbaColor, stroke_width: u32) -> &mut Self {
        self.draw_line(
            &(
                Point::new(self.bbox.x0, location),
                Point::new(self.bbox.x1, location),
            ),
            stroke,
            stroke_width,
        )
    }

    pub fn draw_hlines(&mut self, locations: &[f64], stroke: RgbaColor, stroke_width: u32) -> &mut Self {
        for location in locations {
            self.draw_hline(*location, stroke, stroke_width);
        }
        self
    }

    pub fn draw_rect<T: HasBBox>(
        &mut self,
        item: &T,
        fill: Option<RgbaColor>,
        stroke: Option<RgbaColor>,
        stroke_width: u32,
    ) -> &mut Self {
        let bbox = item.bbox();
        let rect = self.project_rect(bbox);
        if rect.width() == 0 || rect.height() == 0 {
            return self;
        }
        if let Some(fill) = fill {
            draw_filled_rect_mut(&mut self.annotated, rect, fill.to_rgba());
        }
        if let Some(stroke) = stroke {
            for inset in 0..stroke_width.max(1) {
                let x = rect.left() + inset as i32;
                let y = rect.top() + inset as i32;
                let w = rect.width().saturating_sub(inset.saturating_mul(2));
                let h = rect.height().saturating_sub(inset.saturating_mul(2));
                if w == 0 || h == 0 {
                    continue;
                }
                let inset_rect = ImageRect::at(x, y).of_size(w, h);
                draw_hollow_rect_mut(&mut self.annotated, inset_rect, stroke.to_rgba());
            }
        }
        self
    }

    pub fn draw_rects<T: HasBBox>(
        &mut self,
        items: &[T],
        fill: Option<RgbaColor>,
        stroke: Option<RgbaColor>,
        stroke_width: u32,
    ) -> &mut Self {
        for item in items {
            self.draw_rect(item, fill, stroke, stroke_width);
        }
        self
    }

    pub fn draw_circle<T: HasCenter>(
        &mut self,
        item: &T,
        radius: i32,
        fill: Option<RgbaColor>,
        stroke: Option<RgbaColor>,
    ) -> &mut Self {
        let center = item.center();
        let (x, y) = self.project_point(center);
        let center = (x.round() as i32, y.round() as i32);
        if let Some(fill) = fill {
            draw_filled_circle_mut(&mut self.annotated, center, radius.max(1), fill.to_rgba());
        }
        if let Some(stroke) = stroke {
            draw_hollow_circle_mut(&mut self.annotated, center, radius.max(1), stroke.to_rgba());
        }
        self
    }

    pub fn draw_circles<T: HasCenter>(
        &mut self,
        items: &[T],
        radius: i32,
        fill: Option<RgbaColor>,
        stroke: Option<RgbaColor>,
    ) -> &mut Self {
        for item in items {
            self.draw_circle(item, radius, fill, stroke);
        }
        self
    }

    pub fn outline_words(&mut self, words: &[Word], stroke: RgbaColor, stroke_width: u32) -> &mut Self {
        self.draw_rects(words, None, Some(stroke), stroke_width)
    }

    pub fn outline_chars(&mut self, chars: &[Char], stroke: RgbaColor, stroke_width: u32) -> &mut Self {
        self.draw_rects(chars, None, Some(stroke), stroke_width)
    }

    pub fn outline_edges(&mut self, edges: &[Edge], stroke: RgbaColor, stroke_width: u32) -> &mut Self {
        self.draw_lines(edges, stroke, stroke_width)
    }

    pub fn outline_tables(&mut self, tables: &[Table], fill: Option<RgbaColor>, stroke: Option<RgbaColor>, stroke_width: u32) -> &mut Self {
        for table in tables {
            for cell in &table.cells {
                self.draw_rect(cell, fill, stroke, stroke_width);
            }
        }
        self
    }

    pub fn debug_tablefinder(&mut self, table_settings: Option<TableSettings>) -> Result<&mut Self> {
        let finder = if let Some(settings) = table_settings {
            self.page.debug_tablefinder(settings)?
        } else {
            self.page.debug_tablefinder(TableSettings::default())?
        };
        self.overlay_tablefinder(&finder);
        Ok(self)
    }

    pub fn overlay_tablefinder(&mut self, finder: &TableFinder) -> &mut Self {
        let red = RgbaColor::new(255, 0, 0, 255);
        let light_blue = RgbaColor::new(173, 216, 230, 96);
        for edge in &finder.edges {
            self.draw_line(edge, red, 1);
        }
        for intersection in finder.intersections.keys() {
            let point = Point::new(intersection.0.into_inner(), intersection.1.into_inner());
            self.draw_circle(&point, 4, Some(red), Some(red));
        }
        for cell in &finder.cells {
            self.draw_rect(cell, Some(light_blue), Some(red), 1);
        }
        self
    }

    fn render_page_content(&mut self, image: &mut RgbaImage) {
        for rect in &self.page.rects {
            let fill = if rect.fill {
                Some(RgbaColor::new(235, 235, 235, 255).to_rgba())
            } else {
                None
            };
            let stroke = if rect.stroke {
                Some(RgbaColor::default().to_rgba())
            } else {
                None
            };
            let projected = self.project_rect(Bounded::bbox(rect));
            if let Some(fill) = fill {
                draw_filled_rect_mut(image, projected, fill);
            }
            if let Some(stroke) = stroke {
                draw_hollow_rect_mut(image, projected, stroke);
            }
        }

        for line in &self.page.lines {
            self.render_segment(image, line);
        }
        for curve in &self.page.curves {
            self.render_segment(image, curve);
        }
        for image_obj in &self.page.images {
            let rect = self.project_rect(Bounded::bbox(image_obj));
            draw_hollow_rect_mut(image, rect, RgbaColor::new(120, 120, 120, 255).to_rgba());
        }
        for ch in &self.page.chars {
            self.render_char(image, ch);
        }
    }

    fn render_segment<T: HasLineSegments>(&self, image: &mut RgbaImage, item: &T) {
        for (start, end) in item.line_segments() {
            let (x0, y0) = self.project_point(start);
            let (x1, y1) = self.project_point(end);
            draw_line_segment_mut(image, (x0, y0), (x1, y1), RgbaColor::default().to_rgba());
        }
    }

    fn render_char(&self, image: &mut RgbaImage, ch: &Char) {
        let Some(letter) = ch.text.chars().next() else {
            return;
        };
        if letter.is_whitespace() {
            return;
        }
        let bbox = Bounded::bbox(ch);
        let left = ((bbox.x0 - self.bbox.x0) * self.scale()).floor().max(0.0) as u32;
        let top = ((bbox.top - self.bbox.top) * self.scale()).floor().max(0.0) as u32;
        let width = ((bbox.width() * self.scale()).ceil().max(1.0)) as u32;
        let height = ((bbox.height() * self.scale()).ceil().max(1.0)) as u32;

        if let Some(glyph) = font8x8::BASIC_FONTS.get(letter) {
            for (row_idx, row) in glyph.iter().enumerate() {
                for col_idx in 0..8u32 {
                    if ((*row >> col_idx) & 1) == 1 {
                        let x_start = left + (col_idx * width / 8);
                        let x_end = left + (((col_idx + 1) * width + 7) / 8).max(1);
                        let y_start = top + (row_idx as u32 * height / 8);
                        let y_end = top + ((((row_idx as u32) + 1) * height + 7) / 8).max(1);
                        for y in y_start..y_end.min(image.height()) {
                            for x in x_start..x_end.min(image.width()) {
                                image.put_pixel(x, y, RgbaColor::default().to_rgba());
                            }
                        }
                    }
                }
            }
        } else {
            let rect = self.project_rect(bbox);
            draw_hollow_rect_mut(image, rect, RgbaColor::default().to_rgba());
        }
    }

    fn scale(&self) -> f64 {
        self.resolution / DEFAULT_RESOLUTION
    }

    fn project_point(&self, point: Point) -> (f32, f32) {
        (
            ((point.x - self.bbox.x0) * self.scale()) as f32,
            ((point.y - self.bbox.top) * self.scale()) as f32,
        )
    }

    fn project_rect(&self, bbox: BBox) -> ImageRect {
        let x = ((bbox.x0 - self.bbox.x0) * self.scale()).floor() as i32;
        let y = ((bbox.top - self.bbox.top) * self.scale()).floor() as i32;
        let width = ((bbox.width() * self.scale()).ceil() as i64).max(1) as u32;
        let height = ((bbox.height() * self.scale()).ceil() as i64).max(1) as u32;
        ImageRect::at(x, y).of_size(width, height)
    }
}

impl HasCenter for Point {
    fn center(&self) -> Point {
        *self
    }
}

impl Page {
    pub fn to_image(
        &self,
        resolution: Option<f64>,
        width: Option<f64>,
        height: Option<f64>,
        antialias: bool,
        force_mediabox: bool,
    ) -> Result<PageImage> {
        PageImage::new(
            self,
            RenderOptions {
                resolution,
                width,
                height,
                antialias,
                force_mediabox,
            },
        )
    }
}
