use std::fs::File;
use std::panic;
use std::path::Path;

use image::{DynamicImage, ImageReader};
use log::{error, trace};
use nom_exif::{ExifIter, ExifTag, MediaParser, MediaSource};

use crate::common::consts::{HEIC_EXTENSIONS, IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS};
use crate::common::create_crash_message;
use crate::flc;

const MAXIMUM_IMAGE_PIXELS: u32 = 2_000_000_000;

pub fn register_image_decoding_hooks() {
    #[cfg(feature = "heif")]
    libheif_rs::integration::image::register_all_decoding_hooks();
    jxl_oxide::integration::register_image_decoding_hook();
}

// Using this instead of image::open because image::open only reads content of files if extension matches content
// This is not really helpful when trying to show preview of files with wrong extensions
pub(crate) fn decode_normal_image(path: &str) -> Result<DynamicImage, String> {
    let file = File::open(path).map_err(|e| e.to_string())?;
    let reader = ImageReader::new(std::io::BufReader::new(file)).with_guessed_format().map_err(|e| e.to_string())?;
    let img = reader.decode().map_err(|e| e.to_string())?;

    Ok(img)
}

pub fn get_dynamic_image_from_path(path: &str) -> Result<DynamicImage, String> {
    let path_lower = Path::new(path).extension().unwrap_or_default().to_string_lossy().to_lowercase();

    trace!("decoding file \"{path}\"");
    let res = panic::catch_unwind(|| {
        let img = if RAW_IMAGE_EXTENSIONS.iter().any(|ext| path_lower.ends_with(ext)) {
            get_raw_image(path).map_err(|e| flc!("core_image_open_failed", path = path, reason = e))
        } else {
            // Heic files must be registered in image-rs
            decode_normal_image(path).map_err(|e| flc!("core_image_open_failed", path = path, reason = e))
        }?;

        if img.width() == 0 || img.height() == 0 {
            return Err(flc!("core_image_zero_dimensions", path = path));
        }
        if img.width() as u64 * img.height() as u64 > MAXIMUM_IMAGE_PIXELS as u64 {
            return Err(flc!("core_image_too_large", width = img.width(), height = img.height(), max = MAXIMUM_IMAGE_PIXELS));
        }
        Ok(img)
    });

    if let Ok(res) = res {
        match res {
            Ok(t) => {
                if t.width() == 0 || t.height() == 0 {
                    return Err(flc!("core_image_zero_dimensions", path = path));
                }

                let rotation = get_rotation_from_exif(path).unwrap_or(None);
                match rotation {
                    Some(ExifOrientation::Normal) | None => Ok(t),
                    Some(ExifOrientation::MirrorHorizontal) => Ok(t.fliph()),
                    Some(ExifOrientation::Rotate180) => Ok(t.rotate180()),
                    Some(ExifOrientation::MirrorVertical) => Ok(t.flipv()),
                    Some(ExifOrientation::MirrorHorizontalAndRotate270CW) => Ok(t.fliph().rotate270()),
                    Some(ExifOrientation::Rotate90CW) => Ok(t.rotate90()),
                    Some(ExifOrientation::MirrorHorizontalAndRotate90CW) => Ok(t.fliph().rotate90()),
                    Some(ExifOrientation::Rotate270CW) => Ok(t.rotate270()),
                }
            }
            Err(e) => Err(flc!("core_image_open_failed", path = path, reason = e)),
        }
    } else {
        let message = create_crash_message("Image-rs or libraw-rs or jxl-oxide", path, "https://github.com/image-rs/image/issues");
        error!("{message}");
        Err(message)
    }
}

#[cfg(feature = "libraw")]
pub(crate) fn get_raw_image<P: AsRef<Path>>(path: P) -> Result<DynamicImage, String> {
    let buf = std::fs::read(path.as_ref()).map_err(|e| format!("Error reading image: {e}"))?;

    let processor = libraw::Processor::new();
    let processed = processor.process_8bit(&buf).map_err(|e| format!("Error processing RAW image: {e}"))?;

    let width = processed.width();
    let height = processed.height();

    let data = processed.to_vec();
    let data_len = data.len();

    let buffer = image::ImageBuffer::from_raw(width, height, data).ok_or(format!(
        "Cannot create ImageBuffer from raw image with width: {width} and height: {height} and data length: {data_len}",
    ))?;

    Ok(DynamicImage::ImageRgb8(buffer))
}

#[cfg(not(feature = "libraw"))]
pub(crate) fn get_raw_image<P: AsRef<Path> + std::fmt::Debug>(path: P) -> Result<DynamicImage, String> {
    use rawler::decoders::RawDecodeParams;
    use rawler::imgop::develop::RawDevelop;
    use rawler::rawsource::RawSource;

    let mut timer = crate::helpers::debug_timer::Timer::new("Rawler");

    let raw_source = RawSource::new(path.as_ref()).map_err(|err| format!("Failed to create RawSource from path {}: {err}", path.as_ref().to_string_lossy()))?;

    timer.checkpoint("Created RawSource");

    let decoder = rawler::get_decoder(&raw_source).map_err(|e| e.to_string())?;

    timer.checkpoint("Got decoder");

    let params = RawDecodeParams::default();

    // TODO - Nef currently disabled, due really bad quality of some extracted images https://github.com/dnglab/dnglab/issues/638, waiting for new release
    if !path.as_ref().to_string_lossy().to_ascii_lowercase().ends_with(".nef")
        && let Some(extracted_dynamic_image) = decoder.full_image(&raw_source, &params).ok().flatten()
    {
        timer.checkpoint("Decoded full image");

        trace!("{}", timer.report("Everything", false));

        return Ok(extracted_dynamic_image);
    }

    let raw_image = decoder.raw_image(&raw_source, &params, false).map_err(|e| e.to_string())?;

    timer.checkpoint("Decoded raw image");

    let developer = RawDevelop::default();
    let developed_image = developer.develop_intermediate(&raw_image).map_err(|e| e.to_string())?;

    timer.checkpoint("Developed raw image");

    let dynamic_image = developed_image.to_dynamic_image().ok_or("Failed to convert image to DynamicImage".to_string())?;

    timer.checkpoint("Converted to DynamicImage");

    trace!("{}", timer.report("Everything", false));

    Ok(dynamic_image)
}

pub fn check_if_can_display_image(path: &str) -> bool {
    let Some(extension) = Path::new(path).extension() else {
        return false;
    };
    let extension_str = extension.to_string_lossy().to_lowercase();
    #[cfg(feature = "heif")]
    let allowed_extensions = &[IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS, HEIC_EXTENSIONS].concat();

    #[cfg(not(feature = "heif"))]
    let allowed_extensions = &[IMAGE_RS_EXTENSIONS, RAW_IMAGE_EXTENSIONS].concat();

    allowed_extensions.iter().any(|ext| &extension_str == ext)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExifOrientation {
    Normal,
    MirrorHorizontal,
    Rotate180,
    MirrorVertical,
    MirrorHorizontalAndRotate270CW,
    Rotate90CW,
    MirrorHorizontalAndRotate90CW,
    Rotate270CW,
}

pub(crate) fn get_rotation_from_exif(path: &str) -> Result<Option<ExifOrientation>, nom_exif::Error> {
    if let Some(extension) = Path::new(path).extension()
        && HEIC_EXTENSIONS.contains(&extension.to_string_lossy().to_lowercase().as_str())
    {
        return Ok(None); // libheif already applies orientation
    }

    let res = panic::catch_unwind(|| {
        let mut parser = MediaParser::new();
        let ms = MediaSource::file_path(path)?;
        if !ms.has_exif() {
            return Ok(None);
        }
        let exif_iter: ExifIter = parser.parse(ms)?;
        for exif_entry in exif_iter {
            if exif_entry.tag() == Some(ExifTag::Orientation)
                && let Some(value) = exif_entry.get_value()
            {
                return match value.to_string().as_str() {
                    "1" => Ok(Some(ExifOrientation::Normal)),
                    "2" => Ok(Some(ExifOrientation::MirrorHorizontal)),
                    "3" => Ok(Some(ExifOrientation::Rotate180)),
                    "4" => Ok(Some(ExifOrientation::MirrorVertical)),
                    "5" => Ok(Some(ExifOrientation::MirrorHorizontalAndRotate270CW)),
                    "6" => Ok(Some(ExifOrientation::Rotate90CW)),
                    "7" => Ok(Some(ExifOrientation::MirrorHorizontalAndRotate90CW)),
                    "8" => Ok(Some(ExifOrientation::Rotate270CW)),
                    _ => Ok(None),
                };
            }
        }
        Ok(None)
    });

    res.unwrap_or_else(|_| {
        let message = create_crash_message("nom-exif", path, "https://github.com/mindeng/nom-exif");
        error!("{message}");
        Err(nom_exif::Error::IOError(std::io::Error::other("Panic in get_rotation_from_exif")))
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    const TEST_NORMAL_IMAGE: &str = "test_resources/images/normal.jpg";
    const TEST_ROTATED_IMAGE: &str = "test_resources/images/rotated.jpg";

    #[test]
    fn test_image_loading_and_exif_rotation() {
        let normal_img = get_dynamic_image_from_path(TEST_NORMAL_IMAGE).unwrap();
        let rotated_img = get_dynamic_image_from_path(TEST_ROTATED_IMAGE).unwrap();

        assert!(normal_img.width() > 0 && normal_img.height() > 0);
        assert!(rotated_img.width() > 0 && rotated_img.height() > 0);

        let normal_exif = get_rotation_from_exif(TEST_NORMAL_IMAGE).ok();
        let rotated_exif = get_rotation_from_exif(TEST_ROTATED_IMAGE).ok();

        if let Some(normal_orientation) = normal_exif {
            assert!(normal_orientation == Some(ExifOrientation::Normal) || normal_orientation.is_none());
        }

        if let Some(rotated_orientation) = rotated_exif
            && rotated_orientation.is_some()
        {
            let raw_rotated = decode_normal_image(TEST_ROTATED_IMAGE).unwrap();
            if rotated_orientation == Some(ExifOrientation::Rotate90CW) || rotated_orientation == Some(ExifOrientation::Rotate270CW) {
                assert_eq!(rotated_img.width(), raw_rotated.height());
                assert_eq!(rotated_img.height(), raw_rotated.width());
            }
        }
    }

    #[test]
    fn test_check_if_can_display_image() {
        assert!(check_if_can_display_image("test.jpg"));
        assert!(check_if_can_display_image("test.png"));
        assert!(check_if_can_display_image("test.webp"));
        assert!(check_if_can_display_image("test.jxl"));
        assert!(check_if_can_display_image("test.cr2"));
        assert!(check_if_can_display_image("test.JPG"));

        assert!(!check_if_can_display_image("test.txt"));
        assert!(!check_if_can_display_image("test.mp4"));
        assert!(!check_if_can_display_image("test"));
    }

    #[test]
    fn test_error_handling() {
        get_dynamic_image_from_path("nonexistent.jpg").unwrap_err();
        decode_normal_image("nonexistent.jpg").unwrap_err();
        get_rotation_from_exif("nonexistent.jpg").unwrap_err();
    }
}
