/* 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 std::os::raw::c_void;
use std::ptr;

use glutin::platform::windows::EGLContext;
use webrender::{CompositorInputConfig, CompositorSurfaceTransform, LayerCompositor};
use winit::platform::windows::WindowExtWindows;

use crate::WindowWrapper;

use mozangle::egl::ffi::types::{EGLDisplay, EGLSurface, EGLint};
use mozangle::egl;

// A simplistic implementation of the `LayerCompositor` trait to allow wrench to
// composite via DirectComposition. In this initial version, only a single
// swap-chain is supported. Follow up patches will add layer and external
// surface support.

#[repr(C)]
#[derive(Copy, Clone, Debug)]
pub struct CompositorHandle(usize);

#[repr(C)]
#[derive(Copy, Clone, PartialEq, Debug)]
struct LayerId(usize);

impl LayerId {
    const INVALID: LayerId = LayerId(0);
}

// FFI bindings to `composite.cpp`
#[link(name="wr_composite", kind="static")]
extern "C" {
    fn wrc_new(d3d11_device: *const c_void, hwnd: *const c_void) -> CompositorHandle;
    fn wrc_delete(compositor: CompositorHandle);

    fn wrc_create_layer(compositor: CompositorHandle, width: i32, height: i32, is_opaque: bool) -> LayerId;
    fn wrc_get_layer_backbuffer(layer_id: LayerId) -> *mut c_void;
    fn wrc_set_layer_position(layer_id: LayerId, x: f32, y: f32);
    fn wrc_present_layer(layer_id: LayerId);
    fn wrc_add_layer(compositor: CompositorHandle, layer_id: LayerId);

    fn wrc_begin_frame(compositor: CompositorHandle);
    fn wrc_end_frame(compositor: CompositorHandle);
}

// A basic layer - we only create one primary layer in the initial commit
struct WrLayer {
    // Layer dimensions
    width: i32,
    height: i32,
    // EGL surface that gets bound for webrender to draw to
    surface: EGLSurface,
    // Handle to the FFI layer
    layer_id: LayerId,
}

impl WrLayer {
    fn empty() -> Self {
        WrLayer {
            width: 0,
            height: 0,
            layer_id: LayerId::INVALID,
            surface: ptr::null(),
        }
    }
}

// A basic `LayerCompositor` implementation for wrench
pub struct WrCompositor {
    // EGL display and content, provided by winit
    context: EGLContext,
    display: EGLDisplay,
    // FFI compositor handle
    compositor: CompositorHandle,
    // The swapchain layers needed for current scene
    layers: Vec<WrLayer>,
}

impl WrCompositor {
    pub fn new(window: &WindowWrapper) -> Self {
        // Retrieve the D3D11 device from winit - this was created when ANGLE was initialized
        let d3d11_device = window.get_d3d11_device();

        // Get the win32 and EGL information needed from the window
        let (hwnd, display, context) = match window {
            WindowWrapper::Angle(window, angle, _, _) => {
                (window.hwnd(), angle.get_display(), angle.get_context())
            }
            _ => unreachable!(),
        };

        // Construct the FFI part of the compositor impl
        let compositor = unsafe {
            wrc_new(d3d11_device, hwnd)
        };

        WrCompositor {
            display,
            context,
            compositor,
            layers: Vec::new(),
        }
    }
}

impl Drop for WrCompositor {
    fn drop(&mut self) {
        unsafe {
            wrc_delete(self.compositor);
        }
    }
}

impl LayerCompositor for WrCompositor {
    // Begin compositing a frame with the supplied input config
    // The main job of this method is to inspect the input config, create the output
    // config, and create any native OS resources that will be needed as layers get
    // composited.
    fn begin_frame(
        &mut self,
        input: &CompositorInputConfig,
    ) -> bool {
        unsafe {
            // Reset DC visual tree
            wrc_begin_frame(self.compositor);
        }

        let prev_layer_count = self.layers.len();
        let curr_layer_count = input.layers.len();

        if prev_layer_count > curr_layer_count {
            todo!();
        } else if curr_layer_count > prev_layer_count {
            // Construct new empty layers, they'll get resized below
            for _ in 0 .. curr_layer_count-prev_layer_count {
                self.layers.push(WrLayer::empty());
            }
        }

        assert_eq!(self.layers.len(), input.layers.len());

        for (input, layer) in input.layers.iter().zip(self.layers.iter_mut()) {
            let input_size = input.clip_rect.size();

            // TODO(gwc): Handle External surfaces as swapchains separate from content

            // See if we need to resize the swap-chain layer
            if input_size.width != layer.width ||
                input_size.height != layer.height {
                // TODO: Handle resize of layer (not needed for initial commit,
                // but will need to support resizing wrench window)
                assert_eq!(layer.layer_id, LayerId::INVALID);

                // Construct a DC swap-chain
                layer.width = input_size.width;
                layer.height = input_size.height;
                layer.layer_id = unsafe {
                    wrc_create_layer(self.compositor, layer.width, layer.height, input.is_opaque)
                };

                let pbuffer_attribs: [EGLint; 5] = [
                    egl::ffi::WIDTH as EGLint, layer.width,
                    egl::ffi::HEIGHT as EGLint, layer.height,
                    egl::ffi::NONE as EGLint,
                ];

                let attribs: [EGLint; 18] = [
                    egl::ffi::SURFACE_TYPE as EGLint, egl::ffi::WINDOW_BIT as EGLint,
                    egl::ffi::RED_SIZE as EGLint, 8,
                    egl::ffi::GREEN_SIZE as EGLint, 8,
                    egl::ffi::BLUE_SIZE as EGLint, 8,
                    egl::ffi::ALPHA_SIZE as EGLint, 8,
                    egl::ffi::RENDERABLE_TYPE as EGLint, egl::ffi::OPENGL_ES2_BIT as EGLint,
                    // TODO(gw): Can we disable z-buffer for compositing always?
                    egl::ffi::DEPTH_SIZE as EGLint, 24,
                    egl::ffi::STENCIL_SIZE as EGLint, 8,
                    egl::ffi::NONE as EGLint, egl::ffi::NONE as EGLint
                ];

                // Get the D3D backbuffer texture for the swap-chain
                let back_buffer = unsafe {
                    wrc_get_layer_backbuffer(layer.layer_id)
                };

                // Create an EGL surface <-> binding to the D3D backbuffer texture
                let mut egl_config = ptr::null();
                let mut cfg_count = 0;

                let ok = unsafe {
                    egl::ffi::ChooseConfig(self.display, attribs.as_ptr(), &mut egl_config, 1, &mut cfg_count)
                };

                assert_ne!(ok, 0);
                assert_eq!(cfg_count, 1);
                assert_ne!(egl_config, ptr::null());

                let surface = unsafe {
                    egl::ffi::CreatePbufferFromClientBuffer(
                        self.display,
                        egl::ffi::D3D_TEXTURE_ANGLE,
                        back_buffer,
                        egl_config,
                        pbuffer_attribs.as_ptr(),
                    )
                };
                assert_ne!(surface, ptr::null());

                layer.surface = surface;
            }

            unsafe {
                wrc_set_layer_position(
                    layer.layer_id,
                    input.offset.x as f32,
                    input.offset.y as f32,
                );
            }
        }

        true
    }

    // Bind a layer by index for compositing into
    fn bind_layer(&mut self, index: usize, _dirty_rects: &[crate::DeviceIntRect]) {
        // Bind the DC surface to EGL so that WR can composite to the layer
        let layer = &self.layers[index];

        let ok = unsafe {
            egl::ffi::MakeCurrent(
                self.display,
                layer.surface,
                layer.surface,
                self.context,
            )
        };
        assert!(ok != 0);
    }

    // Finish compositing a layer and present the swapchain
    fn present_layer(&mut self, index: usize, _dirty_rects: &[crate::DeviceIntRect]) {
        let layer = &self.layers[index];

        unsafe {
            wrc_present_layer(layer.layer_id);
        }
    }

    fn add_surface(
        &mut self,
        index: usize,
        _transform: CompositorSurfaceTransform,
        _clip_rect: webrender::api::units::DeviceIntRect,
        _image_rendering: webrender::api::ImageRendering,
    ) {
        let layer = &self.layers[index];

        unsafe {
            wrc_add_layer(self.compositor, layer.layer_id);
        }
    }

    // Finish compositing this frame
    fn end_frame(&mut self) {
        unsafe {
            // Do any final commits to DC
            wrc_end_frame(self.compositor);
        }
    }

    fn get_window_properties(&self) -> webrender::WindowProperties {
        webrender::WindowProperties {
            is_opaque: true,
            enable_screenshot: false,
        }
    }
}
