/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

//! CSS handling for the computed value of
//! [`image`][image]s
//!
//! [image]: https://drafts.csswg.org/css-images/#image-values

use crate::values::computed::percentage::Percentage;
use crate::values::computed::position::Position;
use crate::values::computed::url::ComputedImageUrl;
#[cfg(feature = "gecko")]
use crate::values::computed::NumberOrPercentage;
use crate::values::computed::{Angle, Color, Context};
use crate::values::computed::{
    AngleOrPercentage, LengthPercentage, NonNegativeLength, NonNegativeLengthPercentage,
    Resolution, ToComputedValue,
};
use crate::values::generics::image::{self as generic, GradientCompatMode};
use crate::values::specified::image as specified;
use crate::values::specified::position::{HorizontalPositionKeyword, VerticalPositionKeyword};
use std::f32::consts::PI;
use std::fmt::{self, Write};
use style_traits::{CssWriter, ToCss};

pub use specified::ImageRendering;

/// Computed values for an image according to CSS-IMAGES.
/// <https://drafts.csswg.org/css-images/#image-values>
pub type Image =
    generic::GenericImage<Gradient, MozImageRect, ComputedImageUrl, Color, Percentage, Resolution>;

// Images should remain small, see https://github.com/servo/servo/pull/18430
size_of_test!(Image, 16);

/// Computed values for a CSS gradient.
/// <https://drafts.csswg.org/css-images/#gradients>
pub type Gradient = generic::GenericGradient<
    LineDirection,
    LengthPercentage,
    NonNegativeLength,
    NonNegativeLengthPercentage,
    Position,
    Angle,
    AngleOrPercentage,
    Color,
>;

/// Computed values for CSS cross-fade
/// <https://drafts.csswg.org/css-images-4/#cross-fade-function>
pub type CrossFade = generic::CrossFade<Image, Color, Percentage>;

/// A computed radial gradient ending shape.
pub type EndingShape = generic::GenericEndingShape<NonNegativeLength, NonNegativeLengthPercentage>;

/// A computed gradient line direction.
#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToResolvedValue)]
#[repr(C, u8)]
pub enum LineDirection {
    /// An angle.
    Angle(Angle),
    /// A horizontal direction.
    Horizontal(HorizontalPositionKeyword),
    /// A vertical direction.
    Vertical(VerticalPositionKeyword),
    /// A corner.
    Corner(HorizontalPositionKeyword, VerticalPositionKeyword),
}

/// The computed value for an `image-set()` image.
pub type ImageSet = generic::GenericImageSet<Image, Resolution>;

impl ToComputedValue for specified::ImageSet {
    type ComputedValue = ImageSet;

    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
        let items = self.items.to_computed_value(context);
        let dpr = context.device().device_pixel_ratio().get();

        let mut supported_image = false;
        let mut selected_index = std::usize::MAX;
        let mut selected_resolution = 0.0;

        for (i, item) in items.iter().enumerate() {
            if item.has_mime_type && !context.device().is_supported_mime_type(&item.mime_type) {
                // If the MIME type is not supported, we discard the ImageSetItem.
                continue;
            }

            let candidate_resolution = item.resolution.dppx();
            debug_assert!(candidate_resolution >= 0.0, "Resolutions should be non-negative");
            if candidate_resolution == 0.0 {
                // If the resolution is 0, we also treat it as an invalid image.
                continue;
            }

            // https://drafts.csswg.org/css-images-4/#image-set-notation:
            //
            //     Make a UA-specific choice of which to load, based on whatever criteria deemed
            //     relevant (such as the resolution of the display, connection speed, etc).
            //
            // For now, select the lowest resolution greater than display density, otherwise the
            // greatest resolution available.
            let better_candidate = || {
                if selected_resolution < dpr && candidate_resolution > selected_resolution {
                    return true;
                }
                if candidate_resolution < selected_resolution && candidate_resolution >= dpr {
                    return true;
                }
                false
            };

            // The first item with a supported MIME type is obviously the current best candidate
            if !supported_image || better_candidate() {
                supported_image = true;
                selected_index = i;
                selected_resolution = candidate_resolution;
            }
        }

        ImageSet {
            selected_index,
            items,
        }
    }

    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
        Self {
            selected_index: std::usize::MAX,
            items: ToComputedValue::from_computed_value(&computed.items),
        }
    }
}

/// Computed values for `-moz-image-rect(...)`.
#[cfg(feature = "gecko")]
pub type MozImageRect = generic::GenericMozImageRect<NumberOrPercentage, ComputedImageUrl>;

/// Empty enum on non-gecko
#[cfg(not(feature = "gecko"))]
pub type MozImageRect = specified::MozImageRect;

impl generic::LineDirection for LineDirection {
    fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool {
        match *self {
            LineDirection::Angle(angle) => angle.radians() == PI,
            LineDirection::Vertical(VerticalPositionKeyword::Bottom) => {
                compat_mode == GradientCompatMode::Modern
            },
            LineDirection::Vertical(VerticalPositionKeyword::Top) => {
                compat_mode != GradientCompatMode::Modern
            },
            _ => false,
        }
    }

    fn to_css<W>(&self, dest: &mut CssWriter<W>, compat_mode: GradientCompatMode) -> fmt::Result
    where
        W: Write,
    {
        match *self {
            LineDirection::Angle(ref angle) => angle.to_css(dest),
            LineDirection::Horizontal(x) => {
                if compat_mode == GradientCompatMode::Modern {
                    dest.write_str("to ")?;
                }
                x.to_css(dest)
            },
            LineDirection::Vertical(y) => {
                if compat_mode == GradientCompatMode::Modern {
                    dest.write_str("to ")?;
                }
                y.to_css(dest)
            },
            LineDirection::Corner(x, y) => {
                if compat_mode == GradientCompatMode::Modern {
                    dest.write_str("to ")?;
                }
                x.to_css(dest)?;
                dest.write_char(' ')?;
                y.to_css(dest)
            },
        }
    }
}

impl ToComputedValue for specified::LineDirection {
    type ComputedValue = LineDirection;

    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
        match *self {
            specified::LineDirection::Angle(ref angle) => {
                LineDirection::Angle(angle.to_computed_value(context))
            },
            specified::LineDirection::Horizontal(x) => LineDirection::Horizontal(x),
            specified::LineDirection::Vertical(y) => LineDirection::Vertical(y),
            specified::LineDirection::Corner(x, y) => LineDirection::Corner(x, y),
        }
    }

    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
        match *computed {
            LineDirection::Angle(ref angle) => {
                specified::LineDirection::Angle(ToComputedValue::from_computed_value(angle))
            },
            LineDirection::Horizontal(x) => specified::LineDirection::Horizontal(x),
            LineDirection::Vertical(y) => specified::LineDirection::Vertical(y),
            LineDirection::Corner(x, y) => specified::LineDirection::Corner(x, y),
        }
    }
}
