
/* 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::{ExternalScrollId, PipelineId, PropertyBinding, PropertyBindingId, ReferenceFrameKind};
use api::{APZScrollGeneration, HasScrollLinkedEffect, SampledScrollOffset};
use api::{TransformStyle, StickyOffsetBounds, SpatialTreeItemKey};
use api::units::*;
use crate::internal_types::PipelineInstanceId;
use crate::spatial_tree::{CoordinateSystem, SpatialNodeIndex, TransformUpdateState};
use crate::spatial_tree::CoordinateSystemId;
use euclid::{Vector2D, SideOffsets2D};
use crate::scene::SceneProperties;
use crate::util::{LayoutFastTransform, MatrixHelpers, ScaleOffset, TransformedRectKind};
use crate::util::{PointHelpers, VectorHelpers};

/// The kind of a spatial node uid. These are required because we currently create external
/// nodes during DL building, but the internal nodes aren't created until scene building.
/// TODO(gw): The internal scroll and reference frames are not used in any important way
//            by Gecko - they were primarily useful for Servo. So we should plan to remove
//            them completely.
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub enum SpatialNodeUidKind {
    /// The root node of the entire spatial tree
    Root,
    /// Internal scroll frame created during scene building for each iframe
    InternalScrollFrame,
    /// Internal reference frame created during scene building for each iframe
    InternalReferenceFrame,
    /// A normal spatial node uid, defined by a caller provided unique key
    External {
        key: SpatialTreeItemKey,
    },
}

/// A unique identifier for a spatial node, that is stable across display lists
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct SpatialNodeUid {
    /// The unique key for a given pipeline for this uid
    pub kind: SpatialNodeUidKind,
    /// Pipeline id to namespace key kinds
    pub pipeline_id: PipelineId,
    /// Instance of this pipeline id
    pub instance_id: PipelineInstanceId,
}

impl SpatialNodeUid {
    pub fn root() -> Self {
        SpatialNodeUid {
            kind: SpatialNodeUidKind::Root,
            pipeline_id: PipelineId::dummy(),
            instance_id: PipelineInstanceId::new(0),
        }
    }

    pub fn root_scroll_frame(
        pipeline_id: PipelineId,
        instance_id: PipelineInstanceId,
    ) -> Self {
        SpatialNodeUid {
            kind: SpatialNodeUidKind::InternalScrollFrame,
            pipeline_id,
            instance_id,
        }
    }

    pub fn root_reference_frame(
        pipeline_id: PipelineId,
        instance_id: PipelineInstanceId,
    ) -> Self {
        SpatialNodeUid {
            kind: SpatialNodeUidKind::InternalReferenceFrame,
            pipeline_id,
            instance_id,
        }
    }

    pub fn external(
        key: SpatialTreeItemKey,
        pipeline_id: PipelineId,
        instance_id: PipelineInstanceId,
    ) -> Self {
        SpatialNodeUid {
            kind: SpatialNodeUidKind::External {
                key,
            },
            pipeline_id,
            instance_id,
        }
    }
}

/// Defines the content of a spatial node. If the values in the descriptor don't
/// change, that means the rest of the fields in a spatial node will end up with
/// the same result
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct SpatialNodeDescriptor {
    /// The type of this node and any data associated with that node type.
    pub node_type: SpatialNodeType,

    /// Pipeline that this layer belongs to
    pub pipeline_id: PipelineId,
}

#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub enum SpatialNodeType {
    /// A special kind of node that adjusts its position based on the position
    /// of its parent node and a given set of sticky positioning offset bounds.
    /// Sticky positioned is described in the CSS Positioned Layout Module Level 3 here:
    /// https://www.w3.org/TR/css-position-3/#sticky-pos
    StickyFrame(StickyFrameInfo),

    /// Transforms it's content, but doesn't clip it. Can also be adjusted
    /// by scroll events or setting scroll offsets.
    ScrollFrame(ScrollFrameInfo),

    /// A reference frame establishes a new coordinate space in the tree.
    ReferenceFrame(ReferenceFrameInfo),
}

/// Information about a spatial node that can be queried during either scene of
/// frame building.
pub struct SpatialNodeInfo<'a> {
    /// The type of this node and any data associated with that node type.
    pub node_type: &'a SpatialNodeType,

    /// Parent spatial node. If this is None, we are the root node.
    pub parent: Option<SpatialNodeIndex>,

    /// Snapping scale/offset relative to the coordinate system. If None, then
    /// we should not snap entities bound to this spatial node.
    pub snapping_transform: Option<ScaleOffset>,
}

/// Scene building specific representation of a spatial node, which is a much
/// lighter subset of a full spatial node constructed and used for frame building
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
#[derive(PartialEq)]
pub struct SceneSpatialNode {
    /// Snapping scale/offset relative to the coordinate system. If None, then
    /// we should not snap entities bound to this spatial node.
    pub snapping_transform: Option<ScaleOffset>,

    /// Parent spatial node. If this is None, we are the root node.
    pub parent: Option<SpatialNodeIndex>,

    /// Descriptor describing how this spatial node behaves
    pub descriptor: SpatialNodeDescriptor,

    /// If true, this spatial node is known to exist in the root coordinate
    /// system in all cases (it has no animated or complex transforms)
    pub is_root_coord_system: bool,
}

impl SceneSpatialNode {
    pub fn new_reference_frame(
        parent_index: Option<SpatialNodeIndex>,
        transform_style: TransformStyle,
        source_transform: PropertyBinding<LayoutTransform>,
        kind: ReferenceFrameKind,
        origin_in_parent_reference_frame: LayoutVector2D,
        pipeline_id: PipelineId,
        is_root_coord_system: bool,
        is_pipeline_root: bool,
    ) -> Self {
        let info = ReferenceFrameInfo {
            transform_style,
            source_transform,
            kind,
            origin_in_parent_reference_frame,
            is_pipeline_root,
        };
        Self::new(
            pipeline_id,
            parent_index,
            SpatialNodeType::ReferenceFrame(info),
            is_root_coord_system,
        )
    }

    pub fn new_scroll_frame(
        pipeline_id: PipelineId,
        parent_index: SpatialNodeIndex,
        external_id: ExternalScrollId,
        frame_rect: &LayoutRect,
        content_size: &LayoutSize,
        frame_kind: ScrollFrameKind,
        external_scroll_offset: LayoutVector2D,
        offset_generation: APZScrollGeneration,
        has_scroll_linked_effect: HasScrollLinkedEffect,
        is_root_coord_system: bool,
    ) -> Self {
        let node_type = SpatialNodeType::ScrollFrame(ScrollFrameInfo::new(
                *frame_rect,
                LayoutSize::new(
                    (content_size.width - frame_rect.width()).max(0.0),
                    (content_size.height - frame_rect.height()).max(0.0)
                ),
                external_id,
                frame_kind,
                external_scroll_offset,
                offset_generation,
                has_scroll_linked_effect,
            )
        );

        Self::new(
            pipeline_id,
            Some(parent_index),
            node_type,
            is_root_coord_system,
        )
    }

    pub fn new_sticky_frame(
        parent_index: SpatialNodeIndex,
        sticky_frame_info: StickyFrameInfo,
        pipeline_id: PipelineId,
        is_root_coord_system: bool,
    ) -> Self {
        Self::new(
            pipeline_id,
            Some(parent_index),
            SpatialNodeType::StickyFrame(sticky_frame_info),
            is_root_coord_system,
        )
    }

    fn new(
        pipeline_id: PipelineId,
        parent_index: Option<SpatialNodeIndex>,
        node_type: SpatialNodeType,
        is_root_coord_system: bool,
    ) -> Self {
        SceneSpatialNode {
            parent: parent_index,
            descriptor: SpatialNodeDescriptor {
                pipeline_id,
                node_type,
            },
            snapping_transform: None,
            is_root_coord_system,
        }
    }
}

/// Contains information common among all types of SpatialTree nodes.
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct SpatialNode {
    /// The scale/offset of the viewport for this spatial node, relative to the
    /// coordinate system. Includes any accumulated scrolling offsets from nodes
    /// between our reference frame and this node.
    pub viewport_transform: ScaleOffset,

    /// Content scale/offset relative to the coordinate system.
    pub content_transform: ScaleOffset,

    /// Snapping scale/offset relative to the coordinate system. If None, then
    /// we should not snap entities bound to this spatial node.
    pub snapping_transform: Option<ScaleOffset>,

    /// The axis-aligned coordinate system id of this node.
    pub coordinate_system_id: CoordinateSystemId,

    /// The current transform kind of this node.
    pub transform_kind: TransformedRectKind,

    /// Pipeline that this layer belongs to
    pub pipeline_id: PipelineId,

    /// Parent layer. If this is None, we are the root node.
    pub parent: Option<SpatialNodeIndex>,

    /// Child layers
    pub children: Vec<SpatialNodeIndex>,

    /// The type of this node and any data associated with that node type.
    pub node_type: SpatialNodeType,

    /// True if this node is transformed by an invertible transform.  If not, display items
    /// transformed by this node will not be displayed and display items not transformed by this
    /// node will not be clipped by clips that are transformed by this node.
    pub invertible: bool,

    /// Whether this specific node is currently being async zoomed.
    /// Should be set when a SetIsTransformAsyncZooming FrameMsg is received.
    pub is_async_zooming: bool,

    /// Whether this node or any of its ancestors is being pinch zoomed.
    /// This is calculated in update(). This will be used to decide whether
    /// to override corresponding picture's raster space as an optimisation.
    pub is_ancestor_or_self_zooming: bool,
}

/// Snap an offset to be incorporated into a transform, where the local space
/// may be considered the world space. We assume raster scale is 1.0, which
/// may not always be correct if there are intermediate surfaces used, however
/// those are either cases where snapping is not important (e.g. has perspective
/// or is not axis aligned), or an edge case (e.g. SVG filters) which we can accept
/// imperfection for now.
fn snap_offset<OffsetUnits, ScaleUnits>(
    offset: Vector2D<f32, OffsetUnits>,
    scale: Vector2D<f32, ScaleUnits>,
) -> Vector2D<f32, OffsetUnits> {
    let world_offset = WorldPoint::new(offset.x * scale.x, offset.y * scale.y);
    let snapped_world_offset = world_offset.snap();
    Vector2D::new(
        if scale.x != 0.0 { snapped_world_offset.x / scale.x } else { offset.x },
        if scale.y != 0.0 { snapped_world_offset.y / scale.y } else { offset.y },
    )
}

impl SpatialNode {
    pub fn add_child(&mut self, child: SpatialNodeIndex) {
        self.children.push(child);
    }

    pub fn set_scroll_offsets(&mut self, mut offsets: Vec<SampledScrollOffset>) -> bool {
        debug_assert!(offsets.len() > 0);

        let scrolling = match self.node_type {
            SpatialNodeType::ScrollFrame(ref mut scrolling) => scrolling,
            _ => {
                warn!("Tried to scroll a non-scroll node.");
                return false;
            }
        };

        for element in offsets.iter_mut() {
            element.offset = -element.offset - scrolling.external_scroll_offset;

            // Once the final scroll offset (APZ + content external offset) is
            // calculated, we need to snap it to a device pixel. We already snap
            // the final transforms in `update_transform`. However, we need to
            // ensure the offsets are also snapped so that if the offset is used
            // in a nested sticky frame, it is pre-snapped.
            element.offset = element.offset.snap();
        }

        if scrolling.offsets == offsets {
            return false;
        }

        scrolling.offsets = offsets;
        true
    }

    pub fn mark_uninvertible(
        &mut self,
        state: &TransformUpdateState,
    ) {
        self.invertible = false;
        self.viewport_transform = ScaleOffset::identity();
        self.content_transform = ScaleOffset::identity();
        self.coordinate_system_id = state.current_coordinate_system_id;
    }

    pub fn update(
        &mut self,
        state_stack: &[TransformUpdateState],
        coord_systems: &mut Vec<CoordinateSystem>,
        scene_properties: &SceneProperties,
    ) {
        let state = state_stack.last().unwrap();

        self.is_ancestor_or_self_zooming = self.is_async_zooming | state.is_ancestor_or_self_zooming;

        // If any of our parents was not rendered, we are not rendered either and can just
        // quit here.
        if !state.invertible {
            self.mark_uninvertible(state);
            return;
        }

        self.update_transform(
            state_stack,
            coord_systems,
            scene_properties,
        );

        if !self.invertible {
            self.mark_uninvertible(state);
        }
    }

    pub fn update_transform(
        &mut self,
        state_stack: &[TransformUpdateState],
        coord_systems: &mut Vec<CoordinateSystem>,
        scene_properties: &SceneProperties,
    ) {
        let state = state_stack.last().unwrap();

        // Start by assuming we're invertible
        self.invertible = true;

        match self.node_type {
            SpatialNodeType::ReferenceFrame(ref mut info) => {
                let mut cs_scale_offset = ScaleOffset::identity();
                let mut coordinate_system_id = state.current_coordinate_system_id;

                // Resolve the transform against any property bindings.
                let source_transform = {
                    let source_transform = scene_properties.resolve_layout_transform(&info.source_transform);
                    if let ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } = info.kind {
                        assert!(source_transform.is_2d_scale_translation(), "Reference frame was marked as only having 2d scale or translation");
                    }

                    LayoutFastTransform::from(source_transform)
                };

                // Do a change-basis operation on the perspective matrix using
                // the scroll offset.
                let source_transform = match info.kind {
                    ReferenceFrameKind::Perspective { scrolling_relative_to: Some(external_id) } => {
                        let mut scroll_offset = LayoutVector2D::zero();

                        for parent_state in state_stack.iter().rev() {
                            if let Some(parent_external_id) = parent_state.external_id {
                                if parent_external_id == external_id {
                                    break;
                                }
                            }

                            scroll_offset += parent_state.scroll_offset;
                        }

                        // Do a change-basis operation on the
                        // perspective matrix using the scroll offset.
                        source_transform
                            .pre_translate(scroll_offset)
                            .then_translate(-scroll_offset)
                    }
                    ReferenceFrameKind::Perspective { scrolling_relative_to: None } |
                    ReferenceFrameKind::Transform { .. } => source_transform,
                };

                // Previously, the origin of a stacking context transform was snapped
                // in Gecko. However, this causes jittering issues during scrolling in
                // some cases when fractional scrolling is enabled. The origin used in
                // Gecko doesn't have the external scroll offset from the content process
                // removed, so if that content-side scroll amount is fractional, it can
                // cause inconsistent snapping during scene building. Instead, we need
                // to apply the device-pixel snap _after_ the external scroll offset
                // has been removed. To further complicate matters, we _don't_ want to
                // snap this if this spatial node has a snapping transform, as we rely
                // on the fractional intermediate nodes in order to arrive at a correct
                // final snapping result. If we don't have a snapping offset, we've
                // reached a spatial node where snapping will no longer apply (e.g. a
                // complex transform) and then we need to snap the device pixel position
                // of that transform.
                let parent_origin = match self.snapping_transform {
                    Some(..) => {
                        info.origin_in_parent_reference_frame
                    }
                    None => {
                        snap_offset(
                            info.origin_in_parent_reference_frame,
                            state.coordinate_system_relative_scale_offset.scale,
                        )
                    }
                };

                let resolved_transform =
                    LayoutFastTransform::with_vector(parent_origin)
                        .pre_transform(&source_transform);

                // The transformation for this viewport in world coordinates is the transformation for
                // our parent reference frame, plus any accumulated scrolling offsets from nodes
                // between our reference frame and this node. Finally, we also include
                // whatever local transformation this reference frame provides.
                let relative_transform = resolved_transform
                    .then_translate(snap_offset(state.parent_accumulated_scroll_offset, state.coordinate_system_relative_scale_offset.scale))
                    .to_transform()
                    .with_destination::<LayoutPixel>();

                let mut reset_cs_id = match info.transform_style {
                    TransformStyle::Preserve3D => !state.preserves_3d,
                    TransformStyle::Flat => state.preserves_3d,
                };

                // We reset the coordinate system upon either crossing the preserve-3d context boundary,
                // or simply a 3D transformation.
                if !reset_cs_id {
                    // Try to update our compatible coordinate system transform. If we cannot, start a new
                    // incompatible coordinate system.
                    match ScaleOffset::from_transform(&relative_transform) {
                        Some(ref scale_offset) => {
                            // We generally do not want to snap animated transforms as it causes jitter.
                            // However, we do want to snap the visual viewport offset when scrolling.
                            // This may still cause jitter when zooming, unfortunately.
                            let mut maybe_snapped = scale_offset.clone();
                            if let ReferenceFrameKind::Transform { should_snap: true, .. } = info.kind {
                                maybe_snapped.offset = snap_offset(
                                    scale_offset.offset,
                                    state.coordinate_system_relative_scale_offset.scale,
                                );
                            }
                            cs_scale_offset = maybe_snapped.then(&state.coordinate_system_relative_scale_offset);
                        }
                        None => reset_cs_id = true,
                    }
                }
                if reset_cs_id {
                    // If we break 2D axis alignment or have a perspective component, we need to start a
                    // new incompatible coordinate system with which we cannot share clips without masking.
                    let transform = relative_transform.then(
                        &state.coordinate_system_relative_scale_offset.to_transform()
                    );

                    // Push that new coordinate system and record the new id.
                    let coord_system = {
                        let parent_system = &coord_systems[state.current_coordinate_system_id.0 as usize];
                        let mut cur_transform = transform;
                        if parent_system.should_flatten {
                            cur_transform.flatten_z_output();
                        }
                        let world_transform = cur_transform.then(&parent_system.world_transform);
                        let determinant = world_transform.determinant();
                        self.invertible = determinant != 0.0 && !determinant.is_nan();

                        CoordinateSystem {
                            transform,
                            world_transform,
                            should_flatten: match (info.transform_style, info.kind) {
                                (TransformStyle::Flat, ReferenceFrameKind::Transform { .. }) => true,
                                (_, _) => false,
                            },
                            parent: Some(state.current_coordinate_system_id),
                        }
                    };
                    coordinate_system_id = CoordinateSystemId(coord_systems.len() as u32);
                    coord_systems.push(coord_system);
                }

                // Ensure that the current coordinate system ID is propagated to child
                // nodes, even if we encounter a node that is not invertible. This ensures
                // that the invariant in get_relative_transform is not violated.
                self.coordinate_system_id = coordinate_system_id;
                self.viewport_transform = cs_scale_offset;
                self.content_transform = cs_scale_offset;
            }
            SpatialNodeType::StickyFrame(ref mut info) => {
                let animated_offset = if let Some(transform_binding) = info.transform {
                  let transform = scene_properties.resolve_layout_transform(&transform_binding);
                  match ScaleOffset::from_transform(&transform) {
                    Some(ref scale_offset) => {
                      debug_assert!(scale_offset.scale == Vector2D::new(1.0, 1.0),
                                    "Can only animate a translation on sticky elements");
                      LayoutVector2D::from_untyped(scale_offset.offset)
                    }
                    None => {
                      debug_assert!(false, "Can only animate a translation on sticky elements");
                      LayoutVector2D::zero()
                    }
                  }
                } else {
                  LayoutVector2D::zero()
                };

                let sticky_offset = Self::calculate_sticky_offset(
                    &state.nearest_scrolling_ancestor_offset,
                    &state.nearest_scrolling_ancestor_viewport,
                    info,
                );

                // The transformation for the bounds of our viewport is the parent reference frame
                // transform, plus any accumulated scroll offset from our parents, plus any offset
                // provided by our own sticky positioning.
                let accumulated_offset = state.parent_accumulated_scroll_offset + sticky_offset + animated_offset;
                self.viewport_transform = state.coordinate_system_relative_scale_offset
                    .pre_offset(snap_offset(accumulated_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());
                self.content_transform = self.viewport_transform;

                info.current_offset = sticky_offset + animated_offset;

                self.coordinate_system_id = state.current_coordinate_system_id;
            }
            SpatialNodeType::ScrollFrame(_) => {
                // The transformation for the bounds of our viewport is the parent reference frame
                // transform, plus any accumulated scroll offset from our parents.
                let accumulated_offset = state.parent_accumulated_scroll_offset;
                self.viewport_transform = state.coordinate_system_relative_scale_offset
                    .pre_offset(snap_offset(accumulated_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());

                // The transformation for any content inside of us is the viewport transformation, plus
                // whatever scrolling offset we supply as well.
                let added_offset = accumulated_offset + self.scroll_offset();
                self.content_transform = state.coordinate_system_relative_scale_offset
                    .pre_offset(snap_offset(added_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());

                self.coordinate_system_id = state.current_coordinate_system_id;
          }
        }

        //TODO: remove the field entirely?
        self.transform_kind = if self.coordinate_system_id.0 == 0 {
            TransformedRectKind::AxisAligned
        } else {
            TransformedRectKind::Complex
        };
    }

    fn calculate_sticky_offset(
        viewport_scroll_offset: &LayoutVector2D,
        viewport_rect: &LayoutRect,
        info: &StickyFrameInfo
    ) -> LayoutVector2D {
        if info.margins.top.is_none() && info.margins.bottom.is_none() &&
            info.margins.left.is_none() && info.margins.right.is_none() {
            return LayoutVector2D::zero();
        }

        // The viewport and margins of the item establishes the maximum amount that it can
        // be offset in order to keep it on screen. Since we care about the relationship
        // between the scrolled content and unscrolled viewport we adjust the viewport's
        // position by the scroll offset in order to work with their relative positions on the
        // page.
        let mut sticky_rect = info.frame_rect.translate(*viewport_scroll_offset);

        let mut sticky_offset = LayoutVector2D::zero();
        if let Some(margin) = info.margins.top {
            let top_viewport_edge = viewport_rect.min.y + margin;
            if sticky_rect.min.y < top_viewport_edge {
                // If the sticky rect is positioned above the top edge of the viewport (plus margin)
                // we move it down so that it is fully inside the viewport.
                sticky_offset.y = top_viewport_edge - sticky_rect.min.y;
            } else if info.previously_applied_offset.y > 0.0 &&
                sticky_rect.min.y > top_viewport_edge {
                // However, if the sticky rect is positioned *below* the top edge of the viewport
                // and there is already some offset applied to the sticky rect's position, then
                // we need to move it up so that it remains at the correct position. This
                // makes sticky_offset.y negative and effectively reduces the amount of the
                // offset that was already applied. We limit the reduction so that it can, at most,
                // cancel out the already-applied offset, but should never end up adjusting the
                // position the other way.
                sticky_offset.y = top_viewport_edge - sticky_rect.min.y;
                sticky_offset.y = sticky_offset.y.max(-info.previously_applied_offset.y);
            }
        }

        // If we don't have a sticky-top offset (sticky_offset.y + info.previously_applied_offset.y
        // == 0), or if we have a previously-applied bottom offset (previously_applied_offset.y < 0)
        // then we check for handling the bottom margin case. Note that the "don't have a sticky-top
        // offset" case includes the case where we *had* a sticky-top offset but we reduced it to
        // zero in the above block.
        if sticky_offset.y + info.previously_applied_offset.y <= 0.0 {
            if let Some(margin) = info.margins.bottom {
                // If sticky_offset.y is nonzero that means we must have set it
                // in the sticky-top handling code above, so this item must have
                // both top and bottom sticky margins. We adjust the item's rect
                // by the top-sticky offset, and then combine any offset from
                // the bottom-sticky calculation into sticky_offset below.
                sticky_rect.min.y += sticky_offset.y;
                sticky_rect.max.y += sticky_offset.y;

                // Same as the above case, but inverted for bottom-sticky items. Here
                // we adjust items upwards, resulting in a negative sticky_offset.y,
                // or reduce the already-present upward adjustment, resulting in a positive
                // sticky_offset.y.
                let bottom_viewport_edge = viewport_rect.max.y - margin;
                if sticky_rect.max.y > bottom_viewport_edge {
                    sticky_offset.y += bottom_viewport_edge - sticky_rect.max.y;
                } else if info.previously_applied_offset.y < 0.0 &&
                    sticky_rect.max.y < bottom_viewport_edge {
                    sticky_offset.y += bottom_viewport_edge - sticky_rect.max.y;
                    sticky_offset.y = sticky_offset.y.min(-info.previously_applied_offset.y);
                }
            }
        }

        // Same as above, but for the x-axis.
        if let Some(margin) = info.margins.left {
            let left_viewport_edge = viewport_rect.min.x + margin;
            if sticky_rect.min.x < left_viewport_edge {
                sticky_offset.x = left_viewport_edge - sticky_rect.min.x;
            } else if info.previously_applied_offset.x > 0.0 &&
                sticky_rect.min.x > left_viewport_edge {
                sticky_offset.x = left_viewport_edge - sticky_rect.min.x;
                sticky_offset.x = sticky_offset.x.max(-info.previously_applied_offset.x);
            }
        }

        if sticky_offset.x + info.previously_applied_offset.x <= 0.0 {
            if let Some(margin) = info.margins.right {
                sticky_rect.min.x += sticky_offset.x;
                sticky_rect.max.x += sticky_offset.x;
                let right_viewport_edge = viewport_rect.max.x - margin;
                if sticky_rect.max.x > right_viewport_edge {
                    sticky_offset.x += right_viewport_edge - sticky_rect.max.x;
                } else if info.previously_applied_offset.x < 0.0 &&
                    sticky_rect.max.x < right_viewport_edge {
                    sticky_offset.x += right_viewport_edge - sticky_rect.max.x;
                    sticky_offset.x = sticky_offset.x.min(-info.previously_applied_offset.x);
                }
            }
        }

        // The total "sticky offset" (which is the sum that was already applied by
        // the calling code, stored in info.previously_applied_offset, and the extra amount we
        // computed as a result of scrolling, stored in sticky_offset) needs to be
        // clamped to the provided bounds.
        let clamp_adjusted = |value: f32, adjust: f32, bounds: &StickyOffsetBounds| {
            (value + adjust).max(bounds.min).min(bounds.max) - adjust
        };
        sticky_offset.y = clamp_adjusted(sticky_offset.y,
                                         info.previously_applied_offset.y,
                                         &info.vertical_offset_bounds);
        sticky_offset.x = clamp_adjusted(sticky_offset.x,
                                         info.previously_applied_offset.x,
                                         &info.horizontal_offset_bounds);

        // Reapply the content-process side sticky offset, which was removed
        // from the primitive bounds attached to this node, so that interning
        // sees stable values.
        sticky_offset + info.previously_applied_offset
    }

    pub fn prepare_state_for_children(&self, state: &mut TransformUpdateState) {
        state.current_coordinate_system_id = self.coordinate_system_id;
        state.is_ancestor_or_self_zooming = self.is_ancestor_or_self_zooming;
        state.invertible &= self.invertible;

        // The transformation we are passing is the transformation of the parent
        // reference frame and the offset is the accumulated offset of all the nodes
        // between us and the parent reference frame. If we are a reference frame,
        // we need to reset both these values.
        match self.node_type {
            SpatialNodeType::StickyFrame(ref info) => {
                // We don't translate the combined rect by the sticky offset, because sticky
                // offsets actually adjust the node position itself, whereas scroll offsets
                // only apply to contents inside the node.
                state.parent_accumulated_scroll_offset += info.current_offset;
                // We want nested sticky items to take into account the shift
                // we applied as well.
                state.nearest_scrolling_ancestor_offset += info.current_offset;
                state.preserves_3d = false;
                state.external_id = None;
                state.scroll_offset = info.current_offset;
            }
            SpatialNodeType::ScrollFrame(ref scrolling) => {
                state.parent_accumulated_scroll_offset += scrolling.offset();
                state.nearest_scrolling_ancestor_offset = scrolling.offset();
                state.nearest_scrolling_ancestor_viewport = scrolling.viewport_rect;
                state.preserves_3d = false;
                state.external_id = Some(scrolling.external_id);
                state.scroll_offset = scrolling.offset() + scrolling.external_scroll_offset;
            }
            SpatialNodeType::ReferenceFrame(ref info) => {
                state.external_id = None;
                state.scroll_offset = LayoutVector2D::zero();
                state.preserves_3d = info.transform_style == TransformStyle::Preserve3D;
                state.parent_accumulated_scroll_offset = LayoutVector2D::zero();
                state.coordinate_system_relative_scale_offset = self.content_transform;
                let translation = -info.origin_in_parent_reference_frame;
                state.nearest_scrolling_ancestor_viewport =
                    state.nearest_scrolling_ancestor_viewport
                       .translate(translation);
            }
        }
    }

    pub fn scroll_offset(&self) -> LayoutVector2D {
        match self.node_type {
            SpatialNodeType::ScrollFrame(ref scrolling) => scrolling.offset(),
            _ => LayoutVector2D::zero(),
        }
    }

    pub fn matches_external_id(&self, external_id: ExternalScrollId) -> bool {
        match self.node_type {
            SpatialNodeType::ScrollFrame(ref info) if info.external_id == external_id => true,
            _ => false,
        }
    }

    /// Returns true for ReferenceFrames whose source_transform is
    /// bound to the property binding id.
    pub fn is_transform_bound_to_property(&self, id: PropertyBindingId) -> bool {
        if let SpatialNodeType::ReferenceFrame(ref info) = self.node_type {
            if let PropertyBinding::Binding(key, _) = info.source_transform {
                id == key.id
            } else {
                false
            }
        } else {
            false
        }
    }
}

/// Defines whether we have an implicit scroll frame for a pipeline root,
/// or an explicitly defined scroll frame from the display list.
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub enum ScrollFrameKind {
    PipelineRoot {
        is_root_pipeline: bool,
    },
    Explicit,
}

#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct ScrollFrameInfo {
    /// The rectangle of the viewport of this scroll frame. This is important for
    /// positioning of items inside child StickyFrames.
    pub viewport_rect: LayoutRect,

    /// Amount that this ScrollFrame can scroll in both directions.
    pub scrollable_size: LayoutSize,

    /// An external id to identify this scroll frame to API clients. This
    /// allows setting scroll positions via the API without relying on ClipsIds
    /// which may change between frames.
    pub external_id: ExternalScrollId,

    /// Stores whether this is a scroll frame added implicitly by WR when adding
    /// a pipeline (either the root or an iframe). We need to exclude these
    /// when searching for scroll roots we care about for picture caching.
    /// TODO(gw): I think we can actually completely remove the implicit
    ///           scroll frame being added by WR, and rely on the embedder
    ///           to define scroll frames. However, that involves API changes
    ///           so we will use this as a temporary hack!
    pub frame_kind: ScrollFrameKind,

    /// Amount that visual components attached to this scroll node have been
    /// pre-scrolled in their local coordinates.
    pub external_scroll_offset: LayoutVector2D,

    /// A set of a pair of negated scroll offset and scroll generation of this
    /// scroll node. The negated scroll offset is including the pre-scrolled
    /// amount. If, for example, a scroll node was pre-scrolled to y=10 (10
    /// pixels down from the initial unscrolled position), then
    /// `external_scroll_offset` would be (0,10), and this `offset` field would
    /// be (0,-10). If WebRender is then asked to change the scroll position by
    /// an additional 10 pixels (without changing the pre-scroll amount in the
    /// display list), `external_scroll_offset` would remain at (0,10) and
    /// `offset` would change to (0,-20).
    pub offsets: Vec<SampledScrollOffset>,

    /// The generation of the external_scroll_offset.
    /// This is used to pick up the most appropriate scroll offset sampled
    /// off the main thread.
    pub offset_generation: APZScrollGeneration,

    /// Whether the document containing this scroll frame has any scroll-linked
    /// effect or not.
    pub has_scroll_linked_effect: HasScrollLinkedEffect,
}

/// Manages scrolling offset.
impl ScrollFrameInfo {
    pub fn new(
        viewport_rect: LayoutRect,
        scrollable_size: LayoutSize,
        external_id: ExternalScrollId,
        frame_kind: ScrollFrameKind,
        external_scroll_offset: LayoutVector2D,
        offset_generation: APZScrollGeneration,
        has_scroll_linked_effect: HasScrollLinkedEffect,
    ) -> ScrollFrameInfo {
        ScrollFrameInfo {
            viewport_rect,
            scrollable_size,
            external_id,
            frame_kind,
            external_scroll_offset,
            offsets: vec![SampledScrollOffset{
                // If this scroll frame is a newly created one, using
                // `external_scroll_offset` and `offset_generation` is correct.
                // If this scroll frame is a result of updating an existing
                // scroll frame and if there have already been sampled async
                // scroll offsets by APZ, then these offsets will be replaced in
                // SpatialTree::set_scroll_offsets via a
                // RenderBackend::update_document call.
                offset: -external_scroll_offset,
                generation: offset_generation.clone(),
            }],
            offset_generation,
            has_scroll_linked_effect,
        }
    }

    pub fn offset(&self) -> LayoutVector2D {
        debug_assert!(self.offsets.len() > 0, "There should be at least one sampled offset!");

        if self.has_scroll_linked_effect == HasScrollLinkedEffect::No {
            // If there's no scroll-linked effect, use the one-frame delay offset.
            return self.offsets.first().map_or(LayoutVector2D::zero(), |sampled| sampled.offset);
        }

        match self.offsets.iter().find(|sampled| sampled.generation == self.offset_generation) {
            // If we found an offset having the same generation, use it.
            Some(sampled) => sampled.offset,
            // If we don't have any offset having the same generation, i.e.
            // the generation of this scroll frame is behind sampled offsets,
            // use the first queued sampled offset.
            _ => self.offsets.first().map_or(LayoutVector2D::zero(), |sampled| sampled.offset),
        }
    }
}

/// Contains information about reference frames.
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct ReferenceFrameInfo {
    /// The source transform and perspective matrices provided by the stacking context
    /// that forms this reference frame. We maintain the property binding information
    /// here so that we can resolve the animated transform and update the tree each
    /// frame.
    pub source_transform: PropertyBinding<LayoutTransform>,
    pub transform_style: TransformStyle,
    pub kind: ReferenceFrameKind,

    /// The original, not including the transform and relative to the parent reference frame,
    /// origin of this reference frame. This is already rolled into the `transform' property, but
    /// we also store it here to properly transform the viewport for sticky positioning.
    pub origin_in_parent_reference_frame: LayoutVector2D,

    /// True if this is the root reference frame for a given pipeline. This is only used
    /// by the hit-test code, perhaps we can change the interface to not require this.
    pub is_pipeline_root: bool,
}

#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct StickyFrameInfo {
  pub margins: SideOffsets2D<Option<f32>, LayoutPixel>,
  pub frame_rect: LayoutRect,
    pub vertical_offset_bounds: StickyOffsetBounds,
    pub horizontal_offset_bounds: StickyOffsetBounds,
    pub previously_applied_offset: LayoutVector2D,
    pub current_offset: LayoutVector2D,
    pub transform: Option<PropertyBinding<LayoutTransform>>,
}

impl StickyFrameInfo {
    pub fn new(
        frame_rect: LayoutRect,
        margins: SideOffsets2D<Option<f32>, LayoutPixel>,
        vertical_offset_bounds: StickyOffsetBounds,
        horizontal_offset_bounds: StickyOffsetBounds,
        previously_applied_offset: LayoutVector2D,
        transform: Option<PropertyBinding<LayoutTransform>>,
    ) -> StickyFrameInfo {
        StickyFrameInfo {
            frame_rect,
            margins,
            vertical_offset_bounds,
            horizontal_offset_bounds,
            previously_applied_offset,
            current_offset: LayoutVector2D::zero(),
            transform,
        }
    }
}

#[test]
fn test_cst_perspective_relative_scroll() {
    // Verify that when computing the offset from a perspective transform
    // to a relative scroll node that any external scroll offset is
    // ignored. This is because external scroll offsets are not
    // propagated across reference frame boundaries.

    // It's not currently possible to verify this with a wrench reftest,
    // since wrench doesn't understand external scroll ids. When wrench
    // supports this, we could also verify with a reftest.

    use crate::spatial_tree::{SceneSpatialTree, SpatialTree};
    use euclid::Angle;

    let mut cst = SceneSpatialTree::new();
    let pipeline_id = PipelineId::dummy();
    let ext_scroll_id = ExternalScrollId(1, pipeline_id);
    let transform = LayoutTransform::rotation(0.0, 0.0, 1.0, Angle::degrees(45.0));
    let pid = PipelineInstanceId::new(0);

    let root = cst.add_reference_frame(
        cst.root_reference_frame_index(),
        TransformStyle::Flat,
        PropertyBinding::Value(LayoutTransform::identity()),
        ReferenceFrameKind::Transform {
            is_2d_scale_translation: false,
            should_snap: false,
            paired_with_perspective: false,
        },
        LayoutVector2D::zero(),
        pipeline_id,
        SpatialNodeUid::external(SpatialTreeItemKey::new(0, 0), PipelineId::dummy(), pid),
    );

    let scroll_frame_1 = cst.add_scroll_frame(
        root,
        ext_scroll_id,
        pipeline_id,
        &LayoutRect::from_size(LayoutSize::new(100.0, 100.0)),
        &LayoutSize::new(100.0, 500.0),
        ScrollFrameKind::Explicit,
        LayoutVector2D::zero(),
        APZScrollGeneration::default(),
        HasScrollLinkedEffect::No,
        SpatialNodeUid::external(SpatialTreeItemKey::new(0, 1), PipelineId::dummy(), pid),
    );

    let scroll_frame_2 = cst.add_scroll_frame(
        scroll_frame_1,
        ExternalScrollId(2, pipeline_id),
        pipeline_id,
        &LayoutRect::from_size(LayoutSize::new(100.0, 100.0)),
        &LayoutSize::new(100.0, 500.0),
        ScrollFrameKind::Explicit,
        LayoutVector2D::new(0.0, 50.0),
        APZScrollGeneration::default(),
        HasScrollLinkedEffect::No,
        SpatialNodeUid::external(SpatialTreeItemKey::new(0, 3), PipelineId::dummy(), pid),
    );

    let ref_frame = cst.add_reference_frame(
        scroll_frame_2,
        TransformStyle::Preserve3D,
        PropertyBinding::Value(transform),
        ReferenceFrameKind::Perspective {
            scrolling_relative_to: Some(ext_scroll_id),
        },
        LayoutVector2D::zero(),
        pipeline_id,
        SpatialNodeUid::external(SpatialTreeItemKey::new(0, 4), PipelineId::dummy(), pid),
    );

    let mut st = SpatialTree::new();
    st.apply_updates(cst.end_frame_and_get_pending_updates());
    st.update_tree(&SceneProperties::new());

    let world_transform = st.get_world_transform(ref_frame).into_transform().cast_unit();
    let ref_transform = transform.then_translate(LayoutVector3D::new(0.0, -50.0, 0.0));
    assert!(world_transform.approx_eq(&ref_transform));
}

