// This file is part of AllMyToes.
//
// AllMyToes is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation,
// either version 3 of the License, or (at your option) any later version.
//
// AllMyToes is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with AllMyToes.
// If not, see <https://www.gnu.org/licenses/>.
//
mod errors;
mod mimesniff;
mod thumbrequest;
mod thumbsize;
mod writer;
pub use errors::ToeErrorType;
use log::{debug, trace, warn};
use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use thumbrequest::ThumbRequest;
pub use thumbsize::ThumbSize;

/// Get the path to a thumbnail for the given image and with the given size.
pub fn get_thumbnail(
    source_path: String,
    size: ThumbSize,
    force_creation: bool,
) -> Result<String, ToeErrorType> {
    let toe = ThumbRequest::new(source_path, size, force_creation)?;
    get(&toe)
}

/// Returns the path to the thumbnail for a thumbnail request.
///
/// Tests that the input file is readable and returns an Error if not.
/// According to the standard, no thumb shall be provided if the input file is not readable, not
/// matter if a thumb already exists and reading the input would technically not be necessary.
///
/// Issues the creation of the thumbnail series (thumbs for all supported sizes)
/// if the requested size does not exists or is outdated.
fn get(toe: &ThumbRequest) -> Result<String, ToeErrorType> {
    trace!("Get for toe struct {:?}", toe);
    if File::open(&toe.file_path).is_err() {
        return Err(ToeErrorType::FileNotReadable);
    }
    let input_file_path = PathBuf::from(&toe.file_path);
    if input_file_path.starts_with(&toe.thumb_dir) {
        trace!("Input file is located within the thumbnail repository.");
        if let Some(extension) = input_file_path.extension() {
            if extension == "png" {
                trace!("Input file has extension 'png'. Returning the input file as thumb.");
                return Ok(String::from(&toe.file_path));
            }
        }
    }
    if let Some(toe_path) = toe.get_path_to_thumb_if_existing_and_valid(toe.size)? {
        if toe.force_creation {
            trace!("Thumb exists but creation forced. Generate all...");
            generate_all_and_check(toe)
        } else {
            let toe_path_string = toe_path.as_os_str().to_os_string().into_string().unwrap();
            trace!("Thumb exists at {}", toe_path_string);
            Ok(toe_path_string)
        }
    } else {
        trace!("Thumb does not exist. Generate all...");
        generate_all_and_check(toe)
    }
}

/// Issues the creation of the thumbnail series, checks that the requested thumb is available
/// afterwards and returns it or returns an error otherwise.
fn generate_all_and_check(toe: &ThumbRequest) -> Result<String, ToeErrorType> {
    generate_all(toe)?;
    if let Some(toe_path) = toe.get_path_to_thumb_if_existing_and_valid(toe.size)? {
        trace!("Thumb created. Returning it now.");
        Ok(toe_path.as_os_str().to_os_string().into_string().unwrap())
    } else {
        trace!("Thumb should have been created but it doesn't seem to exists.");
        Err(ToeErrorType::ThumbDoesNotExistsAfterCreation)
    }
}

/// Creates the thumbnail series.
///
/// First the input image is read and decoded based on the file name exitension.
/// If that succeeds, the MIME type is derived from the file name for putting it later as meta
/// information into the thumb.
///
/// If either the file extension check or the decoding attempt fails,
/// the MIME type is sniffed from the input and used to do the encoding.
///
/// After successfully reading the input, each thumb size is first checked for existing with a
/// valid mtime and (re-) created if no valid thumb exists for that particular size.
fn generate_all(toe: &ThumbRequest) -> Result<(), ToeErrorType> {
    let file = File::open(&toe.file_path).map_err(|_| ToeErrorType::SourceFileCantBeOpened)?;
    let reader = BufReader::new(file);
    let (mut image, mime_type) = match image::ImageFormat::from_path(&toe.file_path) {
        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(&toe.file_path);
                    trace!("From file name deduced these mime types: {:?}", mime_types);
                    (img, mime_types[0].clone())
                }
                Err(_) => {
                    warn!("Unable to decode file based on file suffix. Probing MIME type...");
                    let file = File::open(&toe.file_path)
                        .map_err(|_| ToeErrorType::SourceFileCantBeOpened)?;
                    let reader = BufReader::new(file);
                    _get_image_from_mime(toe, reader)?
                }
            }
        }
        Err(_) => {
            trace!("Filename does not indicate a supported image format. Probing MIME type...");
            _get_image_from_mime(toe, reader)?
        }
    };

    let original_size = (image.width(), image.height());
    trace!("Image loaded with size {:?}", original_size);

    if image.color() == image::ColorType::Rgba8 {
        trace!("Image is 8-bit RGBA. No conversion needed.");
    } else {
        trace!("Image is not 8-bit RGBA and needs to be converted.");
        image = image::DynamicImage::ImageRgba8(image.into_rgba8());
    }

    for size in ThumbSize::order() {
        if toe.force_creation || toe.get_path_to_thumb_if_existing_and_valid(size)?.is_none() {
            trace!("Thumb with size {} will be created.", size.dir_name());
            image = generate_one_thumbnail(
                toe,
                &image,
                &toe.get_toe_path(size)?,
                size.max_thumb_width(),
                original_size,
                &mime_type,
            )?;
        } else {
            trace!("Thumb of size {} does exist. Skipping.", size.dir_name());
        }
    }
    Ok(())
}

/// Loads the input image by first sniffing the MIME type.
fn _get_image_from_mime(
    toe: &ThumbRequest,
    reader: BufReader<File>,
) -> Result<(image::DynamicImage, mime::Mime), ToeErrorType> {
    let mime_type = mimesniff::mime_type(Path::new(&toe.file_path))
        .map_err(|_| ToeErrorType::ErrorOnProbingMimeType)?
        .ok_or(ToeErrorType::ProbingMimeTypeHadNoResult)?;
    trace!("Detected MIME type: {}", mime_type);
    let image_format = image::ImageFormat::from_mime_type(mime_type.as_ref())
        .ok_or(ToeErrorType::FormatCannotBeHandled)?;
    match image::load(reader, image_format) {
        Ok(img) => Ok((img, mime_type)),
        Err(_) => Err(ToeErrorType::SourceFileCantBeRead),
    }
}

/// Generates one thumbnail (for one size), based on an input image and a given target-size.
///
/// The scaled thumbnail is also returned for reuse in another, smaller, thumbnail-generation
/// iteration.
///
/// This function basically only does the scaling and uses `writer::save_thumb` to actually store
/// the thumbnail with its meta data.
fn generate_one_thumbnail(
    toe: &ThumbRequest,
    source_image: &image::DynamicImage,
    dest_path: &std::path::Path,
    size: u32,
    original_size: (u32, u32),
    mime_type: &mime::Mime,
) -> Result<image::DynamicImage, ToeErrorType> {
    trace!(
        "Creating thumb with max. edge {} from original image size {:?}.",
        size,
        original_size
    );

    let thumb_image = source_image.thumbnail(size, size);
    //let thumb_image = source_image.clone();
    writer::save_thumb(toe, &thumb_image, dest_path, original_size, mime_type)?;
    debug!("Created '{:?}'", dest_path.as_os_str());
    Ok(thumb_image)
}
