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

use crate::dom::bindings::codegen::Bindings::WebGL2RenderingContextBinding::WebGL2RenderingContextConstants as constants;
use crate::dom::bindings::codegen::Bindings::WebGLRenderingContextBinding::WebGLRenderingContextMethods;
use crate::dom::bindings::codegen::Bindings::XRWebGLLayerBinding::XRWebGLLayerInit;
use crate::dom::bindings::codegen::Bindings::XRWebGLLayerBinding::XRWebGLLayerMethods;
use crate::dom::bindings::codegen::Bindings::XRWebGLLayerBinding::XRWebGLRenderingContext;
use crate::dom::bindings::error::Error;
use crate::dom::bindings::error::Fallible;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::reflector::{reflect_dom_object, DomObject};
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::globalscope::GlobalScope;
use crate::dom::webglframebuffer::WebGLFramebuffer;
use crate::dom::webglobject::WebGLObject;
use crate::dom::webglrenderingcontext::WebGLRenderingContext;
use crate::dom::webgltexture::WebGLTexture;
use crate::dom::window::Window;
use crate::dom::xrframe::XRFrame;
use crate::dom::xrlayer::XRLayer;
use crate::dom::xrsession::XRSession;
use crate::dom::xrview::XRView;
use crate::dom::xrviewport::XRViewport;
use canvas_traits::webgl::WebGLCommand;
use canvas_traits::webgl::WebGLContextId;
use canvas_traits::webgl::WebGLTextureId;
use dom_struct::dom_struct;
use euclid::{Rect, Size2D};
use std::convert::TryInto;
use webxr_api::ContextId as WebXRContextId;
use webxr_api::LayerId;
use webxr_api::LayerInit;
use webxr_api::Viewport;

impl<'a> From<&'a XRWebGLLayerInit> for LayerInit {
    fn from(init: &'a XRWebGLLayerInit) -> LayerInit {
        LayerInit::WebGLLayer {
            alpha: init.alpha,
            antialias: init.antialias,
            depth: init.depth,
            stencil: init.stencil,
            framebuffer_scale_factor: *init.framebufferScaleFactor as f32,
            ignore_depth_values: init.ignoreDepthValues,
        }
    }
}

#[dom_struct]
pub struct XRWebGLLayer {
    xr_layer: XRLayer,
    antialias: bool,
    depth: bool,
    stencil: bool,
    alpha: bool,
    ignore_depth_values: bool,
    /// If none, this is an inline session (the composition disabled flag is true)
    framebuffer: Option<Dom<WebGLFramebuffer>>,
}

impl XRWebGLLayer {
    pub fn new_inherited(
        session: &XRSession,
        context: &WebGLRenderingContext,
        init: &XRWebGLLayerInit,
        framebuffer: Option<&WebGLFramebuffer>,
        layer_id: Option<LayerId>,
    ) -> XRWebGLLayer {
        XRWebGLLayer {
            xr_layer: XRLayer::new_inherited(session, context, layer_id),
            antialias: init.antialias,
            depth: init.depth,
            stencil: init.stencil,
            alpha: init.alpha,
            ignore_depth_values: init.ignoreDepthValues,
            framebuffer: framebuffer.map(Dom::from_ref),
        }
    }

    pub fn new(
        global: &GlobalScope,
        session: &XRSession,
        context: &WebGLRenderingContext,
        init: &XRWebGLLayerInit,
        framebuffer: Option<&WebGLFramebuffer>,
        layer_id: Option<LayerId>,
    ) -> DomRoot<XRWebGLLayer> {
        reflect_dom_object(
            Box::new(XRWebGLLayer::new_inherited(
                session,
                context,
                init,
                framebuffer,
                layer_id,
            )),
            global,
        )
    }

    /// https://immersive-web.github.io/webxr/#dom-xrwebgllayer-xrwebgllayer
    #[allow(non_snake_case)]
    pub fn Constructor(
        global: &Window,
        session: &XRSession,
        context: XRWebGLRenderingContext,
        init: &XRWebGLLayerInit,
    ) -> Fallible<DomRoot<Self>> {
        let context = match context {
            XRWebGLRenderingContext::WebGLRenderingContext(ctx) => ctx,
            XRWebGLRenderingContext::WebGL2RenderingContext(ctx) => ctx.base_context(),
        };

        // Step 2
        if session.is_ended() {
            return Err(Error::InvalidState);
        }
        // XXXManishearth step 3: throw error if context is lost
        // XXXManishearth step 4: check XR compat flag for immersive sessions

        let (framebuffer, layer_id) = if session.is_immersive() {
            // Step 9.2. "Initialize layer’s framebuffer to a new opaque framebuffer created with context."
            let size = session
                .with_session(|session| session.recommended_framebuffer_resolution())
                .ok_or(Error::Operation)?;
            let framebuffer = WebGLFramebuffer::maybe_new_webxr(session, &context, size)
                .ok_or(Error::Operation)?;

            // Step 9.3. "Allocate and initialize resources compatible with session’s XR device,
            // including GPU accessible memory buffers, as required to support the compositing of layer."
            let context_id = WebXRContextId::from(context.context_id());
            let layer_init = LayerInit::from(init);
            let layer_id = session
                .with_session(|session| session.create_layer(context_id, layer_init))
                .map_err(|_| Error::Operation)?;

            // Step 9.4: "If layer’s resources were unable to be created for any reason,
            // throw an OperationError and abort these steps."
            (Some(framebuffer), Some(layer_id))
        } else {
            (None, None)
        };

        // Ensure that we finish setting up this layer before continuing.
        context.Finish();

        // Step 10. "Return layer."
        Ok(XRWebGLLayer::new(
            &global.global(),
            session,
            &context,
            init,
            framebuffer.as_deref(),
            layer_id,
        ))
    }

    pub fn layer_id(&self) -> Option<LayerId> {
        self.xr_layer.layer_id()
    }

    pub fn context_id(&self) -> WebGLContextId {
        self.xr_layer.context_id()
    }

    pub fn session(&self) -> &XRSession {
        &self.xr_layer.session()
    }

    pub fn size(&self) -> Size2D<u32, Viewport> {
        if let Some(framebuffer) = self.framebuffer.as_ref() {
            let size = framebuffer.size().unwrap_or((0, 0));
            Size2D::new(
                size.0.try_into().unwrap_or(0),
                size.1.try_into().unwrap_or(0),
            )
        } else {
            let size = self.context().Canvas().get_size();
            Size2D::from_untyped(size)
        }
    }

    fn texture_target(&self) -> u32 {
        if cfg!(target_os = "macos") {
            sparkle::gl::TEXTURE_RECTANGLE
        } else {
            sparkle::gl::TEXTURE_2D
        }
    }

    pub fn begin_frame(&self, frame: &XRFrame) -> Option<()> {
        debug!("XRWebGLLayer begin frame");
        let framebuffer = self.framebuffer.as_ref()?;
        let context = framebuffer.upcast::<WebGLObject>().context();
        let sub_images = frame.get_sub_images(self.layer_id()?)?;
        let session = self.session();
        // TODO: Cache this texture
        let color_texture_id =
            WebGLTextureId::maybe_new(sub_images.sub_image.as_ref()?.color_texture)?;
        let color_texture = WebGLTexture::new_webxr(context, color_texture_id, session);
        let target = self.texture_target();

        // Save the current bindings
        let saved_framebuffer = context.get_draw_framebuffer_slot().get();
        let saved_framebuffer_target = framebuffer.target();
        let saved_texture_id = context
            .textures()
            .active_texture_slot(target, context.webgl_version())
            .ok()
            .and_then(|slot| slot.get().map(|texture| texture.id()));

        // We have to pick a framebuffer target.
        // If there is a draw framebuffer, we use its target,
        // otherwise we just use DRAW_FRAMEBUFFER.
        let framebuffer_target = saved_framebuffer
            .as_ref()
            .and_then(|fb| fb.target())
            .unwrap_or(constants::DRAW_FRAMEBUFFER);

        // Update the attachments
        context.send_command(WebGLCommand::BindTexture(target, Some(color_texture_id)));
        framebuffer.bind(framebuffer_target);
        framebuffer
            .texture2d_even_if_opaque(
                constants::COLOR_ATTACHMENT0,
                self.texture_target(),
                Some(&color_texture),
                0,
            )
            .ok()?;
        if let Some(id) = sub_images.sub_image.as_ref()?.depth_stencil_texture {
            // TODO: Cache this texture
            let depth_stencil_texture_id = WebGLTextureId::maybe_new(id)?;
            let depth_stencil_texture =
                WebGLTexture::new_webxr(context, depth_stencil_texture_id, session);
            framebuffer
                .texture2d_even_if_opaque(
                    constants::DEPTH_STENCIL_ATTACHMENT,
                    constants::TEXTURE_2D,
                    Some(&depth_stencil_texture),
                    0,
                )
                .ok()?;
        }

        // Restore the old bindings
        context.send_command(WebGLCommand::BindTexture(target, saved_texture_id));
        if let Some(framebuffer_target) = saved_framebuffer_target {
            framebuffer.bind(framebuffer_target);
        }
        if let Some(framebuffer) = saved_framebuffer {
            framebuffer.bind(framebuffer_target);
        }
        Some(())
    }

    pub fn end_frame(&self, _frame: &XRFrame) -> Option<()> {
        debug!("XRWebGLLayer end frame");
        // TODO: invalidate the old texture
        let framebuffer = self.framebuffer.as_ref()?;
        // TODO: rebind the current bindings
        framebuffer.bind(constants::FRAMEBUFFER);
        framebuffer
            .texture2d_even_if_opaque(constants::COLOR_ATTACHMENT0, self.texture_target(), None, 0)
            .ok()?;
        framebuffer
            .texture2d_even_if_opaque(
                constants::DEPTH_STENCIL_ATTACHMENT,
                constants::DEPTH_STENCIL_ATTACHMENT,
                None,
                0,
            )
            .ok()?;
        framebuffer.upcast::<WebGLObject>().context().Flush();
        Some(())
    }

    pub(crate) fn context(&self) -> &WebGLRenderingContext {
        self.xr_layer.context()
    }
}

impl XRWebGLLayerMethods for XRWebGLLayer {
    /// https://immersive-web.github.io/webxr/#dom-xrwebgllayer-antialias
    fn Antialias(&self) -> bool {
        self.antialias
    }

    /// https://immersive-web.github.io/webxr/#dom-xrwebgllayer-ignoredepthvalues
    fn IgnoreDepthValues(&self) -> bool {
        self.ignore_depth_values
    }

    /// https://immersive-web.github.io/webxr/#dom-xrwebgllayer-framebuffer
    fn GetFramebuffer(&self) -> Option<DomRoot<WebGLFramebuffer>> {
        self.framebuffer.as_ref().map(|x| DomRoot::from_ref(&**x))
    }

    /// https://immersive-web.github.io/webxr/#dom-xrwebgllayer-framebufferwidth
    fn FramebufferWidth(&self) -> u32 {
        self.size().width
    }

    /// https://immersive-web.github.io/webxr/#dom-xrwebgllayer-framebufferheight
    fn FramebufferHeight(&self) -> u32 {
        self.size().height
    }

    /// https://immersive-web.github.io/webxr/#dom-xrwebgllayer-getviewport
    fn GetViewport(&self, view: &XRView) -> Option<DomRoot<XRViewport>> {
        if self.session() != view.session() {
            return None;
        }

        let index = view.viewport_index();

        let viewport = self.session().with_session(|s| {
            // Inline sssions
            if s.viewports().is_empty() {
                Rect::from_size(self.size().to_i32())
            } else {
                s.viewports()[index]
            }
        });

        Some(XRViewport::new(&self.global(), viewport))
    }
}