use super::{Image, ImageAndMetadata, ImageMetadata};
use crate::core::image::{PixelStorage, WrapMode};
use crate::utils::error::ImageError;
use anyhow::{Context, Result, bail};
use exr::prelude::{read_first_rgba_layer_from_file, write_rgba_file};
use image_rs::{DynamicImage, ImageReader};
use shared::Float;
use shared::core::color::{ColorEncoding, LINEAR, SRGB};
use shared::core::geometry::Point2i;
use shared::core::image::PixelFormat;
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
use std::path::Path;

pub trait ImageIO {
    fn read(path: &Path, encoding: Option<ColorEncoding>) -> Result<ImageAndMetadata>;
    fn write(&self, filename: &str, metadata: &ImageMetadata) -> Result<()>;
    fn write_png(&self, path: &Path) -> Result<()>;
    fn write_exr(&self, path: &Path, metadata: &ImageMetadata) -> Result<()>;
    fn write_qoi(&self, path: &Path) -> Result<()>;
    fn write_pfm(&self, path: &Path) -> Result<()>;
    fn to_u8_buffer(&self) -> Vec<u8>;
}

impl ImageIO for Image {
    fn read(path: &Path, encoding: Option<ColorEncoding>) -> Result<ImageAndMetadata> {
        let ext = path
            .extension()
            .and_then(|s| s.to_str())
            .unwrap_or("")
            .to_lowercase();

        match ext.as_str() {
            "exr" => read_exr(path),
            "pfm" => read_pfm(path),
            _ => read_generic(path, encoding),
        }
    }

    fn write(&self, filename: &str, metadata: &ImageMetadata) -> Result<()> {
        let path = Path::new(filename);
        let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
        let res = match ext.to_lowercase().as_str() {
            "exr" => self.write_exr(path, metadata),
            "png" => self.write_png(path),
            "pfm" => self.write_pfm(path),
            "qoi" => self.write_qoi(path),
            _ => Err(anyhow::anyhow!("Unsupported write format: {}", ext)),
        };
        res.map_err(|e| ImageError::Io(std::io::Error::other(e)))?;
        Ok(())
    }

    fn write_png(&self, path: &Path) -> Result<()> {
        let w = self.resolution().x() as u32;
        let h = self.resolution().y() as u32;

        // Convert whatever we have to u8 [0..255]
        let data = self.to_u8_buffer();
        let channels = self.n_channels();

        match channels {
            1 => {
                // Luma
                image_rs::save_buffer_with_format(
                    path,
                    &data,
                    w,
                    h,
                    image_rs::ColorType::L8,
                    image_rs::ImageFormat::Png,
                )?;
            }
            3 => {
                // RGB
                image_rs::save_buffer_with_format(
                    path,
                    &data,
                    w,
                    h,
                    image_rs::ColorType::Rgb8,
                    image_rs::ImageFormat::Png,
                )?;
            }
            4 => {
                // RGBA
                image_rs::save_buffer_with_format(
                    path,
                    &data,
                    w,
                    h,
                    image_rs::ColorType::Rgba8,
                    image_rs::ImageFormat::Png,
                )?;
            }
            _ => bail!("PNG writer only supports 1, 3, or 4 channels"),
        }
        Ok(())
    }

    fn write_qoi(&self, path: &Path) -> Result<()> {
        let w = self.resolution().x() as u32;
        let h = self.resolution().y() as u32;
        let data = self.to_u8_buffer();

        let color_type = match self.n_channels() {
            3 => image_rs::ColorType::Rgb8,
            4 => image_rs::ColorType::Rgba8,
            _ => bail!("QOI only supports 3 or 4 channels"),
        };

        image_rs::save_buffer_with_format(
            path,
            &data,
            w,
            h,
            color_type,
            image_rs::ImageFormat::Qoi,
        )?;
        Ok(())
    }

    fn write_exr(&self, path: &Path, _metadata: &ImageMetadata) -> Result<()> {
        // EXR requires F32
        let w = self.resolution().x() as usize;
        let h = self.resolution().y() as usize;
        let c = self.n_channels();

        write_rgba_file(path, w, h, |x, y| {
            // Helper to get float value regardless of internal storage
            let get = |ch| {
                self.get_channel_with_wrap(
                    Point2i::new(x as i32, y as i32),
                    ch,
                    WrapMode::Clamp.into(),
                )
            };

            if c == 1 {
                let v = get(0);
                (v, v, v, 1.0)
            } else if c == 3 {
                (get(0), get(1), get(2), 1.0)
            } else {
                (get(0), get(1), get(2), get(3))
            }
        })
        .context("Failed to write EXR")?;

        Ok(())
    }

    fn write_pfm(&self, path: &Path) -> Result<()> {
        let file = File::create(path)?;
        let mut writer = BufWriter::new(file);

        if self.n_channels() != 3 {
            bail!("PFM writing currently only supports 3 channels (RGB)");
        }

        // Header
        let res = self.resolution();
        writeln!(writer, "PF")?;
        writeln!(writer, "{} {}", res.x(), res.y())?;
        let scale = if cfg!(target_endian = "little") {
            -1.0
        } else {
            1.0
        };
        writeln!(writer, "{}", scale)?;

        // PBRT stores top-to-bottom.
        for y in (0..res.y()).rev() {
            for x in 0..res.x() {
                for c in 0..3 {
                    let val =
                        self.get_channel_with_wrap(Point2i::new(x, y), c, WrapMode::Clamp.into());
                    writer.write_all(&val.to_le_bytes())?;
                }
            }
        }

        Ok(())
    }

    // TODO: Change Image to use Vec for data, always. Only convert to Device types on
    // constructors/creation
    fn to_u8_buffer(&self) -> Vec<u8> {
        match &self.pixels {
            PixelStorage::U8(data) => data.to_vec(),
            PixelStorage::F16(data) => data
                .iter()
                .map(|v| (v.to_f32().clamp(0.0, 1.0) * 255.0 + 0.5) as u8)
                .collect(),
            PixelStorage::F32(data) => data
                .iter()
                .map(|v| (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8)
                .collect(),
        }
    }
}

fn read_generic(path: &Path, encoding: Option<ColorEncoding>) -> Result<ImageAndMetadata> {
    let dyn_img = ImageReader::open(path)
        .with_context(|| format!("Failed to open image: {:?}", path))?
        .decode()?;

    let w = dyn_img.width() as i32;
    let h = dyn_img.height() as i32;
    let res = Point2i::new(w, h);

    // Check if it was loaded as high precision or standard
    let rgb_names = vec!["R", "G", "B"];
    let rgba_names = vec!["R", "G", "B", "A"];
    let image = match dyn_img {
        DynamicImage::ImageRgb32F(buf) => Image::from_f32(buf.into_raw(), res, &rgb_names),
        DynamicImage::ImageRgba32F(buf) => Image::from_f32(buf.into_raw(), res, &rgba_names),
        _ => {
            // Default to RGB8 for everything else
            let enc = encoding.unwrap_or(SRGB);
            if dyn_img.color().has_alpha() {
                let buf = dyn_img.to_rgba8();
                Image::from_u8(buf.into_raw(), res, &rgba_names, enc)
            } else {
                let buf = dyn_img.to_rgb8();
                Image::from_u8(buf.into_raw(), res, &rgb_names, enc)
            }
        }
    };

    let metadata = ImageMetadata::default();
    Ok(ImageAndMetadata { image, metadata })
}

fn read_exr(path: &Path) -> Result<ImageAndMetadata> {
    let image = read_first_rgba_layer_from_file(
        path,
        |resolution, _| {
            let size = resolution.width() * resolution.height() * 4;
            vec![0.0 as Float; size]
        },
        |buffer, position, pixel| {
            let width = position.width();
            let idx = (position.y() * width + position.x()) * 4;
            // Map exr pixel struct to our buffer
            buffer[idx] = pixel.0;
            buffer[idx + 1] = pixel.1;
            buffer[idx + 2] = pixel.2;
            buffer[idx + 3] = pixel.3;
        },
    )
    .with_context(|| format!("Failed to read EXR: {:?}", path))?;

    let w = image.layer_data.size.width() as i32;
    let h = image.layer_data.size.height() as i32;

    let image = Image::from_f32(
        image.layer_data.channel_data.pixels,
        Point2i::new(w, h),
        &vec!["R", "G", "B", "A"],
    );

    let metadata = ImageMetadata::default();
    Ok(ImageAndMetadata { image, metadata })
}

fn read_pfm(path: &Path) -> Result<ImageAndMetadata> {
    let file = File::open(path)?;
    let mut reader = BufReader::new(file);

    // PFM Headers are: "PF\nwidth height\nscale\n" (or Pf for grayscale)
    let mut header_word = String::new();
    reader.read_line(&mut header_word)?;
    let header_word = header_word.trim();

    let channels = match header_word {
        "PF" => 3,
        "Pf" => 1,
        _ => bail!("Invalid PFM header: {}", header_word),
    };

    let mut dims_line = String::new();
    reader.read_line(&mut dims_line)?;
    let dims: Vec<i32> = dims_line
        .split_whitespace()
        .map(|s| s.parse().unwrap_or(0))
        .collect();

    if dims.len() < 2 {
        bail!("Invalid PFM dimensions");
    }
    let w = dims[0];
    let h = dims[1];

    let mut scale_line = String::new();
    reader.read_line(&mut scale_line)?;
    let scale: f32 = scale_line.trim().parse().context("Invalid PFM scale")?;

    let file_is_little_endian = scale < 0.0;
    let abs_scale = scale.abs();

    let mut buffer = Vec::new();
    reader.read_to_end(&mut buffer)?;

    let expected_bytes = (w * h * channels) as usize * 4;
    if buffer.len() < expected_bytes {
        bail!("PFM file too short");
    }

    let mut pixels = vec![0.0 as Float; (w * h * channels) as usize];

    // PFM is Bottom-to-Top
    for y in 0..h {
        // Flippety-do
        let src_y = h - 1 - y;
        for x in 0..w {
            for c in 0..channels {
                let src_idx = ((src_y * w + x) * channels + c) as usize * 4;
                let dst_idx = ((y * w + x) * channels + c) as usize;

                let bytes: [u8; 4] = buffer[src_idx..src_idx + 4].try_into()?;

                let val = if file_is_little_endian {
                    f32::from_le_bytes(bytes)
                } else {
                    f32::from_be_bytes(bytes)
                };

                pixels[dst_idx] = val * abs_scale;
            }
        }
    }

    let names = if channels == 1 {
        vec!["Y"]
    } else {
        vec!["R", "G", "B"]
    };

    let image = Image::new(PixelFormat::F32, Point2i::new(w, h), &names, LINEAR.into());
    let metadata = ImageMetadata::default();

    Ok(ImageAndMetadata { image, metadata })
}
