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

//! Common interfaces for Canvas Contexts

use euclid::default::Size2D;
use script_bindings::root::Dom;
use script_layout_interface::HTMLCanvasData;
use snapshot::Snapshot;
use webrender_api::ImageKey;

use crate::dom::bindings::codegen::UnionTypes::HTMLCanvasElementOrOffscreenCanvas;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::htmlcanvaselement::HTMLCanvasElement;
use crate::dom::node::{Node, NodeDamage};
#[cfg(feature = "webgpu")]
use crate::dom::types::GPUCanvasContext;
use crate::dom::types::{
    CanvasRenderingContext2D, OffscreenCanvas, OffscreenCanvasRenderingContext2D,
    WebGL2RenderingContext, WebGLRenderingContext,
};

pub(crate) trait LayoutCanvasRenderingContextHelpers {
    /// `None` is rendered as transparent black (cleared canvas)
    fn canvas_data_source(self) -> Option<ImageKey>;
}

pub(crate) trait LayoutHTMLCanvasElementHelpers {
    fn data(self) -> HTMLCanvasData;
}

pub(crate) trait CanvasContext {
    type ID;

    fn context_id(&self) -> Self::ID;

    fn canvas(&self) -> Option<HTMLCanvasElementOrOffscreenCanvas>;

    fn resize(&self);

    /// Returns none if area of canvas is zero.
    ///
    /// In case of other errors it returns cleared snapshot
    fn get_image_data(&self) -> Option<Snapshot>;

    fn origin_is_clean(&self) -> bool {
        true
    }

    fn size(&self) -> Size2D<u32> {
        self.canvas()
            .map(|canvas| canvas.size())
            .unwrap_or_default()
    }

    fn mark_as_dirty(&self) {
        if let Some(HTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(canvas)) = &self.canvas()
        {
            canvas.upcast::<Node>().dirty(NodeDamage::Other);
        }
    }

    fn update_rendering(&self) {}

    fn onscreen(&self) -> bool {
        let Some(canvas) = self.canvas() else {
            return false;
        };

        match canvas {
            HTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(ref canvas) => {
                canvas.upcast::<Node>().is_connected()
            },
            // FIXME(34628): Offscreen canvases should be considered offscreen if a placeholder is set.
            // <https://www.w3.org/TR/webgpu/#abstract-opdef-updating-the-rendering-of-a-webgpu-canvas>
            HTMLCanvasElementOrOffscreenCanvas::OffscreenCanvas(_) => false,
        }
    }
}

pub(crate) trait CanvasHelpers {
    fn size(&self) -> Size2D<u32>;
    fn canvas(&self) -> Option<&HTMLCanvasElement>;
}

impl CanvasHelpers for HTMLCanvasElementOrOffscreenCanvas {
    fn size(&self) -> Size2D<u32> {
        match self {
            HTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(canvas) => {
                canvas.get_size().cast()
            },
            HTMLCanvasElementOrOffscreenCanvas::OffscreenCanvas(canvas) => canvas.get_size(),
        }
    }

    fn canvas(&self) -> Option<&HTMLCanvasElement> {
        match self {
            HTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(canvas) => Some(canvas),
            HTMLCanvasElementOrOffscreenCanvas::OffscreenCanvas(canvas) => canvas.placeholder(),
        }
    }
}

/// Non rooted variant of [`crate::dom::bindings::codegen::Bindings::HTMLCanvasElementBinding::RenderingContext`]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
#[derive(Clone, JSTraceable, MallocSizeOf)]
pub(crate) enum RenderingContext {
    Placeholder(Dom<OffscreenCanvas>),
    Context2d(Dom<CanvasRenderingContext2D>),
    WebGL(Dom<WebGLRenderingContext>),
    WebGL2(Dom<WebGL2RenderingContext>),
    #[cfg(feature = "webgpu")]
    WebGPU(Dom<GPUCanvasContext>),
}

impl CanvasContext for RenderingContext {
    type ID = ();

    fn context_id(&self) -> Self::ID {}

    fn canvas(&self) -> Option<HTMLCanvasElementOrOffscreenCanvas> {
        match self {
            RenderingContext::Placeholder(offscreen_canvas) => offscreen_canvas.context()?.canvas(),
            RenderingContext::Context2d(context) => context.canvas(),
            RenderingContext::WebGL(context) => context.canvas(),
            RenderingContext::WebGL2(context) => context.canvas(),
            #[cfg(feature = "webgpu")]
            RenderingContext::WebGPU(context) => context.canvas(),
        }
    }

    fn resize(&self) {
        match self {
            RenderingContext::Placeholder(offscreen_canvas) => {
                if let Some(context) = offscreen_canvas.context() {
                    context.resize()
                }
            },
            RenderingContext::Context2d(context) => context.resize(),
            RenderingContext::WebGL(context) => context.resize(),
            RenderingContext::WebGL2(context) => context.resize(),
            #[cfg(feature = "webgpu")]
            RenderingContext::WebGPU(context) => context.resize(),
        }
    }

    fn get_image_data(&self) -> Option<Snapshot> {
        match self {
            RenderingContext::Placeholder(offscreen_canvas) => {
                offscreen_canvas.context()?.get_image_data()
            },
            RenderingContext::Context2d(context) => context.get_image_data(),
            RenderingContext::WebGL(context) => context.get_image_data(),
            RenderingContext::WebGL2(context) => context.get_image_data(),
            #[cfg(feature = "webgpu")]
            RenderingContext::WebGPU(context) => context.get_image_data(),
        }
    }

    fn origin_is_clean(&self) -> bool {
        match self {
            RenderingContext::Placeholder(offscreen_canvas) => offscreen_canvas
                .context()
                .is_none_or(|context| context.origin_is_clean()),
            RenderingContext::Context2d(context) => context.origin_is_clean(),
            RenderingContext::WebGL(context) => context.origin_is_clean(),
            RenderingContext::WebGL2(context) => context.origin_is_clean(),
            #[cfg(feature = "webgpu")]
            RenderingContext::WebGPU(context) => context.origin_is_clean(),
        }
    }

    fn size(&self) -> Size2D<u32> {
        match self {
            RenderingContext::Placeholder(offscreen_canvas) => offscreen_canvas
                .context()
                .map(|context| context.size())
                .unwrap_or_default(),
            RenderingContext::Context2d(context) => context.size(),
            RenderingContext::WebGL(context) => context.size(),
            RenderingContext::WebGL2(context) => context.size(),
            #[cfg(feature = "webgpu")]
            RenderingContext::WebGPU(context) => context.size(),
        }
    }

    fn mark_as_dirty(&self) {
        match self {
            RenderingContext::Placeholder(offscreen_canvas) => {
                if let Some(context) = offscreen_canvas.context() {
                    context.mark_as_dirty()
                }
            },
            RenderingContext::Context2d(context) => context.mark_as_dirty(),
            RenderingContext::WebGL(context) => context.mark_as_dirty(),
            RenderingContext::WebGL2(context) => context.mark_as_dirty(),
            #[cfg(feature = "webgpu")]
            RenderingContext::WebGPU(context) => context.mark_as_dirty(),
        }
    }

    fn update_rendering(&self) {
        match self {
            RenderingContext::Placeholder(offscreen_canvas) => {
                if let Some(context) = offscreen_canvas.context() {
                    context.update_rendering()
                }
            },
            RenderingContext::Context2d(context) => context.update_rendering(),
            RenderingContext::WebGL(context) => context.update_rendering(),
            RenderingContext::WebGL2(context) => context.update_rendering(),
            #[cfg(feature = "webgpu")]
            RenderingContext::WebGPU(context) => context.update_rendering(),
        }
    }

    fn onscreen(&self) -> bool {
        match self {
            RenderingContext::Placeholder(offscreen_canvas) => offscreen_canvas
                .context()
                .is_some_and(|context| context.onscreen()),
            RenderingContext::Context2d(context) => context.onscreen(),
            RenderingContext::WebGL(context) => context.onscreen(),
            RenderingContext::WebGL2(context) => context.onscreen(),
            #[cfg(feature = "webgpu")]
            RenderingContext::WebGPU(context) => context.onscreen(),
        }
    }
}

/// Non rooted variant of [`crate::dom::bindings::codegen::Bindings::OffscreenCanvasBinding::OffscreenRenderingContext`]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
#[derive(Clone, JSTraceable, MallocSizeOf)]
pub(crate) enum OffscreenRenderingContext {
    Context2d(Dom<OffscreenCanvasRenderingContext2D>),
    //WebGL(Dom<WebGLRenderingContext>),
    //WebGL2(Dom<WebGL2RenderingContext>),
    //#[cfg(feature = "webgpu")]
    //WebGPU(Dom<GPUCanvasContext>),
}

impl CanvasContext for OffscreenRenderingContext {
    type ID = ();

    fn context_id(&self) -> Self::ID {}

    fn canvas(&self) -> Option<HTMLCanvasElementOrOffscreenCanvas> {
        match self {
            OffscreenRenderingContext::Context2d(context) => context.canvas(),
        }
    }

    fn resize(&self) {
        match self {
            OffscreenRenderingContext::Context2d(context) => context.resize(),
        }
    }

    fn get_image_data(&self) -> Option<Snapshot> {
        match self {
            OffscreenRenderingContext::Context2d(context) => context.get_image_data(),
        }
    }

    fn origin_is_clean(&self) -> bool {
        match self {
            OffscreenRenderingContext::Context2d(context) => context.origin_is_clean(),
        }
    }

    fn size(&self) -> Size2D<u32> {
        match self {
            OffscreenRenderingContext::Context2d(context) => context.size(),
        }
    }

    fn mark_as_dirty(&self) {
        match self {
            OffscreenRenderingContext::Context2d(context) => context.mark_as_dirty(),
        }
    }

    fn update_rendering(&self) {
        match self {
            OffscreenRenderingContext::Context2d(context) => context.update_rendering(),
        }
    }

    fn onscreen(&self) -> bool {
        match self {
            OffscreenRenderingContext::Context2d(context) => context.onscreen(),
        }
    }
}