/* 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 http://mozilla.org/MPL/2.0/. */

use api::{BorderRadius, BoxShadowClipMode, ClipMode, ColorF, PrimitiveKeyKind};
use api::PropertyBinding;
use api::units::*;
use crate::clip::{ClipItemKey, ClipItemKeyKind, ClipNodeId};
use crate::scene_building::SceneBuilder;
use crate::spatial_tree::SpatialNodeIndex;
use crate::gpu_types::BoxShadowStretchMode;
use crate::render_task_graph::RenderTaskId;
use crate::internal_types::LayoutPrimitiveInfo;

#[derive(Debug, Clone, MallocSizeOf)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct BoxShadowClipSource {
    // Parameters that define the shadow and are constant.
    pub shadow_radius: BorderRadius,
    pub blur_radius: f32,
    pub clip_mode: BoxShadowClipMode,
    pub stretch_mode_x: BoxShadowStretchMode,
    pub stretch_mode_y: BoxShadowStretchMode,

    // The current cache key (in device-pixels), and handles
    // to the cached clip region and blurred texture.
    pub cache_key: Option<(DeviceIntSize, BoxShadowCacheKey)>,
    pub render_task: Option<RenderTaskId>,

    // Local-space size of the required render task size.
    pub shadow_rect_alloc_size: LayoutSize,

    // Local-space size of the required render task size without any downscaling
    // applied. This is needed to stretch the shadow properly.
    pub original_alloc_size: LayoutSize,

    // The minimal shadow rect for the parameters above,
    // used when drawing the shadow rect to be blurred.
    pub minimal_shadow_rect: LayoutRect,

    // Local space rect for the shadow to be drawn or
    // stretched in the shadow primitive.
    pub prim_shadow_rect: LayoutRect,
}

// The blur shader samples BLUR_SAMPLE_SCALE * blur_radius surrounding texels.
pub const BLUR_SAMPLE_SCALE: f32 = 3.0;

// Maximum blur radius for box-shadows (different than blur filters).
// Taken from nsCSSRendering.cpp in Gecko.
pub const MAX_BLUR_RADIUS: f32 = 300.;

// A cache key that uniquely identifies a minimally sized
// and blurred box-shadow rect that can be stored in the
// texture cache and applied to clip-masks.
#[derive(Debug, Clone, Eq, Hash, MallocSizeOf, PartialEq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct BoxShadowCacheKey {
    pub blur_radius_dp: i32,
    pub clip_mode: BoxShadowClipMode,
    // NOTE(emilio): Only the original allocation size needs to be in the cache
    // key, since the actual size is derived from that.
    pub original_alloc_size: DeviceIntSize,
    pub br_top_left: DeviceIntSize,
    pub br_top_right: DeviceIntSize,
    pub br_bottom_right: DeviceIntSize,
    pub br_bottom_left: DeviceIntSize,
    pub device_pixel_scale: Au,
}

impl<'a> SceneBuilder<'a> {
    pub fn add_box_shadow(
        &mut self,
        spatial_node_index: SpatialNodeIndex,
        clip_node_id: ClipNodeId,
        prim_info: &LayoutPrimitiveInfo,
        box_offset: &LayoutVector2D,
        color: ColorF,
        mut blur_radius: f32,
        spread_radius: f32,
        border_radius: BorderRadius,
        clip_mode: BoxShadowClipMode,
    ) {
        if color.a == 0.0 {
            return;
        }

        // Inset shadows get smaller as spread radius increases.
        let (spread_amount, prim_clip_mode) = match clip_mode {
            BoxShadowClipMode::Outset => (spread_radius, ClipMode::ClipOut),
            BoxShadowClipMode::Inset => (-spread_radius, ClipMode::Clip),
        };

        // Ensure the blur radius is somewhat sensible.
        blur_radius = f32::min(blur_radius, MAX_BLUR_RADIUS);

        // Adjust the border radius of the box shadow per CSS-spec.
        let shadow_radius = adjust_border_radius_for_box_shadow(border_radius, spread_amount);

        // Apply parameters that affect where the shadow rect
        // exists in the local space of the primitive.
        let shadow_rect = self.snap_rect(
            &prim_info
                .rect
                .translate(*box_offset)
                .inflate(spread_amount, spread_amount),
            spatial_node_index,
        );

        // If blur radius is zero, we can use a fast path with
        // no blur applied.
        if blur_radius == 0.0 {
            // Trivial reject of box-shadows that are not visible.
            if box_offset.x == 0.0 && box_offset.y == 0.0 && spread_amount == 0.0 {
                return;
            }

            let mut clips = Vec::with_capacity(2);
            let (final_prim_rect, clip_radius) = match clip_mode {
                BoxShadowClipMode::Outset => {
                    if shadow_rect.is_empty() {
                        return;
                    }

                    // TODO(gw): Add a fast path for ClipOut + zero border radius!
                    clips.push(ClipItemKey {
                        kind: ClipItemKeyKind::rounded_rect(
                            prim_info.rect,
                            border_radius,
                            ClipMode::ClipOut,
                        ),
                        spatial_node_index,
                    });

                    (shadow_rect, shadow_radius)
                }
                BoxShadowClipMode::Inset => {
                    if !shadow_rect.is_empty() {
                        clips.push(ClipItemKey {
                            kind: ClipItemKeyKind::rounded_rect(
                                shadow_rect,
                                shadow_radius,
                                ClipMode::ClipOut,
                            ),
                            spatial_node_index,
                        });
                    }

                    (prim_info.rect, border_radius)
                }
            };

            clips.push(ClipItemKey {
                kind: ClipItemKeyKind::rounded_rect(
                    final_prim_rect,
                    clip_radius,
                    ClipMode::Clip,
                ),
                spatial_node_index,
            });

            self.add_primitive(
                spatial_node_index,
                clip_node_id,
                &LayoutPrimitiveInfo::with_clip_rect(final_prim_rect, prim_info.clip_rect),
                clips,
                PrimitiveKeyKind::Rectangle {
                    color: PropertyBinding::Value(color.into()),
                },
            );
        } else {
            // Normal path for box-shadows with a valid blur radius.
            let blur_offset = (BLUR_SAMPLE_SCALE * blur_radius).ceil();
            let mut extra_clips = vec![];

            // Add a normal clip mask to clip out the contents
            // of the surrounding primitive.
            extra_clips.push(ClipItemKey {
                kind: ClipItemKeyKind::rounded_rect(
                    prim_info.rect,
                    border_radius,
                    prim_clip_mode,
                ),
                spatial_node_index,
            });

            // Get the local rect of where the shadow will be drawn,
            // expanded to include room for the blurred region.
            let dest_rect = shadow_rect.inflate(blur_offset, blur_offset);

            // Draw the box-shadow as a solid rect, using a box-shadow
            // clip mask item.
            let prim = PrimitiveKeyKind::Rectangle {
                color: PropertyBinding::Value(color.into()),
            };

            // Create the box-shadow clip item.
            let shadow_clip_source = ClipItemKey {
                kind: ClipItemKeyKind::box_shadow(
                    shadow_rect,
                    shadow_radius,
                    dest_rect,
                    blur_radius,
                    clip_mode,
                ),
                spatial_node_index,
            };

            let prim_info = match clip_mode {
                BoxShadowClipMode::Outset => {
                    // Certain spread-radii make the shadow invalid.
                    if shadow_rect.is_empty() {
                        return;
                    }

                    // Add the box-shadow clip source.
                    extra_clips.push(shadow_clip_source);

                    // Outset shadows are expanded by the shadow
                    // region from the original primitive.
                    LayoutPrimitiveInfo::with_clip_rect(dest_rect, prim_info.clip_rect)
                }
                BoxShadowClipMode::Inset => {
                    // If the inner shadow rect contains the prim
                    // rect, no pixels will be shadowed.
                    if border_radius.is_zero() && shadow_rect
                        .inflate(-blur_radius, -blur_radius)
                        .contains_box(&prim_info.rect)
                    {
                        return;
                    }

                    // Inset shadows are still visible, even if the
                    // inset shadow rect becomes invalid (they will
                    // just look like a solid rectangle).
                    if !shadow_rect.is_empty() {
                        extra_clips.push(shadow_clip_source);
                    }

                    // Inset shadows draw inside the original primitive.
                    prim_info.clone()
                }
            };

            self.add_primitive(
                spatial_node_index,
                clip_node_id,
                &prim_info,
                extra_clips,
                prim,
            );
        }
    }
}

fn adjust_border_radius_for_box_shadow(radius: BorderRadius, spread_amount: f32) -> BorderRadius {
    BorderRadius {
        top_left: adjust_corner_for_box_shadow(radius.top_left, spread_amount),
        top_right: adjust_corner_for_box_shadow(radius.top_right, spread_amount),
        bottom_right: adjust_corner_for_box_shadow(radius.bottom_right, spread_amount),
        bottom_left: adjust_corner_for_box_shadow(radius.bottom_left, spread_amount),
    }
}

fn adjust_corner_for_box_shadow(corner: LayoutSize, spread_amount: f32) -> LayoutSize {
    LayoutSize::new(
        adjust_radius_for_box_shadow(corner.width, spread_amount),
        adjust_radius_for_box_shadow(corner.height, spread_amount),
    )
}

fn adjust_radius_for_box_shadow(border_radius: f32, spread_amount: f32) -> f32 {
    if border_radius > 0.0 {
        (border_radius + spread_amount).max(0.0)
    } else {
        0.0
    }
}
