/* 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/. */

extern crate euclid;
extern crate gleam;
extern crate glutin;
extern crate webrender;
extern crate winit;

#[path = "common/boilerplate.rs"]
mod boilerplate;

use crate::boilerplate::{Example, HandyDandyRectBuilder};
use euclid::SideOffsets2D;
use webrender::api::*;
use webrender::render_api::*;
use webrender::api::units::*;
use winit::dpi::LogicalPosition;


const EXT_SCROLL_ID_ROOT: u64 = 1;
const EXT_SCROLL_ID_CONTENT: u64 = 2;

struct App {
    cursor_position: WorldPoint,
    scroll_offset: LayoutVector2D,
}

impl Example for App {
    fn render(
        &mut self,
        _api: &mut RenderApi,
        builder: &mut DisplayListBuilder,
        _txn: &mut Transaction,
        _device_size: DeviceIntSize,
        pipeline_id: PipelineId,
        _document_id: DocumentId,
    ) {
        let root_space_and_clip = SpaceAndClipInfo::root_scroll(pipeline_id);
        builder.push_simple_stacking_context(
            LayoutPoint::zero(),
            root_space_and_clip.spatial_id,
            PrimitiveFlags::IS_BACKFACE_VISIBLE,
        );

        if true {
            // scrolling and clips stuff
            // let's make a scrollbox
            let scrollbox = (0, 0).to(300, 400);
            builder.push_simple_stacking_context(
                LayoutPoint::new(10., 10.),
                root_space_and_clip.spatial_id,
                PrimitiveFlags::IS_BACKFACE_VISIBLE,
            );
            // set the scrolling clip
            let space1 = builder.define_scroll_frame(
                root_space_and_clip.spatial_id,
                ExternalScrollId(EXT_SCROLL_ID_ROOT, PipelineId::dummy()),
                (0, 0).by(1000, 1000),
                scrollbox,
                LayoutVector2D::zero(),
                APZScrollGeneration::default(),
                HasScrollLinkedEffect::No,
                SpatialTreeItemKey::new(0, 0),
            );
            let space_and_clip1 = SpaceAndClipInfo {
                spatial_id: space1,
                clip_chain_id: root_space_and_clip.clip_chain_id,
            };

            // now put some content into it.
            // start with a white background
            let info = CommonItemProperties::new((0, 0).to(1000, 1000), space_and_clip1);
            builder.push_hit_test(
                info.clip_rect,
                ClipChainId::INVALID,
                info.spatial_id,
                info.flags,
                (0, 1)
            );
            builder.push_rect(&info, info.clip_rect, ColorF::new(1.0, 1.0, 1.0, 1.0));

            // let's make a 50x50 blue square as a visual reference
            let info = CommonItemProperties::new((0, 0).to(50, 50), space_and_clip1);
            builder.push_hit_test(
                info.clip_rect,
                ClipChainId::INVALID,
                info.spatial_id,
                info.flags,
                (0, 2)
            );
            builder.push_rect(&info, info.clip_rect, ColorF::new(0.0, 0.0, 1.0, 1.0));

            // and a 50x50 green square next to it with an offset clip
            // to see what that looks like
            let info = CommonItemProperties::new(
                (50, 0).to(100, 50).intersection(&(60, 10).to(110, 60)).unwrap(),
                space_and_clip1,
            );
            builder.push_hit_test(
                info.clip_rect,
                ClipChainId::INVALID,
                info.spatial_id,
                info.flags,
                (0, 3)
            );
            builder.push_rect(&info, info.clip_rect, ColorF::new(0.0, 1.0, 0.0, 1.0));

            // Below the above rectangles, set up a nested scrollbox. It's still in
            // the same stacking context, so note that the rects passed in need to
            // be relative to the stacking context.
            let space2 = builder.define_scroll_frame(
                space1,
                ExternalScrollId(EXT_SCROLL_ID_CONTENT, PipelineId::dummy()),
                (0, 100).to(300, 1000),
                (0, 100).to(200, 300),
                LayoutVector2D::zero(),
                APZScrollGeneration::default(),
                HasScrollLinkedEffect::No,
                SpatialTreeItemKey::new(0, 1),
            );
            let space_and_clip2 = SpaceAndClipInfo {
                spatial_id: space2,
                clip_chain_id: root_space_and_clip.clip_chain_id,
            };

            // give it a giant gray background just to distinguish it and to easily
            // visually identify the nested scrollbox
            let info = CommonItemProperties::new(
                (-1000, -1000).to(5000, 5000),
                space_and_clip2,
            );
            builder.push_hit_test(
                info.clip_rect,
                ClipChainId::INVALID,
                info.spatial_id,
                info.flags,
                (0, 4)
            );
            builder.push_rect(&info, info.clip_rect, ColorF::new(0.5, 0.5, 0.5, 1.0));

            // add a teal square to visualize the scrolling/clipping behaviour
            // as you scroll the nested scrollbox
            let info = CommonItemProperties::new((0, 200).to(50, 250), space_and_clip2);
            builder.push_hit_test(
                info.clip_rect,
                ClipChainId::INVALID,
                info.spatial_id,
                info.flags,
                (0, 5)
            );
            builder.push_rect(&info, info.clip_rect, ColorF::new(0.0, 1.0, 1.0, 1.0));

            // Add a sticky frame. It will "stick" twice while scrolling, once
            // at a margin of 10px from the bottom, for 40 pixels of scrolling,
            // and once at a margin of 10px from the top, for 60 pixels of
            // scrolling.
            let sticky_id = builder.define_sticky_frame(
                space_and_clip2.spatial_id,
                (50, 350).by(50, 50),
                SideOffsets2D::new(Some(10.0), None, Some(10.0), None),
                StickyOffsetBounds::new(-40.0, 60.0),
                StickyOffsetBounds::new(0.0, 0.0),
                LayoutVector2D::new(0.0, 0.0),
                SpatialTreeItemKey::new(0, 2),
                None,
            );

            let info = CommonItemProperties::new(
                (50, 350).by(50, 50),
                SpaceAndClipInfo {
                    spatial_id: sticky_id,
                    clip_chain_id: space_and_clip2.clip_chain_id,
                },
            );
            builder.push_hit_test(
                info.clip_rect,
                ClipChainId::INVALID,
                info.spatial_id,
                info.flags,
                (0, 6)
            );
            builder.push_rect(
                &info,
                info.clip_rect,
                ColorF::new(0.5, 0.5, 1.0, 1.0),
            );

            // just for good measure add another teal square further down and to
            // the right, which can be scrolled into view by the user
            let info = CommonItemProperties::new(
                (250, 350).to(300, 400),
                space_and_clip2,
            );
            builder.push_hit_test(
                info.clip_rect,
                ClipChainId::INVALID,
                info.spatial_id,
                info.flags,
                (0, 7)
            );
            builder.push_rect(&info, info.clip_rect, ColorF::new(0.0, 1.0, 1.0, 1.0));

            builder.pop_stacking_context();
        }

        builder.pop_stacking_context();
    }

    fn on_event(
        &mut self,
        event: winit::event::WindowEvent,
        window: &winit::window::Window,
        api: &mut RenderApi,
        document_id: DocumentId,
    ) -> bool {
        let mut txn = Transaction::new();
        match event {
            winit::event::WindowEvent::KeyboardInput {
                input: winit::event::KeyboardInput {
                    state: winit::event::ElementState::Pressed,
                    virtual_keycode: Some(key),
                    ..
                },
                ..
            } => {
                let offset = match key {
                    winit::event::VirtualKeyCode::Down => Some(LayoutVector2D::new(0.0, -10.0)),
                    winit::event::VirtualKeyCode::Up => Some(LayoutVector2D::new(0.0, 10.0)),
                    winit::event::VirtualKeyCode::Right => Some(LayoutVector2D::new(-10.0, 0.0)),
                    winit::event::VirtualKeyCode::Left => Some(LayoutVector2D::new(10.0, 0.0)),
                    _ => None,
                };

                if let Some(offset) = offset {
                    self.scroll_offset += offset;

                    txn.set_scroll_offsets(
                        ExternalScrollId(EXT_SCROLL_ID_CONTENT, PipelineId::dummy()),
                        vec![SampledScrollOffset {
                            offset: self.scroll_offset,
                            generation: APZScrollGeneration::default(),
                        }],
                    );
                    txn.generate_frame(0, true, false, RenderReasons::empty());
                }
            }
            winit::event::WindowEvent::CursorMoved { position, .. } => {
                let pos: LogicalPosition<f32> = position.to_logical(window.scale_factor());
                self.cursor_position = WorldPoint::new(pos.x, pos.y);
            }
            winit::event::WindowEvent::MouseWheel { delta, .. } => {
                const LINE_HEIGHT: f32 = 38.0;
                let (dx, dy) = match delta {
                    winit::event::MouseScrollDelta::LineDelta(dx, dy) => (dx, dy * LINE_HEIGHT),
                    winit::event::MouseScrollDelta::PixelDelta(pos) => (pos.x as f32, pos.y as f32),
                };

                self.scroll_offset += LayoutVector2D::new(dx, dy);

                txn.set_scroll_offsets(
                    ExternalScrollId(EXT_SCROLL_ID_CONTENT, PipelineId::dummy()),
                    vec![SampledScrollOffset {
                            offset: self.scroll_offset,
                            generation: APZScrollGeneration::default(),
                    }],
                );

                txn.generate_frame(0, true, false, RenderReasons::empty());
            }
            winit::event::WindowEvent::MouseInput { .. } => {
                let results = api.hit_test(
                    document_id,
                    self.cursor_position,
                );

                println!("Hit test results:");
                for item in &results.items {
                    println!("  • {:?}", item);
                }
                println!("");
            }
            _ => (),
        }

        api.send_transaction(document_id, txn);

        false
    }
}

fn main() {
    let mut app = App {
        cursor_position: WorldPoint::zero(),
        scroll_offset: LayoutVector2D::zero(),
    };
    boilerplate::main_wrapper(&mut app, None);
}
