#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![warn(clippy::dbg_macro, clippy::todo, missing_docs)]

#[cfg(feature = "image")]
mod image;

mod array2d;
mod bordercrop;
mod math;
mod smartcrop;

use std::{marker::PhantomData, num::NonZeroU32};

pub use bordercrop::find_border_crop;
pub use smartcrop::find_best_crop;

/// Trait for images to be procressed by Smartcrop
pub trait Image: Sized {
    /// Get the width of the image
    fn width(&self) -> u32;
    /// Get the height of the image
    fn height(&self) -> u32;
    /// Get the color of a pixel
    fn get(&self, x: u32, y: u32) -> RGB;
}

/// Trait for images to be resized by Smartcrop
///
/// Smartcrop downscales images to improve performance
pub trait ResizableImage<I: Image> {
    /// Resize the image to the specified dimensions
    fn resize(&self, width: u32, height: u32) -> I;
    /// First crop the image, then resize it to the specified dimensions
    fn crop_and_resize(&self, crop: Crop, width: u32, height: u32) -> I;
}

/// Error that occurred during a Smartcrop operation
#[derive(PartialEq, Debug)]
pub enum Error {
    /// The given image is of size zero
    ZeroSizedImage,
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::ZeroSizedImage => f.write_str("zero-sized image"),
        }
    }
}

/// 24bit RGB color
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct RGB {
    /// Red (0-255)
    pub r: u8,
    /// Green (0-255)
    pub g: u8,
    /// Blue (0-255)
    pub b: u8,
}

impl RGB {
    /// Create a new 24bit RGB color
    pub fn new(r: u8, g: u8, b: u8) -> RGB {
        RGB { r, g, b }
    }

    fn cie(self: &RGB) -> f64 {
        //TODO: Change it as soon as https://github.com/jwagner/smartcrop.js/issues/77 is closed
        0.5126 * self.b as f64 + 0.7152 * self.g as f64 + 0.0722 * self.r as f64
    }

    fn saturation(self: &RGB) -> f64 {
        let maximum = f64::max(
            f64::max(self.r as f64 / 255.0, self.g as f64 / 255.0),
            self.b as f64 / 255.0,
        );
        let minimum = f64::min(
            f64::min(self.r as f64 / 255.0, self.g as f64 / 255.0),
            self.b as f64 / 255.0,
        );

        if maximum == minimum {
            return 0.0;
        }

        let l = (maximum + minimum) / 2.0;
        let d = maximum - minimum;

        if l > 0.5 {
            d / (2.0 - maximum - minimum)
        } else {
            d / (maximum + minimum)
        }
    }

    fn normalize(&self) -> [f64; 3] {
        if self.r == self.g && self.g == self.b {
            let inv_sqrt_3: f64 = 1.0 / 3.0f64.sqrt();
            return [inv_sqrt_3, inv_sqrt_3, inv_sqrt_3];
        }

        let r = self.r as f64;
        let g = self.g as f64;
        let b = self.b as f64;

        let mag = (r.powi(2) + g.powi(2) + b.powi(2)).sqrt();

        [r / mag, g / mag, b / mag]
    }
}

/// Score used to determine the best crop
#[derive(Clone, PartialEq, Debug)]
pub struct Score {
    /// Detail score
    pub detail: f64,
    /// Saturation score
    pub saturation: f64,
    /// Skin color score
    pub skin: f64,
    /// Total weighted score of the crop
    pub total: f64,
}

/// Crop position and size
#[derive(Clone, PartialEq, Debug)]
pub struct Crop {
    /// x position of the cropped image
    pub x: u32,
    /// y position of the cropped image
    pub y: u32,
    /// width of the cropped image
    pub width: u32,
    /// height of the cropped image
    pub height: u32,
}

impl Crop {
    fn scale(&self, ratio: f64) -> Crop {
        Crop {
            x: (self.x as f64 * ratio).round() as u32,
            y: (self.y as f64 * ratio).round() as u32,
            width: (self.width as f64 * ratio).round() as u32,
            height: (self.height as f64 * ratio).round() as u32,
        }
    }

    fn base_on(&self, other: &Crop) -> Crop {
        Crop {
            x: self.x + other.x,
            y: self.y + other.y,
            width: self.width,
            height: self.height,
        }
    }
}

/// Crop with attached score
#[derive(Debug)]
pub struct ScoredCrop {
    /// Crop position and size
    pub crop: Crop,
    /// Score used to determine the best crop
    pub score: Score,
}

impl ScoredCrop {
    /// Scale the crop to be applied to an image of a different size
    pub fn scale(&self, ratio: f64) -> ScoredCrop {
        ScoredCrop {
            crop: self.crop.scale(ratio),
            score: self.score.clone(),
        }
    }
}

/// Image wrapper for cropping images without modifying the underlying data structure
struct CroppedImage<'a, RI: Image + ResizableImage<I>, I: Image> {
    image: &'a RI,
    crop: Crop,
    _marker: PhantomData<I>,
}

impl<'a, RI: Image + ResizableImage<I>, I: Image> CroppedImage<'a, RI, I> {
    fn new(image: &'a RI, crop: Crop) -> Self {
        Self {
            image,
            crop,
            _marker: PhantomData,
        }
    }
}

impl<RI: Image + ResizableImage<I>, I: Image> Image for CroppedImage<'_, RI, I> {
    fn width(&self) -> u32 {
        self.crop.width
    }

    fn height(&self) -> u32 {
        self.crop.height
    }

    fn get(&self, x: u32, y: u32) -> RGB {
        self.image.get(self.crop.x + x, self.crop.y + y)
    }
}

impl<RI: Image + ResizableImage<I>, I: Image> ResizableImage<I> for CroppedImage<'_, RI, I> {
    fn resize(&self, width: u32, height: u32) -> I {
        self.image.crop_and_resize(self.crop.clone(), width, height)
    }

    fn crop_and_resize(&self, crop: Crop, width: u32, height: u32) -> I {
        self.image
            .crop_and_resize(crop.base_on(&self.crop), width, height)
    }
}

/// Analyze the image and find the best crop of the given aspect ratio
/// which excludes black borders
pub fn find_best_crop_no_borders<I: Image + ResizableImage<RI>, RI: Image>(
    img: &I,
    width: NonZeroU32,
    height: NonZeroU32,
) -> Result<ScoredCrop, Error> {
    let pre_crop = find_border_crop(img);

    match pre_crop {
        Some(crop) => {
            let cropped = CroppedImage::new(img, crop.clone());
            let mut best_crop = find_best_crop(&cropped, width, height)?;
            best_crop.crop = best_crop.crop.base_on(&crop);
            Ok(best_crop)
        }
        None => find_best_crop(img, width, height),
    }
}
