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

/*

   An example of how to implement the Compositor trait that
   allows picture caching surfaces to be composited by the operating
   system.

   The current example supports DirectComposite on Windows only.

*/

use euclid::Angle;
use gleam::gl;
use std::ffi::CString;
use std::sync::mpsc;
use webrender::{CompositorSurfaceTransform, Transaction, api::*};
use webrender::api::units::*;
use webrender::Device;
#[cfg(target_os = "windows")]
use compositor_windows as compositor;
#[cfg(target_os = "linux")]
use compositor_wayland as compositor;
use std::{env, f32, process};

// A very hacky integration with DirectComposite. It proxies calls from the compositor
// interface to a simple C99 library which does the DirectComposition / D3D11 / ANGLE
// interfacing. This is a very unsafe impl due to the way the window pointer is passed
// around!
struct DirectCompositeInterface {
    window: *mut compositor::Window,
}

impl DirectCompositeInterface {
    fn new(window: *mut compositor::Window) -> Self {
        DirectCompositeInterface { window }
    }
}

impl webrender::Compositor for DirectCompositeInterface {
    fn create_surface(
        &mut self,
        _device: &mut Device,
        id: webrender::NativeSurfaceId,
        _virtual_offset: DeviceIntPoint,
        tile_size: DeviceIntSize,
        is_opaque: bool,
    ) {
        compositor::create_surface(
            self.window,
            id.0,
            tile_size.width,
            tile_size.height,
            is_opaque,
        );
    }

    fn destroy_surface(&mut self, _device: &mut Device, id: webrender::NativeSurfaceId) {
        compositor::destroy_surface(self.window, id.0);
    }

    fn create_tile(&mut self, _device: &mut Device, id: webrender::NativeTileId) {
        compositor::create_tile(self.window, id.surface_id.0, id.x, id.y);
    }

    fn destroy_tile(&mut self, _device: &mut Device, id: webrender::NativeTileId) {
        compositor::destroy_tile(self.window, id.surface_id.0, id.x, id.y);
    }

    fn bind(
        &mut self,
        _device: &mut Device,
        id: webrender::NativeTileId,
        dirty_rect: DeviceIntRect,
        _valid_rect: DeviceIntRect,
    ) -> webrender::NativeSurfaceInfo {
        let (fbo_id, x, y) = compositor::bind_surface(
            self.window,
            id.surface_id.0,
            id.x,
            id.y,
            dirty_rect.min.x,
            dirty_rect.min.y,
            dirty_rect.width(),
            dirty_rect.height(),
        );

        webrender::NativeSurfaceInfo {
            origin: DeviceIntPoint::new(x, y),
            fbo_id,
        }
    }

    fn unbind(&mut self, _device: &mut Device) {
        compositor::unbind_surface(self.window);
    }

    fn begin_frame(&mut self, _device: &mut Device) {
        compositor::begin_transaction(self.window);
    }

    fn add_surface(
        &mut self,
        _device: &mut Device,
        id: webrender::NativeSurfaceId,
        transform: CompositorSurfaceTransform,
        clip_rect: DeviceIntRect,
        _image_rendering: ImageRendering,
    ) {
        compositor::add_surface(
            self.window,
            id.0,
            transform.offset.x as i32,
            transform.offset.y as i32,
            clip_rect.min.x,
            clip_rect.min.y,
            clip_rect.width(),
            clip_rect.height(),
        );
    }

    fn end_frame(&mut self, _device: &mut Device) {
        compositor::end_transaction(self.window);
    }
    fn create_external_surface(
        &mut self,
        _device: &mut Device,
        _id: webrender::NativeSurfaceId,
        _: bool,
    ) {
        todo!()
    }

    fn attach_external_image(
        &mut self,
        _device: &mut Device,
        _id: webrender::NativeSurfaceId,
        _external_image: ExternalImageId,
    ) {
        todo!()
    }

    fn enable_native_compositor(&mut self, _device: &mut Device, _enable: bool) {
        todo!()
    }

    fn deinit(&mut self, _device: &mut Device) {
        compositor::deinit(self.window);
    }

    fn get_capabilities(&self, _device: &mut Device) -> webrender::CompositorCapabilities {
        webrender::CompositorCapabilities {
            virtual_surface_size: 1024 * 1024,
            ..Default::default()
        }
    }

    fn invalidate_tile(
        &mut self,
        _device: &mut Device,
        _id: webrender::NativeTileId,
        _valid_rect: DeviceIntRect,
    ) {
    }

    fn start_compositing(
        &mut self,
        _device: &mut Device,
        _color: webrender::webrender_api::ColorF,
        _dirty_rects: &[DeviceIntRect],
        _opaque_rects: &[DeviceIntRect],
    ) {
    }

    fn create_backdrop_surface(
        &mut self,
        _device: &mut Device,
        _id: webrender::NativeSurfaceId,
        _color: webrender::webrender_api::ColorF,
    ) {
        todo!()
    }

    fn get_window_visibility(&self, _device: &mut Device) -> webrender::WindowVisibility {
        todo!()
    }
}

// Simplisitic implementation of the WR notifier interface to know when a frame
// has been prepared and can be rendered.
struct Notifier {
    tx: mpsc::Sender<()>,
}

impl Notifier {
    fn new(tx: mpsc::Sender<()>) -> Self {
        Notifier { tx }
    }
}

impl RenderNotifier for Notifier {
    fn clone(&self) -> Box<dyn RenderNotifier> {
        Box::new(Notifier {
            tx: self.tx.clone(),
        })
    }

    fn wake_up(&self, _composite_needed: bool) {}

    fn new_frame_ready(&self, _: DocumentId, _scrolled: bool, _composite_needed: bool, _: FramePublishId) {
        self.tx.send(()).ok();
    }
}

fn push_rotated_rect(
    builder: &mut DisplayListBuilder,
    rect: LayoutRect,
    color: ColorF,
    spatial_id: SpatialId,
    _root_pipeline_id: PipelineId,
    angle: f32,
    time: f32,
    item_key: SpatialTreeItemKey,
) {
    let color = color.scale_rgb(time);
    let rotation = LayoutTransform::rotation(
        0.0,
        0.0,
        1.0,
        Angle::radians(2.0 * std::f32::consts::PI * angle),
    );
    let center = rect.center();
    let transform_origin = LayoutVector3D::new(center.x, center.y, 0.0);
    let transform = rotation
        .pre_translate(-transform_origin)
        .then_translate(transform_origin);
    let spatial_id = builder.push_reference_frame(
        LayoutPoint::zero(),
        spatial_id,
        TransformStyle::Flat,
        PropertyBinding::Value(transform),
        ReferenceFrameKind::Transform {
            is_2d_scale_translation: false,
            should_snap: false,
            paired_with_perspective: false,
        },
        item_key,
    );
    builder.push_rect(
        &CommonItemProperties::new(
            rect,
            SpaceAndClipInfo {
                spatial_id,
                clip_chain_id: ClipChainId::INVALID,
            },
        ),
        rect,
        color,
    );
}

fn build_display_list(
    builder: &mut DisplayListBuilder,
    scroll_id: ExternalScrollId,
    root_pipeline_id: PipelineId,
    layout_size: LayoutSize,
    time: f32,
    invalidations: Invalidations,
) {
    let size_factor = match invalidations {
        Invalidations::Small => 0.1,
        Invalidations::Large | Invalidations::Scrolling => 1.0,
    };

    let fixed_space_info = SpaceAndClipInfo {
        spatial_id: SpatialId::root_scroll_node(root_pipeline_id),
        clip_chain_id: ClipChainId::INVALID,
    };

    let scroll_spatial_id = builder.define_scroll_frame(
        fixed_space_info.spatial_id,
        scroll_id,
        LayoutRect::from_size(layout_size),
        LayoutRect::from_size(layout_size),
        LayoutVector2D::zero(),
        APZScrollGeneration::default(),
        HasScrollLinkedEffect::No,
        SpatialTreeItemKey::new(1, 0),
    );

    builder.push_rect(
        &CommonItemProperties::new(
            LayoutRect::from_size(layout_size).inflate(-10.0, -10.0),
            fixed_space_info,
        ),
        LayoutRect::from_size(layout_size).inflate(-10.0, -10.0),
        ColorF::new(0.8, 0.8, 0.8, 1.0),
    );

    push_rotated_rect(
        builder,
        LayoutRect::from_origin_and_size(
            LayoutPoint::new(100.0, 100.0),
            LayoutSize::new(size_factor * 400.0, size_factor * 400.0),
        ),
        ColorF::new(1.0, 0.0, 0.0, 1.0),
        scroll_spatial_id,
        root_pipeline_id,
        time,
        time,
        SpatialTreeItemKey::new(1, 1),
    );

    push_rotated_rect(
        builder,
        LayoutRect::from_origin_and_size(
            LayoutPoint::new(800.0, 100.0),
            LayoutSize::new(size_factor * 100.0, size_factor * 600.0),
        ),
        ColorF::new(0.0, 1.0, 0.0, 1.0),
        fixed_space_info.spatial_id,
        root_pipeline_id,
        0.2,
        time,
        SpatialTreeItemKey::new(1, 2),
    );

    push_rotated_rect(
        builder,
        LayoutRect::from_origin_and_size(
            LayoutPoint::new(700.0, 200.0),
            LayoutSize::new(size_factor * 300.0, size_factor * 300.0),
        ),
        ColorF::new(0.0, 0.0, 1.0, 1.0),
        scroll_spatial_id,
        root_pipeline_id,
        0.1,
        time,
        SpatialTreeItemKey::new(1, 3),
    );

    push_rotated_rect(
        builder,
        LayoutRect::from_origin_and_size(
            LayoutPoint::new(100.0, 600.0),
            LayoutSize::new(size_factor * 400.0, size_factor * 400.0),
        ),
        ColorF::new(1.0, 1.0, 0.0, 1.0),
        scroll_spatial_id,
        root_pipeline_id,
        time,
        time,
        SpatialTreeItemKey::new(1, 4),
    );

    push_rotated_rect(
        builder,
        LayoutRect::from_origin_and_size(
            LayoutPoint::new(700.0, 600.0),
            LayoutSize::new(size_factor * 400.0, size_factor * 400.0),
        ),
        ColorF::new(0.0, 1.0, 1.0, 1.0),
        scroll_spatial_id,
        root_pipeline_id,
        time,
        time,
        SpatialTreeItemKey::new(1, 5),
    );
}

#[derive(Debug, Copy, Clone)]
enum Invalidations {
    Large,
    Small,
    Scrolling,
}

#[repr(C)]
#[derive(Debug, Copy, Clone)]
enum Sync {
    None = 0,
    Swap = 1,
    Commit = 2,
    Flush = 3,
    Query = 4,
}

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() != 6 {
        println!("USAGE: compositor [native|none] [small|large|scroll] [none|swap|commit|flush|query] width height");
        process::exit(0);
    }

    let enable_compositor = match args[1].parse::<String>().unwrap().as_str() {
        "native" => true,
        "none" => false,
        _ => panic!("invalid compositor [native, none]"),
    };

    let inv_mode = match args[2].parse::<String>().unwrap().as_str() {
        "small" => Invalidations::Small,
        "large" => Invalidations::Large,
        "scroll" => Invalidations::Scrolling,
        _ => panic!("invalid invalidations [small, large, scroll]"),
    };

    let sync_mode = match args[3].parse::<String>().unwrap().as_str() {
        "none" => Sync::None,
        "swap" => Sync::Swap,
        "commit" => Sync::Commit,
        "flush" => Sync::Flush,
        "query" => Sync::Query,
        _ => panic!("invalid sync mode [none, swap, commit, flush, query]"),
    };

    let width = args[4].parse().unwrap();
    let height = args[5].parse().unwrap();
    let device_size = DeviceIntSize::new(width, height);

    // Load GL, construct WR and the native compositor interface.
    let window = compositor::create_window(
        device_size.width,
        device_size.height,
        enable_compositor,
        sync_mode as i32,
    );
    let debug_flags = DebugFlags::empty();
    let compositor_config = if enable_compositor {
        webrender::CompositorConfig::Native {
            compositor: Box::new(DirectCompositeInterface::new(window)),
        }
    } else {
        webrender::CompositorConfig::Draw {
            max_partial_present_rects: 0,
            draw_previous_partial_present_regions: false,
            partial_present: None,
        }
    };
    let opts = webrender::WebRenderOptions {
        clear_color: ColorF::new(1.0, 1.0, 1.0, 1.0),
        debug_flags,
        compositor_config,
        surface_origin_is_top_left: false,
        ..webrender::WebRenderOptions::default()
    };
    let (tx, rx) = mpsc::channel();
    let notifier = Box::new(Notifier::new(tx));
    let gl = unsafe {
        gl::GlesFns::load_with(|symbol| {
            let symbol = CString::new(symbol).unwrap();
            let ptr = compositor::get_proc_address(symbol.as_ptr());
            ptr
        })
    };
    let (mut renderer, sender) =
        webrender::create_webrender_instance(gl.clone(), notifier, opts, None).unwrap();
    let mut api = sender.create_api();
    let document_id = api.add_document(device_size);
    let device_pixel_ratio = 1.0;
    let mut current_epoch = Epoch(0);
    let root_pipeline_id = PipelineId(0, 0);
    let layout_size = device_size.to_f32() / euclid::Scale::new(device_pixel_ratio);
    let mut time = 0.0;
    let scroll_id = ExternalScrollId(3, root_pipeline_id);

    // Kick off first transaction which will mean we get a notify below to build the DL and render.
    let mut txn = Transaction::new();
    txn.set_root_pipeline(root_pipeline_id);

    if let Invalidations::Scrolling = inv_mode {
        let mut root_builder = DisplayListBuilder::new(root_pipeline_id);
        root_builder.begin();

        build_display_list(
            &mut root_builder,
            scroll_id,
            root_pipeline_id,
            layout_size,
            1.0,
            inv_mode,
        );

        txn.set_display_list(current_epoch, root_builder.end());
    }

    txn.generate_frame(0, RenderReasons::empty());
    api.send_transaction(document_id, txn);

    // Tick the compositor (in this sample, we don't block on UI events)
    while compositor::tick(window) {
        // If there is a new frame ready to draw
        if let Ok(..) = rx.try_recv() {
            // Update and render. This will invoke the native compositor interface implemented above
            // as required.
            renderer.update();
            renderer.render(device_size, 0).unwrap();
            let _ = renderer.flush_pipeline_info();

            // Construct a simple display list that can be drawn and composited by DC.
            let mut txn = Transaction::new();

            match inv_mode {
                Invalidations::Small | Invalidations::Large => {
                    let mut root_builder = DisplayListBuilder::new(root_pipeline_id);
                    root_builder.begin();

                    build_display_list(
                        &mut root_builder,
                        scroll_id,
                        root_pipeline_id,
                        layout_size,
                        time,
                        inv_mode,
                    );

                    txn.set_display_list(current_epoch, root_builder.end());
                }
                Invalidations::Scrolling => {
                    let d = 0.5 - 0.5 * (2.0 * f32::consts::PI * 5.0 * time).cos();
                    txn.set_scroll_offsets(
                        scroll_id,
                        vec![SampledScrollOffset {
                            offset: LayoutVector2D::new(0.0, (d * 100.0).round()),
                            generation: APZScrollGeneration::default(),
                        }],
                    );
                }
            }

            txn.generate_frame(0, RenderReasons::empty());
            api.send_transaction(document_id, txn);
            current_epoch.0 += 1;
            time += 0.001;
            if time > 1.0 {
                time = 0.0;
            }

            // This does nothing when native compositor is enabled
            compositor::swap_buffers(window);
        }
    }

    renderer.deinit();
    compositor::destroy_window(window);
}
