use libvips::ops;
use ring::hmac;
use serde::{Deserialize, Deserializer, Serialize};
use std::cmp::PartialOrd;
use std::collections::BTreeMap;
use std::fmt::Display;
use std::ops::{Div, Mul};
use strum::Display;

#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(rename_all = "lowercase")]
pub struct ImageOptions {
    #[serde(default, rename = "sig", skip_serializing_if = "Option::is_none")]
    pub signature: Option<String>,

    /// Background color
    #[serde(
        default,
        rename = "bg",
        skip_serializing_if = "Option::is_none",
        deserialize_with = "deserialize_colour"
    )]
    pub background: Option<Colour>,

    /// Aspect ratio
    #[serde(
        default,
        rename = "ar",
        deserialize_with = "deserialize_aspect_ratio",
        skip_serializing_if = "Option::is_none"
    )]
    pub aspect_ratio: Option<AspectRatio>,

    /// Lossless output
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lossless: Option<bool>,

    /// Quality
    #[serde(default, rename = "q", skip_serializing_if = "Option::is_none")]
    pub quality: Option<i32>,

    /// Device Pixel Ratio
    #[serde(default, rename = "dpr", skip_serializing_if = "Option::is_none")]
    pub device_pixel_ratio: Option<i32>,

    /// Rotation
    #[serde(
        default,
        rename = "rot",
        deserialize_with = "deserialize_rotation",
        skip_serializing_if = "Option::is_none"
    )]
    pub rotate: Option<Rotation>,

    /// Width
    #[serde(
        default,
        rename = "w",
        deserialize_with = "deserialize_dimension",
        skip_serializing_if = "Option::is_none"
    )]
    pub width: Option<i32>,

    /// Height
    #[serde(
        default,
        rename = "h",
        deserialize_with = "deserialize_dimension",
        skip_serializing_if = "Option::is_none"
    )]
    pub height: Option<i32>,

    /// Fit
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub fit: Option<Fit>,

    /// Image format
    #[serde(default, rename = "fm", skip_serializing_if = "Option::is_none")]
    pub format: Option<Format>,

    /// Download
    #[serde(default, rename = "dl", skip_serializing_if = "Option::is_none")]
    pub download: Option<String>,

    /// Trim
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trim: Option<Trim>,

    #[serde(
        default,
        rename = "trim-colour",
        deserialize_with = "deserialize_colour",
        skip_serializing_if = "Option::is_none"
    )]
    pub trim_colour: Option<Colour>,
    //pub heif_effort: i32,
    //pub heif_encoder: Encoder,

    // Sharpen
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sharpen: Option<Percentage>,

    // Blur
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub blur: Option<Percentage>,

    // Filters
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub kodachrome: Option<Percentage>,

    #[serde(
        default,
        deserialize_with = "deserialize_percentage",
        skip_serializing_if = "Option::is_none"
    )]
    pub technicolor: Option<Percentage>,

    #[serde(
        default,
        deserialize_with = "deserialize_percentage",
        skip_serializing_if = "Option::is_none"
    )]
    pub vintage: Option<Percentage>,

    #[serde(
        default,
        deserialize_with = "deserialize_percentage",
        skip_serializing_if = "Option::is_none"
    )]
    pub polaroid: Option<Percentage>,

    #[serde(
        default,
        deserialize_with = "deserialize_percentage",
        skip_serializing_if = "Option::is_none"
    )]
    pub sepia: Option<Percentage>,

    #[serde(
        default,
        deserialize_with = "deserialize_percentage",
        skip_serializing_if = "Option::is_none"
    )]
    pub monochrome: Option<Percentage>,

    // Duotone filter with colours
    #[serde(
        default,
        deserialize_with = "deserialize_duotone_colours",
        skip_serializing_if = "Option::is_none"
    )]
    pub duotone: Option<DuotoneColours>,

    // Duotone alpha/opacity
    #[serde(
        default,
        rename = "duotone-alpha",
        deserialize_with = "deserialize_percentage",
        skip_serializing_if = "Option::is_none"
    )]
    pub duotone_alpha: Option<Percentage>,

    // Tint filter with colour
    #[serde(
        default,
        deserialize_with = "deserialize_colour",
        skip_serializing_if = "Option::is_none"
    )]
    pub tint: Option<Colour>,
}

impl Default for ImageOptions {
    fn default() -> Self {
        ImageOptions {
            signature: None,
            background: None,
            quality: None,
            aspect_ratio: None,
            download: None,
            trim: None,
            trim_colour: None,
            sharpen: None,
            blur: None,
            kodachrome: None,
            technicolor: None,
            vintage: None,
            polaroid: None,
            sepia: None,
            monochrome: None,
            duotone: None,
            duotone_alpha: None,
            tint: None,
            width: None,
            height: None,
            device_pixel_ratio: Some(1),
            rotate: None,
            format: None,
            //heif_effort: 6, // 0-6
            //heif_encoder: Encoder::Rav1E,
            lossless: None,
            fit: None,
        }
    }
}

impl ImageOptions {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn any_set(&self) -> bool {
        self.signature.is_some()
            || self.background.is_some()
            || (self.quality.is_some() && self.quality.unwrap() != 75)
            || self.aspect_ratio.is_some()
            || self.download.is_some()
            || self.trim.is_some()
            || self.trim_colour.is_some()
            || self.sharpen.is_some()
            || self.blur.is_some()
            || self.kodachrome.is_some()
            || self.technicolor.is_some()
            || self.vintage.is_some()
            || self.polaroid.is_some()
            || self.sepia.is_some()
            || self.monochrome.is_some()
            || self.duotone.is_some()
            || self.duotone_alpha.is_some()
            || self.tint.is_some()
            || self.width.is_some()
            || self.height.is_some()
            || (self.device_pixel_ratio.is_some() && self.device_pixel_ratio.unwrap() != 1)
            || self.rotate.is_some()
            || self.fit.is_some()
            || self.format.is_some()
            || self.lossless.is_some()
    }

    /// Calculate the resize scale based on the image dimensions and the specified width and height.
    pub fn get_resize_scale(&self, image_width: i32, image_height: i32) -> f64 {
        if let Some(width) = self.width {
            let scale_x = f64::from(width) / f64::from(image_width);
            if let Some(height) = self.height {
                scale_x.min(f64::from(height) / f64::from(image_height))
            } else {
                scale_x
            }
        } else if let Some(height) = self.height {
            f64::from(height) / f64::from(image_height)
        } else {
            1.0
        }
    }

    pub fn query_str(&self) -> String {
        let mut params: BTreeMap<String, String> = BTreeMap::new();

        // Insert each option if it has a value
        if let Some(bg) = &self.background {
            params.insert("background".into(), bg.into());
        }
        if let Some(q) = self.quality {
            params.insert("quality".into(), q.to_string());
        }
        if let Some(ar) = &self.aspect_ratio {
            params.insert("ar".into(), ar.to_string());
        }
        if let Some(download) = &self.download {
            params.insert("download".into(), download.to_string());
        }
        if let Some(trim) = &self.trim {
            params.insert("trim".into(), trim.to_string());
        }
        if let Some(trim_colour) = &self.trim_colour {
            params.insert("trim-colour".into(), trim_colour.into());
        }
        if let Some(sharpen) = &self.sharpen {
            params.insert("sharpen".into(), sharpen.0.to_string());
        }
        if let Some(blur) = &self.blur {
            params.insert("blur".into(), blur.0.to_string());
        }
        if let Some(kodachrome) = &self.kodachrome {
            params.insert("kodachrome".into(), kodachrome.0.to_string());
        }
        if let Some(technicolor) = &self.technicolor {
            params.insert("technicolor".into(), technicolor.0.to_string());
        }
        if let Some(vintage) = &self.vintage {
            params.insert("vintage".into(), vintage.0.to_string());
        }
        if let Some(polaroid) = &self.polaroid {
            params.insert("polaroid".into(), polaroid.0.to_string());
        }
        if let Some(sepia) = &self.sepia {
            params.insert("sepia".into(), sepia.0.to_string());
        }
        if let Some(monochrome) = &self.monochrome {
            params.insert("monochrome".into(), monochrome.0.to_string());
        }
        if let Some(duotone) = &self.duotone {
            let shadow_hex: String = (&duotone.shadow).into();
            let highlight_hex: String = (&duotone.highlight).into();
            params.insert(
                "duotone".into(),
                format!("{},{}", shadow_hex, highlight_hex),
            );
        }
        if let Some(duotone_alpha) = &self.duotone_alpha {
            params.insert("duotone-alpha".into(), duotone_alpha.0.to_string());
        }
        if let Some(tint) = &self.tint {
            params.insert("tint".into(), tint.into());
        }
        if let Some(width) = self.width {
            params.insert("width".into(), width.to_string());
        }
        if let Some(height) = self.height {
            params.insert("height".into(), height.to_string());
        }
        if let Some(dpr) = self.device_pixel_ratio {
            params.insert("dpr".into(), dpr.to_string());
        }
        if let Some(rot) = &self.rotate {
            params.insert("rot".into(), rot.0.to_string());
        }
        if let Some(fit) = &self.fit {
            params.insert("fit".into(), fit.to_string().to_lowercase());
        }
        if let Some(fmt) = &self.format {
            params.insert("format".into(), fmt.to_string());
        }
        if let Some(lossless) = self.lossless {
            params.insert("lossless".into(), lossless.to_string());
        }

        // Create the query string
        params
            .iter()
            .map(|(k, v)| format!("{}={}", k, v))
            .collect::<Vec<_>>()
            .join("&")
    }

    pub fn sign(&self, secret: &str) -> std::string::String {
        let key = hmac::Key::new(hmac::HMAC_SHA256, secret.as_bytes());
        hex::encode(hmac::sign(&key, self.query_str().as_bytes()).as_ref())
    }

    pub fn verify_signature(&self, signing_secret: &str) -> bool {
        if let Some(ref sig_hex) = self.signature
            && let Ok(sig_bytes) = hex::decode(sig_hex)
        {
            let key = hmac::Key::new(hmac::HMAC_SHA256, signing_secret.as_bytes());
            return ring::hmac::verify(&key, self.query_str().as_bytes(), &sig_bytes).is_ok();
        }
        false
    }
}

#[derive(Debug, Serialize, Clone, Copy, Deserialize, PartialEq)]
pub struct Percentage(pub u8);

impl Percentage {
    /// Create a new Percentage
    ///
    /// # Errors
    ///
    /// Returns an error if the value is not between 1 and 100
    pub fn new(value: u8) -> Result<Self, String> {
        if !(1..=100).contains(&value) {
            return Err(format!(
                "percentage must be between 1 and 100, got {}",
                value
            ));
        }
        Ok(Self(value))
    }
}

impl TryFrom<u8> for Percentage {
    type Error = String;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

impl From<&Percentage> for f64 {
    fn from(val: &Percentage) -> Self {
        f64::from(val.0)
    }
}

fn deserialize_percentage<'de, D>(deserializer: D) -> Result<Option<Percentage>, D::Error>
where
    D: Deserializer<'de>,
{
    match String::deserialize(deserializer) {
        Ok(value) => {
            let percentage = value.parse::<u8>().map_err(|err| {
                serde::de::Error::custom(format!("failed to parse percentage: {}", err))
            })?;
            Percentage::new(percentage)
                .map(Some)
                .map_err(serde::de::Error::custom)
        }
        Err(err) => Err(err),
    }
}

#[derive(Debug, Serialize, Clone, Deserialize, PartialEq)]
pub struct Rotation(pub u16);

impl Rotation {
    /// Create a new Rotation
    ///
    /// # Errors
    ///
    /// Returns an error if the value is not 90, 180, or 270
    pub fn new(value: u16) -> Result<Self, String> {
        if value != 90 && value != 180 && value != 270 {
            return Err(format!(
                "rotation must be one of 90, 180, or 270, got {}",
                value
            ));
        }
        Ok(Self(value))
    }
}

impl TryFrom<u16> for Rotation {
    type Error = String;

    fn try_from(value: u16) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

impl From<&Rotation> for f64 {
    fn from(val: &Rotation) -> Self {
        f64::from(val.0)
    }
}

fn deserialize_rotation<'de, D>(deserializer: D) -> Result<Option<Rotation>, D::Error>
where
    D: Deserializer<'de>,
{
    match u16::deserialize(deserializer) {
        Ok(value) => Rotation::new(value)
            .map(Some)
            .map_err(serde::de::Error::custom),
        Err(err) => Err(err),
    }
}

#[derive(Debug, Serialize, Clone, Deserialize)]
pub struct Colour {
    pub r: u8,
    pub g: u8,
    pub b: u8,
}

impl Colour {
    /// Parse a colour from a hex string (e.g., "ff0000" for red)
    ///
    /// # Errors
    ///
    /// Returns an error if the hex string is not exactly 6 characters or contains invalid hex characters
    pub fn from_hex(hex: &str) -> Result<Self, String> {
        if hex.len() != 6 {
            return Err(format!(
                "hex color must be exactly 6 characters, got {}",
                hex.len()
            ));
        }
        let r = u8::from_str_radix(&hex[0..2], 16)
            .map_err(|e| format!("invalid hex format for red channel: {}", e))?;
        let g = u8::from_str_radix(&hex[2..4], 16)
            .map_err(|e| format!("invalid hex format for green channel: {}", e))?;
        let b = u8::from_str_radix(&hex[4..6], 16)
            .map_err(|e| format!("invalid hex format for blue channel: {}", e))?;
        Ok(Self { r, g, b })
    }

    /// Convert the colour to a hex string (e.g., "ff0000" for red)
    pub fn to_hex(&self) -> String {
        format!("{:02x}{:02x}{:02x}", self.r, self.g, self.b)
    }
}

impl Default for Colour {
    fn default() -> Self {
        Colour {
            r: 255,
            g: 255,
            b: 255,
        }
    }
}

impl TryFrom<&str> for Colour {
    type Error = String;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::from_hex(value)
    }
}

impl From<&Colour> for Vec<f64> {
    fn from(val: &Colour) -> Self {
        vec![f64::from(val.r), f64::from(val.g), f64::from(val.b)]
    }
}

impl From<&Colour> for String {
    fn from(val: &Colour) -> Self {
        val.to_hex()
    }
}

#[derive(Debug, Serialize, Clone, Deserialize)]
pub struct DuotoneColours {
    pub shadow: Colour,
    pub highlight: Colour,
}

impl DuotoneColours {
    /// Parse duotone colours from a comma-separated hex string (e.g., "ff0000,00ff00")
    ///
    /// # Errors
    ///
    /// Returns an error if the format is invalid
    pub fn from_hex(value: &str) -> Result<Self, String> {
        let parts: Vec<&str> = value.split(',').collect();
        if parts.len() != 2 {
            return Err("duotone must contain exactly two comma-separated hex colours".to_string());
        }

        let shadow = Colour::from_hex(parts[0].trim())
            .map_err(|e| format!("invalid shadow colour: {}", e))?;
        let highlight = Colour::from_hex(parts[1].trim())
            .map_err(|e| format!("invalid highlight colour: {}", e))?;

        Ok(Self { shadow, highlight })
    }
}

fn deserialize_colour<'de, D>(deserializer: D) -> Result<Option<Colour>, D::Error>
where
    D: Deserializer<'de>,
{
    let result = String::deserialize(deserializer);
    match result {
        Ok(value) => {
            if value.is_empty() {
                return Ok(None);
            }
            Colour::from_hex(&value)
                .map(Some)
                .map_err(serde::de::Error::custom)
        }
        Err(err) => Err(err),
    }
}

fn deserialize_duotone_colours<'de, D>(deserializer: D) -> Result<Option<DuotoneColours>, D::Error>
where
    D: Deserializer<'de>,
{
    let result = String::deserialize(deserializer);
    match result {
        Ok(value) => {
            if value.is_empty() {
                return Ok(None);
            }
            DuotoneColours::from_hex(&value)
                .map(Some)
                .map_err(serde::de::Error::custom)
        }
        Err(err) => Err(err),
    }
}

#[derive(Debug, Display, PartialEq, Serialize, Deserialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum Format {
    #[strum(serialize = "avif")]
    Avif,
    #[strum(serialize = "jpeg")]
    Jpeg,
    #[strum(serialize = "webp")]
    Webp,
    #[strum(serialize = "png")]
    Png,
}

impl Format {
    #[must_use]
    pub fn content_type(self) -> &'static str {
        match self {
            Format::Avif => "image/avif",
            Format::Jpeg => "image/jpeg",
            Format::Webp => "image/webp",
            Format::Png => "image/png",
        }
    }
}

#[derive(Debug, Display, PartialEq, Serialize, Deserialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum Trim {
    /// Automatically trim the image using either a colour or the alpha channel
    #[strum(serialize = "auto")]
    Auto,
    /// Trim the image using a colour
    #[strum(serialize = "colour")]
    Colour,
}

#[derive(Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct AspectRatio {
    pub ratio: f64,
    pub x: i32,
    pub y: i32,
}

impl AspectRatio {
    /// Create a new AspectRatio
    ///
    /// # Errors
    ///
    /// Returns an error if either x or y is zero
    #[inline]
    pub fn new(x: i32, y: i32) -> Result<Self, String> {
        if x == 0 {
            return Err("aspect ratio numerator cannot be zero".to_string());
        }
        if y == 0 {
            return Err("aspect ratio denominator cannot be zero".to_string());
        }
        Ok(AspectRatio::from_dimensions(x, y))
    }

    #[inline]
    pub(crate) fn from_dimensions(x: i32, y: i32) -> Self {
        Self {
            ratio: f64::from(x) / f64::from(y),
            x,
            y,
        }
    }
}

impl Display for AspectRatio {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}:{}", self.x, self.y)
    }
}

impl Div<AspectRatio> for i32 {
    type Output = i32;

    #[allow(clippy::cast_possible_truncation)]
    #[inline]
    fn div(self, aspect_ratio: AspectRatio) -> Self::Output {
        (f64::from(self) / aspect_ratio.ratio).round() as i32
    }
}

impl Mul<AspectRatio> for i32 {
    type Output = i32;

    #[allow(clippy::cast_possible_truncation)]
    #[inline]
    fn mul(self, aspect_ratio: AspectRatio) -> Self::Output {
        (f64::from(self) * aspect_ratio.ratio).round() as i32
    }
}

impl From<&mut ImageOptions> for ops::HeifsaveBufferOptions {
    fn from(options: &mut ImageOptions) -> ops::HeifsaveBufferOptions {
        let mut opts = ops::HeifsaveBufferOptions {
            q: options.quality.unwrap_or(75),
            lossless: options.lossless.unwrap_or(false),
            compression: ops::ForeignHeifCompression::Hevc,
            effort: 4,
            ..Default::default()
        };
        if let Some(Format::Avif) = options.format {
            opts.compression = ops::ForeignHeifCompression::Av1;
            opts.bitdepth = 8;
        }
        opts
    }
}

impl From<&mut ImageOptions> for ops::WebpsaveBufferOptions {
    fn from(options: &mut ImageOptions) -> ops::WebpsaveBufferOptions {
        ops::WebpsaveBufferOptions {
            q: options.quality.unwrap_or(80),
            lossless: options.lossless.unwrap_or(false),
            ..Default::default()
        }
    }
}

impl From<&mut ImageOptions> for ops::JpegsaveBufferOptions {
    fn from(options: &mut ImageOptions) -> ops::JpegsaveBufferOptions {
        ops::JpegsaveBufferOptions {
            q: options.quality.unwrap_or(80),
            optimize_coding: false,
            keep: ops::ForeignKeep::None,
            // Setting interlace to true slows down the encoding process significantly
            interlace: false,
            ..Default::default()
        }
    }
}

impl From<&mut ImageOptions> for ops::PngsaveBufferOptions {
    fn from(options: &mut ImageOptions) -> ops::PngsaveBufferOptions {
        ops::PngsaveBufferOptions {
            q: options.quality.unwrap_or(80),
            compression: 6,
            interlace: true,
            ..Default::default()
        }
    }
}

#[derive(Display, PartialEq, Debug, Deserialize, Serialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum Fit {
    /// Fits within bounds without cropping or distortion, maintaining aspect ratio.
    Clip,

    /// Fills dimensions, cropping excess without distortion.
    Crop,

    /// Fits within bounds without cropping or distortion but won't upscale smaller images.
    Max,
}

fn deserialize_aspect_ratio<'de, D>(deserializer: D) -> Result<Option<AspectRatio>, D::Error>
where
    D: Deserializer<'de>,
{
    let result = String::deserialize(deserializer);
    match result {
        Ok(value) if value.is_empty() => Ok(None),
        Ok(value) => {
            let parts: Vec<&str> = value.split(':').collect();
            if parts.len() == 2
                && let (Ok(numerator), Ok(denominator)) =
                    (parts[0].parse::<i32>(), parts[1].parse::<i32>())
            {
                return AspectRatio::new(numerator, denominator)
                    .map(Some)
                    .map_err(serde::de::Error::custom);
            }
            Err(serde::de::Error::custom("invalid aspect ratio"))
        }
        Err(err) => Err(err),
    }
}

fn deserialize_dimension<'de, D>(deserializer: D) -> Result<Option<i32>, D::Error>
where
    D: Deserializer<'de>,
{
    let result = i32::deserialize(deserializer);
    match result {
        Ok(value) if value > 0 => Ok(Some(value)),
        Ok(_) => Ok(None),
        Err(err) => Err(err),
    }
}

#[allow(clippy::cast_possible_truncation)]
fn calculate_crop_dimensions(
    image_options: &ImageOptions,
    image_width: i32,
    image_height: i32,
    aspect_ratio: Option<AspectRatio>,
) -> (i32, i32) {
    // Use the given `ar` or default to the image's aspect ratio
    let ar =
        aspect_ratio.unwrap_or_else(|| AspectRatio::from_dimensions(image_width, image_height));
    match (image_options.width, image_options.height) {
        // If no constraints are provided, use the original dimensions
        (None, None) => (image_width, image_height),

        // If only width is provided, calculate the height based on aspect ratio
        (Some(width), None) => (width, width / ar),

        // If only height is provided, calculate the width based on aspect ratio
        (None, Some(height)) => ((height * ar), height),

        // If both width and height are specified, use them directly
        (Some(width), Some(height)) => (width, height),
    }
}

#[allow(clippy::cast_possible_truncation)]
fn calculate_clip_dimensions(
    image_options: &ImageOptions,
    image_width: i32,
    image_height: i32,
    aspect_ratio: Option<AspectRatio>,
) -> (i32, i32) {
    let ar =
        aspect_ratio.unwrap_or_else(|| AspectRatio::from_dimensions(image_width, image_height));
    match (image_options.width, image_options.height) {
        // If no constraints are provided, use the original dimensions
        (None, None) => (image_width, image_height),

        // If only width is provided, calculate the height based on aspect ratio
        (Some(width), None) => (width, width / ar),

        // If only height is provided, calculate the width based on aspect ratio
        (None, Some(height)) => (height * ar, height),

        // If both width and height are provided, fit within the bounding box
        (Some(width), Some(height)) => {
            let target_aspect_ratio = AspectRatio::from_dimensions(width, height);
            if target_aspect_ratio > ar {
                (height * ar, height)
            } else {
                (width, width / ar)
            }
        }
    }
}

#[allow(clippy::cast_possible_truncation)]
fn calculate_max_dimensions(
    image_options: &ImageOptions,
    image_width: i32,
    image_height: i32,
    aspect_ratio: Option<AspectRatio>,
) -> (i32, i32) {
    let ar =
        aspect_ratio.unwrap_or_else(|| AspectRatio::from_dimensions(image_width, image_height));
    match (image_options.width, image_options.height) {
        // If no constraints are provided, use the original dimensions
        (None, None) => (image_width, image_height),

        // Constrained by width only, but do not upscale
        (Some(width), None) => {
            let new_width = width.min(image_width);
            let new_height = (new_width / ar).min(image_height);
            (new_width, new_height)
        }

        // Constrained by height only, but do not upscale
        (None, Some(height)) => {
            let new_height = height.min(image_height);
            let new_width = (new_height * ar).min(image_width);
            (new_width, new_height)
        }

        // Constrained by both width and height, but do not upscale
        (Some(width), Some(height)) => {
            let target_aspect_ratio = AspectRatio::from_dimensions(width, height);

            if target_aspect_ratio > ar {
                // Fit by height
                let new_height = height.min(image_height);
                let new_width = (new_height * ar).min(image_width);
                (new_width, new_height)
            } else {
                // Fit by width
                let new_width = width.min(image_width);
                let new_height = (new_width / ar).min(image_height);
                (new_width, new_height)
            }
        }
    }
}

pub fn calculate_dimensions(image_options: &mut ImageOptions, image_width: i32, image_height: i32) {
    let dpr = image_options.device_pixel_ratio.unwrap_or(1);

    let aspect_ratio = match image_options.aspect_ratio.clone() {
        Some(ar) => Some(ar),
        None => Some(AspectRatio::from_dimensions(
            image_width * dpr,
            image_height * dpr,
        )),
    };

    // Determine the new dimensions based on the `fit` parameter
    let (width, height) = match image_options.fit {
        Some(Fit::Crop) => {
            calculate_crop_dimensions(image_options, image_width, image_height, aspect_ratio)
        }
        Some(Fit::Max) => {
            calculate_max_dimensions(image_options, image_width, image_height, aspect_ratio)
        }
        Some(Fit::Clip) | None => {
            calculate_clip_dimensions(image_options, image_width, image_height, aspect_ratio)
        }
    };

    // Apply the Device Pixel Ratio (DPR) scaling
    image_options.width = Some(width * dpr);
    image_options.height = Some(height * dpr);
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::{extract::Query, http::Uri};
    use rstest::rstest;

    #[rstest]
    // No resizing
    #[case::no_resizing("", (600, 400), (600, 400))]
    #[case::no_resizing("?", (600, 400), (600, 400))]
    // Clip: Width only
    #[case::clip_width_only_invalid("?w=0&fit=clip", (600, 400), (600, 400))]
    #[case::clip_width_only("?w=150&fit=clip", (600, 400), (150, 100))]
    #[case::clip_width_only("?w=300&fit=clip", (600, 400), (300, 200))]
    // Clip: Height only
    #[case::clip_height_only_invalid("?h=0&fit=clip", (600, 400), (600, 400))]
    #[case::clip_height_only("?h=100&fit=clip", (600, 400), (150, 100))]
    #[case::clip_height_only("?h=200&fit=clip", (600, 400), (300, 200))]
    // Clip: Width and Height
    #[case::clip_width_and_height_invalid("?w=0&h=0&fit=clip", (600, 400), (600, 400))]
    #[case::clip_width_and_height("?w=300&h=200&fit=clip", (600, 400), (300, 200))]
    #[case::clip_width_and_height("?w=100&h=100&fit=clip", (600, 400), (100, 67))]
    // Crop: Width only
    #[case::crop_width_only_invalid("?w=0&fit=crop", (600, 400), (600, 400))]
    #[case::crop_width_only("?w=150&fit=crop", (600, 400), (150, 100))]
    #[case::crop_width_only("?w=300&fit=crop", (600, 400), (300, 200))]
    // Crop: Height only
    #[case::crop_height_only_invalid("?h=0&fit=crop", (600, 400), (600, 400))]
    #[case::crop_height_only("?h=100&fit=crop", (600, 400), (150, 100))]
    #[case::crop_height_only("?h=200&fit=crop", (600, 400), (300, 200))]
    // Crop: Width and Height
    #[case::crop_width_and_height_invalid("?w=0&h=0&fit=crop", (600, 400), (600, 400))]
    #[case::crop_width_and_height("?w=300&h=200&fit=crop", (600, 400), (300, 200))]
    #[case::crop_width_and_height("?w=100&h=100&fit=crop", (600, 400), (100, 100))]
    // Max: Width only
    #[case::max_width_only_invalid("?w=0&fit=max", (600, 400), (600, 400))]
    #[case::max_width_only("?w=150&fit=max", (600, 400), (150, 100))]
    #[case::max_width_only("?w=300&fit=max", (600, 400), (300, 200))]
    // Max: Height only
    #[case::max_height_only_invalid("?h=0&fit=max", (600, 400), (600, 400))]
    #[case::max_height_only("?h=100&fit=max", (600, 400), (150, 100))]
    #[case::max_height_only("?h=200&fit=max", (600, 400), (300, 200))]
    // Max: Width and Height
    #[case::max_width_and_height_invalid("?w=0&h=0&fit=max", (600, 400), (600, 400))]
    #[case::max_width_and_height("?w=300&h=200&fit=max", (600, 400), (300, 200))]
    #[case::max_width_and_height("?w=100&h=100&fit=max", (600, 400), (100, 67))]
    fn test_calculate_dimensions(
        #[case] query: &str,
        #[case] image_dimensions: (i32, i32),
        #[case] expected: (i32, i32),
    ) {
        let url = String::from("https://google.com/image.jpg") + query;
        let uri: Uri = url.parse().expect("failed to parse url");
        let mut image_options: Query<ImageOptions> =
            Query::try_from_uri(&uri).expect("failed to parse query");
        let (width, height) = image_dimensions;
        calculate_dimensions(&mut image_options, width, height);
        assert_eq!(
            (image_options.width.unwrap(), image_options.height.unwrap()),
            expected
        );
    }

    fn get_image_options() -> ImageOptions {
        ImageOptions {
            width: Some(300),
            height: Some(200),
            quality: Some(80),
            aspect_ratio: Some(AspectRatio::from_dimensions(16, 9)),
            device_pixel_ratio: Some(2),
            fit: Some(Fit::Crop),
            background: Some(Colour { r: 255, g: 0, b: 0 }),
            format: Some(Format::Jpeg),
            download: Some("image.jpg".to_string()),
            trim: Some(Trim::Auto),
            trim_colour: Some(Colour { r: 0, g: 255, b: 0 }),
            sharpen: Some(Percentage(50)),
            kodachrome: Some(Percentage(50)),
            tint: Some(Colour {
                r: 255,
                g: 0,
                b: 255,
            }),
            ..Default::default()
        }
    }

    #[test]
    fn test_query_str_valid() {
        let query_str = get_image_options().query_str();
        assert_eq!(
            query_str,
            "ar=16:9&background=ff0000&download=image.jpg&dpr=2&fit=crop&format=jpeg&height=200&kodachrome=50&quality=80&sharpen=50&tint=ff00ff&trim=auto&trim-colour=00ff00&width=300"
        );
    }

    #[test]
    fn test_sign_valid() {
        let secret = "super_secret_key";
        let signature = get_image_options().sign(secret);
        assert_eq!(
            signature,
            "934868feeed5203a0fde3fc40661c21d1cc1f5fd732c04b0fb9363d25813575f"
        );
    }

    #[test]
    fn test_verify_signature_valid() {
        let secret = "super_secret_key";
        let mut options = get_image_options();
        let signature = options.sign(secret);
        options.signature = Some(signature);
        assert!(options.verify_signature(secret));
    }

    #[test]
    fn test_verify_signature_invalid() {
        let secret = "super_secret_key";
        let mut options = get_image_options();
        options.signature = Some("invalid_signature".to_string());
        assert!(!options.verify_signature(secret));
    }

    #[test]
    fn test_verify_signature_no_signature() {
        let secret = "super_secret_key";
        let options = get_image_options();
        assert!(!options.verify_signature(secret));
    }

    #[test]
    fn test_any_set_with_options() {
        let options = get_image_options();
        assert!(options.any_set());
    }

    #[test]
    fn test_any_set_default() {
        let options = ImageOptions::default();
        assert!(!options.any_set());
    }

    #[test]
    fn test_any_set_single_option_vintage() {
        let options = ImageOptions {
            vintage: Some(Percentage(50)),
            ..Default::default()
        };
        assert!(options.any_set());
    }

    #[test]
    fn test_any_set_single_option_signature() {
        let options = ImageOptions {
            signature: Some("test".to_string()),
            ..Default::default()
        };
        assert!(options.any_set());
    }

    #[test]
    fn test_any_set_single_option_tint() {
        let options = ImageOptions {
            tint: Some(Colour { r: 255, g: 0, b: 0 }),
            ..Default::default()
        };
        assert!(options.any_set());
    }

    #[test]
    fn test_get_resize_scale_width_only() {
        let options = ImageOptions {
            width: Some(300),
            ..Default::default()
        };
        let scale = options.get_resize_scale(600, 400);
        assert_eq!(scale, 0.5);
    }

    #[test]
    fn test_get_resize_scale_height_only() {
        let options = ImageOptions {
            height: Some(200),
            ..Default::default()
        };
        let scale = options.get_resize_scale(600, 400);
        assert_eq!(scale, 0.5);
    }

    #[test]
    fn test_get_resize_scale_width_and_height() {
        let options = ImageOptions {
            width: Some(300),
            height: Some(150),
            ..Default::default()
        };
        let scale = options.get_resize_scale(600, 400);
        assert_eq!(scale, 0.375);
    }

    #[test]
    fn test_get_resize_scale_no_dimensions() {
        let options = ImageOptions::default();
        let scale = options.get_resize_scale(600, 400);
        assert_eq!(scale, 1.0);
    }

    #[test]
    fn test_colour_to_vec() {
        let colour = Colour {
            r: 255,
            g: 128,
            b: 64,
        };
        let vec: Vec<f64> = (&colour).into();
        assert_eq!(vec, vec![255.0, 128.0, 64.0]);
    }

    #[test]
    fn test_colour_to_string() {
        let colour = Colour {
            r: 255,
            g: 128,
            b: 64,
        };
        let string: String = (&colour).into();
        assert_eq!(string, "ff8040");
    }

    #[test]
    fn test_deserialize_colour_valid() {
        let value = "\"ff0000\"";
        let mut str = serde_json::Deserializer::from_str(value);
        let result = deserialize_colour(&mut str);
        assert!(result.is_ok());
        let colour = result.unwrap().unwrap();
        assert_eq!(colour.r, 255);
        assert_eq!(colour.g, 0);
        assert_eq!(colour.b, 0);
    }

    #[test]
    fn test_deserialize_colour_invalid() {
        let value = "\"zzzzzz\"";
        let mut str = serde_json::Deserializer::from_str(value);
        let result = deserialize_colour(&mut str);
        assert!(result.is_err());
    }

    #[test]
    fn test_aspect_ratio_new() {
        let ar = AspectRatio::new(16, 9).unwrap();
        assert_eq!(ar.x, 16);
        assert_eq!(ar.y, 9);
        assert!((ar.ratio - (16.0 / 9.0)).abs() < 0.0001);
    }

    #[test]
    fn test_aspect_ratio_display() {
        let ar = AspectRatio::from_dimensions(16, 9);
        assert_eq!(ar.to_string(), "16:9");
    }

    #[test]
    fn test_aspect_ratio_div() {
        let ar = AspectRatio::from_dimensions(16, 9);
        let result = 320 / ar;
        assert_eq!(result, 180);
    }

    #[test]
    fn test_aspect_ratio_mul() {
        let ar = AspectRatio::from_dimensions(16, 9);
        let result = 180 * ar;
        assert_eq!(result, 320);
    }

    #[test]
    fn test_aspect_ratio_parsing_valid() {
        let value = "\"16:9\"";
        let mut str = serde_json::Deserializer::from_str(value);
        let result = deserialize_aspect_ratio(&mut str);
        assert!(result.is_ok());
        let ar = result.unwrap().unwrap();
        assert_eq!(ar.x, 16);
        assert_eq!(ar.y, 9);
    }

    #[test]
    fn test_aspect_ratio_parsing_invalid() {
        let value = "\"9:0\"";
        let mut str = serde_json::Deserializer::from_str(value);
        let result = deserialize_aspect_ratio(&mut str);
        assert!(result.is_err());
    }

    #[test]
    fn test_percentage_from() {
        let percentage = Percentage(75);
        let value: f64 = (&percentage).into();
        assert_eq!(value, 75.0);
    }

    #[test]
    fn test_percentage_parsing_from() {
        let value = "75";
        let percentage = value.parse::<i32>().unwrap();
        assert_eq!(percentage, 75);
    }

    #[test]
    fn test_deserialize_percentage_valid() {
        let json = "75";
        let result: Result<Option<Percentage>, _> = serde_json::from_str(json);
        assert!(result.is_ok());
        let percentage = result.unwrap().unwrap();
        assert_eq!(percentage.0, 75);
    }

    #[test]
    fn test_deserialize_percentage_out_of_range() {
        let json = "\"150\"";
        let result: Result<Option<Percentage>, _> = serde_json::from_str(json);
        assert!(result.is_err());
    }

    #[test]
    fn test_rotation_from() {
        let rotation = Rotation(90);
        let value: f64 = (&rotation).into();
        assert_eq!(value, 90.0);
    }

    #[test]
    fn test_rotation_valid() {
        for v in [90, 180, 270] {
            let json = v.to_string();
            let mut str = serde_json::Deserializer::from_str(&json);
            let result = deserialize_rotation(&mut str);
            assert!(result.is_ok());
            assert_eq!(result.unwrap(), Some(Rotation(v)));
        }
    }

    #[test]
    fn test_rotation_invalid() {
        for v in [0, 1, 45, 89, 91, 100, 360, -90] {
            let json = v.to_string();
            let mut str = serde_json::Deserializer::from_str(&json);
            let result = deserialize_rotation(&mut str);
            assert!(result.is_err(), "rotation {v} should not be accepted");
        }
    }

    #[test]
    fn test_from_image_options_heif() {
        let mut options = ImageOptions {
            quality: Some(90),
            lossless: Some(true),
            format: Some(Format::Avif),
            ..Default::default()
        };
        let heif_opts: ops::HeifsaveBufferOptions = (&mut options).into();
        assert_eq!(heif_opts.q, 90);
        assert!(heif_opts.lossless);
        assert_eq!(heif_opts.bitdepth, 8);
    }

    #[test]
    fn test_from_image_options_webp() {
        let mut options = ImageOptions {
            quality: Some(90),
            lossless: Some(true),
            ..Default::default()
        };
        let webp_opts: ops::WebpsaveBufferOptions = (&mut options).into();
        assert_eq!(webp_opts.q, 90);
        assert!(webp_opts.lossless);
    }

    #[test]
    fn test_from_image_options_jpeg() {
        let mut options = ImageOptions {
            quality: Some(90),
            ..Default::default()
        };
        let jpeg_opts: ops::JpegsaveBufferOptions = (&mut options).into();
        assert_eq!(jpeg_opts.q, 90);
        assert!(!jpeg_opts.optimize_coding);
        assert!(!jpeg_opts.interlace);
    }

    #[test]
    fn test_from_image_options_png() {
        let mut options = ImageOptions {
            quality: Some(90),
            ..Default::default()
        };
        let png_opts: ops::PngsaveBufferOptions = (&mut options).into();
        assert_eq!(png_opts.q, 90);
        assert_eq!(png_opts.compression, 6);
        assert!(png_opts.interlace);
    }

    #[test]
    fn test_deserialize_duotone_colours_valid() {
        let value = "\"ff0000,00ff00\"";
        let mut str = serde_json::Deserializer::from_str(value);
        let result = deserialize_duotone_colours(&mut str);
        assert!(result.is_ok());
        let duotone = result.unwrap().unwrap();
        assert_eq!(duotone.shadow.r, 255);
        assert_eq!(duotone.shadow.g, 0);
        assert_eq!(duotone.shadow.b, 0);
        assert_eq!(duotone.highlight.r, 0);
        assert_eq!(duotone.highlight.g, 255);
        assert_eq!(duotone.highlight.b, 0);
    }

    #[test]
    fn test_deserialize_duotone_colours_empty_string() {
        let value = "\"\"";
        let mut str = serde_json::Deserializer::from_str(value);
        let result = deserialize_duotone_colours(&mut str);
        assert!(result.is_ok());
        assert!(result.unwrap().is_none());
    }

    #[test]
    fn test_deserialize_duotone_colours_invalid_too_few_colours() {
        let value = "\"ff0000\"";
        let mut str = serde_json::Deserializer::from_str(value);
        let result = deserialize_duotone_colours(&mut str);
        assert!(result.is_err());
    }

    #[test]
    fn test_deserialize_duotone_colours_invalid_too_many_colours() {
        let value = "\"ff0000,00ff00,0000ff\"";
        let mut str = serde_json::Deserializer::from_str(value);
        let result = deserialize_duotone_colours(&mut str);
        assert!(result.is_err());
    }

    #[test]
    fn test_deserialize_duotone_colours_invalid_hex_length() {
        let value = "\"ff000,00ff00\"";
        let mut str = serde_json::Deserializer::from_str(value);
        let result = deserialize_duotone_colours(&mut str);
        assert!(result.is_err());
    }

    #[test]
    fn test_tint_color_support() {
        let options = ImageOptions {
            tint: Some(Colour {
                r: 255,
                g: 0,
                b: 255,
            }),
            ..Default::default()
        };
        assert!(options.any_set());

        let query_str = options.query_str();
        assert!(query_str.contains("tint=ff00ff"));
    }

    #[test]
    fn test_tint_deserialization() {
        use axum::{extract::Query, http::Uri};

        let url = "https://example.com/image.jpg?tint=ff00ff";
        let uri: Uri = url.parse().expect("failed to parse url");
        let options: Query<ImageOptions> =
            Query::try_from_uri(&uri).expect("failed to parse query");

        assert!(options.tint.is_some());
        let tint = options.tint.as_ref().unwrap();
        assert_eq!(tint.r, 255);
        assert_eq!(tint.g, 0);
        assert_eq!(tint.b, 255);
    }
}
