use image::DynamicImage;
use log::{debug, error, log_enabled, trace, warn, Level::Trace};
use mime::Mime;
use std::fs::File;
use std::io::{BufReader, Seek, SeekFrom};
use std::path::Path;

use crate::errors::ToeErrorType;
use crate::mimesniff;

#[derive(Debug, PartialEq)]
pub enum Rotation {
    R0,
    R90,
    R180,
    R270,
}

#[derive(Debug, PartialEq)]
pub struct ReOrientation {
    pub rotation: Rotation,
    pub flipped: bool,
}

impl Rotation {
    pub fn degrees(&self) -> usize {
        match self {
            Rotation::R0 => 0,
            Rotation::R90 => 90,
            Rotation::R180 => 180,
            Rotation::R270 => 270,
        }
    }
}

impl std::fmt::Display for ReOrientation {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Rotated {} degrees, {}",
            self.rotation.degrees(),
            if self.flipped {
                "flipped"
            } else {
                "not flipped"
            }
        )
    }
}

pub struct Source {
    pub image: Option<DynamicImage>,
    pub mime: Mime,
    pub reorientation: Option<ReOrientation>,
}

impl Source {
    pub fn load(input_file: &String) -> Result<Source, ToeErrorType> {
        let file = File::open(input_file).map_err(|_| ToeErrorType::SourceFileCantBeOpened)?;
        let mut reader = BufReader::new(file);
        let reorientation = calc_reorientation(&mut reader)?;
        let (image, mime) = load_image(input_file, reader)?;
        Ok(Source {
            image,
            mime,
            reorientation,
        })
    }
}

/// For a certain input BufReader, this function checks if EXIF data is found.
/// If yes, it further checks if some “reorientation” tags are set in the EXIF data.
/// If yes, this function returns with `Ok(Some(ReOrientation))`.
/// If no EXIF re-orientation information exists, `Ok(None)` is returned.
fn calc_reorientation(reader: &mut BufReader<File>) -> Result<Option<ReOrientation>, ToeErrorType> {
    let exif_option = exif::Reader::new().read_from_container(reader).ok();
    let re_orientation = if let Some(exif_data) = &exif_option {
        debug!("Found EXIF data in input.");
        if let Some(field) = exif_data.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
            if log_enabled!(Trace) {
                trace!("EXIF fields:");
                for f in exif_data.fields() {
                    trace!(
                        "  {}/{}: {}",
                        f.ifd_num.index(),
                        f.tag,
                        f.display_value().with_unit(exif_data)
                    );
                }
            }
            if let Some(value) = field.value.get_uint(0) {
                trace!("Found orientation tag with value {}.", value);
                match value {
                    1 => Some(ReOrientation {
                        rotation: Rotation::R0,
                        flipped: false,
                    }),
                    2 => Some(ReOrientation {
                        rotation: Rotation::R0,
                        flipped: true,
                    }),
                    3 => Some(ReOrientation {
                        rotation: Rotation::R180,
                        flipped: false,
                    }),
                    4 => Some(ReOrientation {
                        rotation: Rotation::R180,
                        flipped: true,
                    }),
                    5 => Some(ReOrientation {
                        rotation: Rotation::R270,
                        flipped: true,
                    }),
                    6 => Some(ReOrientation {
                        rotation: Rotation::R270,
                        flipped: false,
                    }),
                    7 => Some(ReOrientation {
                        rotation: Rotation::R90,
                        flipped: true,
                    }),
                    8 => Some(ReOrientation {
                        rotation: Rotation::R90,
                        flipped: false,
                    }),
                    v => {
                        error!(
                            "Value {} of EXIF orientation tag invalid. Assuming no re-orientation.",
                            v
                        );
                        None
                    }
                }
            } else {
                error!("Input file has EXIF orientation tag but the value can't be read. Assuming no re-orientation.");
                None
            }
        } else {
            trace!("No orientation tag found in EXIF data.");
            None
        }
    } else {
        trace!("No exif data found in input.");
        None
    };
    Ok(re_orientation)
}

/// Loads an image from an existing buffer and returns the image as `Some(DynamicImage)`
/// and the image file's mime type. If the mime type can be detected but cannot be loaded,
/// `None` is returned.
///
/// The buffer is given here to re-use an existing buffer from the former EXIF-check.
/// In case the mime type needs to be probed separately, the image file is read a second time
/// inside of this function.
pub fn load_image(
    input_file: &String,
    mut reader: BufReader<File>,
) -> Result<(Option<DynamicImage>, Mime), ToeErrorType> {
    reader.seek(SeekFrom::Start(0)).map_err(|_| {
        ToeErrorType::InputFileError("Cannot reset read position for input file.".to_string())
    })?;
    match image::ImageFormat::from_path(input_file) {
        Ok(image_format) => {
            trace!(
                "Detected format of source image based on file name: {:?}",
                image_format,
            );
            match image::load(reader, image_format) {
                Ok(img) => {
                    let mime_db = xdg_mime::SharedMimeInfo::new();
                    let mime_types: Vec<mime::Mime> =
                        mime_db.get_mime_types_from_file_name(input_file);
                    trace!("From file name deduced these mime types: {:?}", mime_types);
                    Ok((Some(img), mime_types[0].clone()))
                }
                Err(_) => {
                    warn!("Unable to decode file based on file suffix. Probing MIME type...");
                    let file =
                        File::open(input_file).map_err(|_| ToeErrorType::SourceFileCantBeOpened)?;
                    let reader = BufReader::new(file);
                    _get_image_from_mime(input_file, reader)
                }
            }
        }
        Err(_) => {
            trace!("Filename does not indicate a supported image format. Probing MIME type...");
            _get_image_from_mime(input_file, reader)
        }
    }
}

/// Loads the input image by first sniffing the MIME type.
fn _get_image_from_mime(
    input_file: &String,
    reader: BufReader<File>,
) -> Result<(Option<DynamicImage>, mime::Mime), ToeErrorType> {
    let mime_type = mimesniff::mime_type(Path::new(input_file))
        .map_err(|_| ToeErrorType::ErrorOnProbingMimeType)?
        .ok_or(ToeErrorType::ProbingMimeTypeHadNoResult)?;
    trace!("Detected MIME type: {}", mime_type);

    match image::ImageFormat::from_mime_type(mime_type.as_ref()) {
        Some(image_format) => {
            trace!("MIME type detected as image type.");
            match image::load(reader, image_format) {
                Ok(img) => Ok((Some(img), mime_type)),
                Err(_) => Err(ToeErrorType::SourceFileCantBeRead),
            }
        }
        None => {
            trace!("MIME type not a supported image type.");
            Ok((None, mime_type))
        }
    }
}
