// Copyright (C) 2025 Christian Mauduit <ufoot@ufoot.org>

//! Image-based 3D source implementation.
//!
//! This module provides the ability to load pathfinding volumes from a stack of images.
//! Each image represents a 2D layer (z-level) of the 3D volume.

use super::cell_type::*;
use super::source_3d::*;
use crate::errors::*;
use crate::mesh_3d::{Shape3D};
use image::{DynamicImage, GenericImageView};
use std::path::Path;

/// A 3D volume source that reads from a stack of images.
///
/// Each image represents a horizontal layer (z-level) in the 3D volume.
/// Pixels are converted to FREE or WALL cells based on their brightness.
/// By default, pixels darker than 50% brightness (0.5) are considered walls.
///
/// # Example
///
/// ```no_run
/// use shortestpath::mesh_source::{Source3DFromImage, Source3D, CellType};
/// use image::DynamicImage;
///
/// // Load images for each layer
/// let layer0 = image::open("layer0.png").unwrap();
/// let layer1 = image::open("layer1.png").unwrap();
///
/// let source = Source3DFromImage::from_images(&[layer0, layer1], 0.5).unwrap();
/// assert_eq!(source.depth(), 2);
/// ```
#[derive(Debug, Clone)]
pub struct Source3DFromImage {
    width: usize,
    height: usize,
    depth: usize,
    cells: Vec<Vec<Vec<CellType>>>,
}

impl Source3DFromImage {
    /// Default brightness threshold for determining walls (0.0-1.0).
    ///
    /// Pixels with brightness below this value are considered walls.
    pub const DEFAULT_THRESHOLD: f32 = 0.5;

    /// Creates a Source3DFromImage from a vector of image file paths.
    ///
    /// Each path represents one layer (z-level) in the volume.
    /// Layers are ordered from z=0 to z=depth-1.
    ///
    /// # Arguments
    ///
    /// * `paths` - Vector of file paths, one per layer
    /// * `threshold` - Brightness threshold (0.0-1.0). Pixels darker than this are walls.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use shortestpath::mesh_source::{Source3DFromImage, Source3D};
    ///
    /// let paths = vec!["layer0.png", "layer1.png", "layer2.png"];
    /// let source = Source3DFromImage::from_paths(&paths, 0.5).unwrap();
    /// assert_eq!(source.depth(), 3);
    /// ```
    pub fn from_paths<P: AsRef<Path>>(paths: &[P], threshold: f32) -> Result<Self> {
        if paths.is_empty() {
            return Err(Error::report_bug("No image paths provided"));
        }

        let mut images = Vec::with_capacity(paths.len());
        for path in paths {
            let img = image::open(path)
                .map_err(|e| Error::report_bug(&format!("Failed to load image: {e}")))?;
            images.push(img);
        }

        Self::from_images(&images, threshold)
    }

    /// Creates a Source3DFromImage from a slice of DynamicImages.
    ///
    /// Each image represents one layer (z-level) in the volume.
    /// All images must have the same dimensions.
    ///
    /// # Arguments
    ///
    /// * `images` - Slice of images, one per layer
    /// * `threshold` - Brightness threshold (0.0-1.0). Pixels darker than this are walls.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use shortestpath::mesh_source::Source3DFromImage;
    /// use image::DynamicImage;
    ///
    /// let layer0 = image::open("layer0.png").unwrap();
    /// let layer1 = image::open("layer1.png").unwrap();
    ///
    /// let source = Source3DFromImage::from_images(&[layer0, layer1], 0.5).unwrap();
    /// ```
    pub fn from_images(images: &[DynamicImage], threshold: f32) -> Result<Self> {
        if images.is_empty() {
            return Err(Error::report_bug("No images provided"));
        }

        let width = images[0].width() as usize;
        let height = images[0].height() as usize;
        let depth = images.len();

        if width == 0 || height == 0 {
            return Err(Error::report_bug("Images have zero dimensions"));
        }

        // Verify all images have the same dimensions
        for (z, img) in images.iter().enumerate() {
            if img.width() as usize != width || img.height() as usize != height {
                return Err(Error::report_bug(&format!(
                    "Image at z={z} has different dimensions: {}x{} vs {}x{}",
                    img.width(),
                    img.height(),
                    width,
                    height
                )));
            }
        }

        let mut cells = Vec::with_capacity(depth);
        for img in images {
            let mut layer = Vec::with_capacity(height);
            for y in 0..height {
                let mut row = Vec::with_capacity(width);
                for x in 0..width {
                    let pixel = img.get_pixel(x as u32, y as u32);

                    // Calculate brightness from RGB (luminance)
                    // Using standard luminance formula: 0.299*R + 0.587*G + 0.114*B
                    // Normalize to 0.0-1.0 range
                    let r = pixel[0] as f32 / 255.0;
                    let g = pixel[1] as f32 / 255.0;
                    let b = pixel[2] as f32 / 255.0;
                    let luma = 0.299 * r + 0.587 * g + 0.114 * b;

                    let cell_type = if luma < threshold {
                        CellType::WALL
                    } else {
                        CellType::FLOOR
                    };
                    row.push(cell_type);
                }
                layer.push(row);
            }
            cells.push(layer);
        }

        Ok(Self {
            width,
            height,
            depth,
            cells,
        })
    }

    /// Creates a Source3DFromImage with the default threshold.
    ///
    /// # Arguments
    ///
    /// * `images` - Slice of images, one per layer
    ///
    /// # Example
    ///
    /// ```no_run
    /// use shortestpath::mesh_source::Source3DFromImage;
    /// use image::DynamicImage;
    ///
    /// let layer0 = image::open("layer0.png").unwrap();
    /// let layer1 = image::open("layer1.png").unwrap();
    ///
    /// let source = Source3DFromImage::new(&[layer0, layer1]).unwrap();
    /// ```
    pub fn new(images: &[DynamicImage]) -> Result<Self> {
        Self::from_images(images, Self::DEFAULT_THRESHOLD)
    }
}

impl Source3D for Source3DFromImage {
    fn get(&self, x: usize, y: usize, z: usize) -> Result<CellType> {
        if z >= self.depth || y >= self.height || x >= self.width {
            return Err(Error::invalid_xyz(x, y, z));
        }
        Ok(self.cells[z][y][x])
    }

    fn width(&self) -> usize {
        self.width
    }

    fn height(&self) -> usize {
        self.height
    }

    fn depth(&self) -> usize {
        self.depth
    }
}

impl Shape3D for Source3DFromImage {
    fn shape(&self) -> (usize, usize, usize) {
        (self.width, self.height, self.depth)
    }
}

impl std::fmt::Display for Source3DFromImage {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "Source3DFromImage ({}x{}x{}):", self.width(), self.height(), self.depth())?;
        write!(f, "{}", crate::mesh_source::repr::repr_source_3d(self))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use image::{GrayImage, Luma};

    fn create_test_image(width: u32, height: u32, brightness: u8) -> DynamicImage {
        let img = GrayImage::from_fn(width, height, |_, _| Luma([brightness]));
        DynamicImage::ImageLuma8(img)
    }

    fn create_test_image_pattern(width: u32, height: u32) -> DynamicImage {
        let img = GrayImage::from_fn(width, height, |x, y| {
            // Create a checkerboard pattern
            if (x + y) % 2 == 0 {
                Luma([255]) // Bright (free)
            } else {
                Luma([0]) // Dark (wall)
            }
        });
        DynamicImage::ImageLuma8(img)
    }

    #[test]
    fn test_dimensions() {
        let layer0 = create_test_image(10, 8, 255);
        let layer1 = create_test_image(10, 8, 255);
        let layer2 = create_test_image(10, 8, 255);

        let source = Source3DFromImage::new(&[layer0, layer1, layer2]).unwrap();
        assert_eq!(source.width(), 10);
        assert_eq!(source.height(), 8);
        assert_eq!(source.depth(), 3);
        assert_eq!(Shape3D::shape(&source), (10, 8, 3));
    }

    #[test]
    fn test_threshold() {
        // Create images with different brightness
        let bright = create_test_image(5, 5, 200); // Above threshold (200/255 ≈ 0.78 > 0.5)
        let dark = create_test_image(5, 5, 50);    // Below threshold (50/255 ≈ 0.20 < 0.5)

        let source = Source3DFromImage::new(&[bright, dark]).unwrap();

        // Layer 0 (bright) should be all FREE
        assert_eq!(source.get(0, 0, 0).unwrap(), CellType::FLOOR);
        assert_eq!(source.get(4, 4, 0).unwrap(), CellType::FLOOR);
        assert_eq!(source.get(2, 2, 0).unwrap(), CellType::FLOOR);

        // Layer 1 (dark) should be all WALL
        assert_eq!(source.get(0, 0, 1).unwrap(), CellType::WALL);
        assert_eq!(source.get(4, 4, 1).unwrap(), CellType::WALL);
        assert_eq!(source.get(2, 2, 1).unwrap(), CellType::WALL);
    }

    #[test]
    fn test_pattern() {
        let layer = create_test_image_pattern(4, 4);
        let source = Source3DFromImage::new(&[layer]).unwrap();

        // Checkerboard pattern
        assert_eq!(source.get(0, 0, 0).unwrap(), CellType::FLOOR);  // (0+0)%2==0
        assert_eq!(source.get(1, 0, 0).unwrap(), CellType::WALL);  // (1+0)%2==1
        assert_eq!(source.get(0, 1, 0).unwrap(), CellType::WALL);  // (0+1)%2==1
        assert_eq!(source.get(1, 1, 0).unwrap(), CellType::FLOOR);  // (1+1)%2==0
    }

    #[test]
    fn test_out_of_bounds() {
        let layer = create_test_image(5, 5, 255);
        let source = Source3DFromImage::new(&[layer]).unwrap();

        assert!(source.get(5, 0, 0).is_err());
        assert!(source.get(0, 5, 0).is_err());
        assert!(source.get(0, 0, 1).is_err());
        assert!(source.get(10, 10, 10).is_err());
    }

    #[test]
    fn test_mismatched_dimensions() {
        let layer0 = create_test_image(10, 8, 255);
        let layer1 = create_test_image(10, 10, 255); // Different height

        let result = Source3DFromImage::new(&[layer0, layer1]);
        assert!(result.is_err());
    }

    #[test]
    fn test_empty_images() {
        let result = Source3DFromImage::new(&[]);
        assert!(result.is_err());
    }

    #[test]
    fn test_from_paths_testdata() {
        use crate::mesh_source::repr_source_3d;
        use crate::mesh_source::testutil::testdata_path;

        let paths = vec![
            testdata_path("3d/blob1-20x10.png"),
            testdata_path("3d/blob2-20x10.png"),
            testdata_path("3d/blob3-20x10.png"),
        ];

        let source = Source3DFromImage::from_paths(&paths, 0.5).unwrap();

        // Verify dimensions match filename
        assert_eq!(source.width(), 20);
        assert_eq!(source.height(), 10);
        assert_eq!(source.depth(), 3);

        // Get the string representation
        let repr = repr_source_3d(&source);

        // Expected pattern - three layers with blob shapes
        // Each layer shows a different blob pattern
        let expected = "\
....................
....................
....#.##............
..###########.......
...############.....
....###########.....
......######........
....................
....................
....................
----
................####
.................###
....#.##.........###
..###########....###
...############...##
....###########....#
#.....######........
#...................
##..................
###.................
----
................####
.................###
....#.#............#
..#####............#
...####............#
....###............#
#.....#.............
#...................
##..................
###.................
----
";

        assert_eq!(repr, expected, "3D image representation should match expected pattern");
    }

    #[test]
    fn test_from_paths_with_threshold_testdata() {
        use crate::mesh_source::testutil::testdata_path;

        let paths = vec![
            testdata_path("3d/blob1-20x10.png"),
            testdata_path("3d/blob2-20x10.png"),
            testdata_path("3d/blob3-20x10.png"),
        ];

        // Test with different thresholds
        let source_low = Source3DFromImage::from_paths(&paths, 0.1).unwrap();
        let source_high = Source3DFromImage::from_paths(&paths, 0.9).unwrap();

        // Count free cells for each threshold
        let mut free_count_low = 0;
        let mut free_count_high = 0;

        for z in 0..source_low.depth() {
            for y in 0..source_low.height() {
                for x in 0..source_low.width() {
                    if source_low.get(x, y, z).unwrap() == CellType::FLOOR {
                        free_count_low += 1;
                    }
                    if source_high.get(x, y, z).unwrap() == CellType::FLOOR {
                        free_count_high += 1;
                    }
                }
            }
        }

        // Lower threshold should have more or equal free cells
        assert!(
            free_count_low >= free_count_high,
            "Lower threshold should have more or equal free cells than higher threshold"
        );
    }

    #[test]
    fn test_from_images_testdata() {
        use crate::mesh_source::testutil::testdata_path;

        // Load images first
        let img1 = image::open(testdata_path("3d/blob1-20x10.png")).unwrap();
        let img2 = image::open(testdata_path("3d/blob2-20x10.png")).unwrap();
        let img3 = image::open(testdata_path("3d/blob3-20x10.png")).unwrap();

        let images = vec![img1, img2, img3];
        let source = Source3DFromImage::from_images(&images, 0.5).unwrap();

        // Verify dimensions
        assert_eq!(source.width(), 20);
        assert_eq!(source.height(), 10);
        assert_eq!(source.depth(), 3);

        // Verify we can access all cells
        assert!(source.get(0, 0, 0).is_ok());
        assert!(source.get(19, 9, 2).is_ok());
        assert!(source.get(20, 0, 0).is_err()); // Out of bounds
        assert!(source.get(0, 0, 3).is_err()); // Out of bounds
    }
}
