// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

use super::wgpu_types;
use crate::WebGpuQuerySet;
use deno_core::error::AnyError;
use deno_core::op2;
use deno_core::OpState;
use deno_core::Resource;
use deno_core::ResourceId;
use serde::Deserialize;
use std::borrow::Cow;
use std::cell::RefCell;
use std::rc::Rc;

use super::error::WebGpuResult;

pub(crate) struct WebGpuCommandEncoder(
    pub(crate) super::Instance,
    pub(crate) wgpu_core::id::CommandEncoderId, // TODO: should maybe be option?
);
impl Resource for WebGpuCommandEncoder {
    fn name(&self) -> Cow<str> {
        "webGPUCommandEncoder".into()
    }

    fn close(self: Rc<Self>) {
        self.0.command_encoder_drop(self.1);
    }
}

pub(crate) struct WebGpuCommandBuffer(
    pub(crate) super::Instance,
    pub(crate) RefCell<Option<wgpu_core::id::CommandBufferId>>,
);
impl Resource for WebGpuCommandBuffer {
    fn name(&self) -> Cow<str> {
        "webGPUCommandBuffer".into()
    }

    fn close(self: Rc<Self>) {
        if let Some(id) = *self.1.borrow() {
            self.0.command_buffer_drop(id);
        }
    }
}

#[op2]
#[serde]
pub fn op_webgpu_create_command_encoder(
    state: &mut OpState,
    #[smi] device_rid: ResourceId,
    #[string] label: Cow<str>,
) -> Result<WebGpuResult, AnyError> {
    let instance = state.borrow::<super::Instance>();
    let device_resource = state
        .resource_table
        .get::<super::WebGpuDevice>(device_rid)?;
    let device = device_resource.1;

    let descriptor = wgpu_types::CommandEncoderDescriptor { label: Some(label) };

    gfx_put!(instance.device_create_command_encoder(
    device,
    &descriptor,
    None
  ) => state, WebGpuCommandEncoder)
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GpuRenderPassColorAttachment {
    view: ResourceId,
    resolve_target: Option<ResourceId>,
    clear_value: Option<wgpu_types::Color>,
    load_op: LoadOp,
    store_op: wgpu_core::command::StoreOp,
}

#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LoadOp {
    /// Clear the output attachment with the clear color. Clearing is faster than loading.
    Clear = 0,
    /// Do not clear output attachment.
    Load = 1,
}

impl LoadOp {
    fn into_wgt<V>(self, clear: V) -> wgpu_types::LoadOp<V> {
        match self {
            LoadOp::Clear => wgpu_types::LoadOp::Clear(clear),
            LoadOp::Load => wgpu_types::LoadOp::Load,
        }
    }
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GpuRenderPassDepthStencilAttachment {
    view: ResourceId,
    depth_clear_value: Option<f32>,
    depth_load_op: Option<LoadOp>,
    depth_store_op: Option<wgpu_core::command::StoreOp>,
    depth_read_only: bool,
    stencil_clear_value: u32,
    stencil_load_op: Option<LoadOp>,
    stencil_store_op: Option<wgpu_core::command::StoreOp>,
    stencil_read_only: bool,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GPURenderPassTimestampWrites {
    query_set: ResourceId,
    beginning_of_pass_write_index: Option<u32>,
    end_of_pass_write_index: Option<u32>,
}

#[op2]
#[serde]
pub fn op_webgpu_command_encoder_begin_render_pass(
    state: &mut OpState,
    #[smi] command_encoder_rid: ResourceId,
    #[string] label: Cow<str>,
    #[serde] color_attachments: Vec<Option<GpuRenderPassColorAttachment>>,
    #[serde] depth_stencil_attachment: Option<GpuRenderPassDepthStencilAttachment>,
    #[smi] occlusion_query_set: Option<ResourceId>,
    #[serde] timestamp_writes: Option<GPURenderPassTimestampWrites>,
) -> Result<WebGpuResult, AnyError> {
    let command_encoder_resource = state
        .resource_table
        .get::<WebGpuCommandEncoder>(command_encoder_rid)?;

    let color_attachments = color_attachments
        .into_iter()
        .map(|color_attachment| {
            let rp_at = if let Some(at) = color_attachment.as_ref() {
                let texture_view_resource = state
                    .resource_table
                    .get::<super::texture::WebGpuTextureView>(at.view)?;

                let resolve_target = at
                    .resolve_target
                    .map(|rid| {
                        state
                            .resource_table
                            .get::<super::texture::WebGpuTextureView>(rid)
                    })
                    .transpose()?
                    .map(|texture| texture.1);

                Some(wgpu_core::command::RenderPassColorAttachment {
                    view: texture_view_resource.1,
                    resolve_target,
                    load_op: at.load_op.into_wgt(at.clear_value.unwrap_or_default()),
                    store_op: at.store_op,
                })
            } else {
                None
            };
            Ok(rp_at)
        })
        .collect::<Result<Vec<_>, AnyError>>()?;

    let mut processed_depth_stencil_attachment = None;

    if let Some(attachment) = depth_stencil_attachment {
        let texture_view_resource = state
            .resource_table
            .get::<super::texture::WebGpuTextureView>(attachment.view)?;

        processed_depth_stencil_attachment =
            Some(wgpu_core::command::RenderPassDepthStencilAttachment {
                view: texture_view_resource.1,
                depth: wgpu_core::command::PassChannel {
                    load_op: attachment
                        .depth_load_op
                        .map(|load_op| load_op.into_wgt(attachment.depth_clear_value)),
                    store_op: attachment.depth_store_op,
                    read_only: attachment.depth_read_only,
                },
                stencil: wgpu_core::command::PassChannel {
                    load_op: attachment
                        .stencil_load_op
                        .map(|load_op| load_op.into_wgt(Some(attachment.stencil_clear_value))),
                    store_op: attachment.stencil_store_op,
                    read_only: attachment.stencil_read_only,
                },
            });
    }

    let timestamp_writes = if let Some(timestamp_writes) = timestamp_writes {
        let query_set_resource = state
            .resource_table
            .get::<WebGpuQuerySet>(timestamp_writes.query_set)?;
        let query_set = query_set_resource.1;

        Some(wgpu_core::command::PassTimestampWrites {
            query_set,
            beginning_of_pass_write_index: timestamp_writes.beginning_of_pass_write_index,
            end_of_pass_write_index: timestamp_writes.end_of_pass_write_index,
        })
    } else {
        None
    };

    let occlusion_query_set_resource = occlusion_query_set
        .map(|rid| state.resource_table.get::<WebGpuQuerySet>(rid))
        .transpose()?
        .map(|query_set| query_set.1);

    let instance = state.borrow::<super::Instance>();
    let command_encoder = &command_encoder_resource.1;
    let descriptor = wgpu_core::command::RenderPassDescriptor {
        label: Some(label),
        color_attachments: Cow::from(color_attachments),
        depth_stencil_attachment: processed_depth_stencil_attachment.as_ref(),
        timestamp_writes: timestamp_writes.as_ref(),
        occlusion_query_set: occlusion_query_set_resource,
    };

    let (render_pass, error) =
        instance.command_encoder_create_render_pass(*command_encoder, &descriptor);
    let rid = state
        .resource_table
        .add(super::render_pass::WebGpuRenderPass(RefCell::new(
            render_pass,
        )));

    Ok(WebGpuResult::rid_err(rid, error))
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GPUComputePassTimestampWrites {
    query_set: ResourceId,
    beginning_of_pass_write_index: Option<u32>,
    end_of_pass_write_index: Option<u32>,
}

#[op2]
#[serde]
pub fn op_webgpu_command_encoder_begin_compute_pass(
    state: &mut OpState,
    #[smi] command_encoder_rid: ResourceId,
    #[string] label: Cow<str>,
    #[serde] timestamp_writes: Option<GPUComputePassTimestampWrites>,
) -> Result<WebGpuResult, AnyError> {
    let command_encoder_resource = state
        .resource_table
        .get::<WebGpuCommandEncoder>(command_encoder_rid)?;

    let timestamp_writes = if let Some(timestamp_writes) = timestamp_writes {
        let query_set_resource = state
            .resource_table
            .get::<WebGpuQuerySet>(timestamp_writes.query_set)?;
        let query_set = query_set_resource.1;

        Some(wgpu_core::command::PassTimestampWrites {
            query_set,
            beginning_of_pass_write_index: timestamp_writes.beginning_of_pass_write_index,
            end_of_pass_write_index: timestamp_writes.end_of_pass_write_index,
        })
    } else {
        None
    };

    let instance = state.borrow::<super::Instance>();
    let command_encoder = &command_encoder_resource.1;
    let descriptor = wgpu_core::command::ComputePassDescriptor {
        label: Some(label),
        timestamp_writes: timestamp_writes.as_ref(),
    };

    let (compute_pass, error) =
        instance.command_encoder_create_compute_pass(*command_encoder, &descriptor);
    let rid = state
        .resource_table
        .add(super::compute_pass::WebGpuComputePass(RefCell::new(
            compute_pass,
        )));

    Ok(WebGpuResult::rid_err(rid, error))
}

#[op2]
#[serde]
pub fn op_webgpu_command_encoder_copy_buffer_to_buffer(
    state: &mut OpState,
    #[smi] command_encoder_rid: ResourceId,
    #[smi] source: ResourceId,
    #[number] source_offset: u64,
    #[smi] destination: ResourceId,
    #[number] destination_offset: u64,
    #[number] size: u64,
) -> Result<WebGpuResult, AnyError> {
    let instance = state.borrow::<super::Instance>();
    let command_encoder_resource = state
        .resource_table
        .get::<WebGpuCommandEncoder>(command_encoder_rid)?;
    let command_encoder = command_encoder_resource.1;
    let source_buffer_resource = state
        .resource_table
        .get::<super::buffer::WebGpuBuffer>(source)?;
    let source_buffer = source_buffer_resource.1;
    let destination_buffer_resource = state
        .resource_table
        .get::<super::buffer::WebGpuBuffer>(destination)?;
    let destination_buffer = destination_buffer_resource.1;

    gfx_ok!(instance.command_encoder_copy_buffer_to_buffer(
        command_encoder,
        source_buffer,
        source_offset,
        destination_buffer,
        destination_offset,
        size
    ))
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GpuTexelCopyBufferInfo {
    buffer: ResourceId,
    offset: u64,
    bytes_per_row: Option<u32>,
    rows_per_image: Option<u32>,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GpuTexelCopyTextureInfo {
    pub texture: ResourceId,
    pub mip_level: u32,
    pub origin: wgpu_types::Origin3d,
    pub aspect: wgpu_types::TextureAspect,
}

#[op2]
#[serde]
pub fn op_webgpu_command_encoder_copy_buffer_to_texture(
    state: &mut OpState,
    #[smi] command_encoder_rid: ResourceId,
    #[serde] source: GpuTexelCopyBufferInfo,
    #[serde] destination: GpuTexelCopyTextureInfo,
    #[serde] copy_size: wgpu_types::Extent3d,
) -> Result<WebGpuResult, AnyError> {
    let instance = state.borrow::<super::Instance>();
    let command_encoder_resource = state
        .resource_table
        .get::<WebGpuCommandEncoder>(command_encoder_rid)?;
    let command_encoder = command_encoder_resource.1;
    let source_buffer_resource = state
        .resource_table
        .get::<super::buffer::WebGpuBuffer>(source.buffer)?;
    let destination_texture_resource = state
        .resource_table
        .get::<super::texture::WebGpuTexture>(destination.texture)?;

    let source = wgpu_core::command::TexelCopyBufferInfo {
        buffer: source_buffer_resource.1,
        layout: wgpu_types::TexelCopyBufferLayout {
            offset: source.offset,
            bytes_per_row: source.bytes_per_row,
            rows_per_image: source.rows_per_image,
        },
    };
    let destination = wgpu_core::command::TexelCopyTextureInfo {
        texture: destination_texture_resource.id,
        mip_level: destination.mip_level,
        origin: destination.origin,
        aspect: destination.aspect,
    };
    gfx_ok!(instance.command_encoder_copy_buffer_to_texture(
        command_encoder,
        &source,
        &destination,
        &copy_size
    ))
}

#[op2]
#[serde]
pub fn op_webgpu_command_encoder_copy_texture_to_buffer(
    state: &mut OpState,
    #[smi] command_encoder_rid: ResourceId,
    #[serde] source: GpuTexelCopyTextureInfo,
    #[serde] destination: GpuTexelCopyBufferInfo,
    #[serde] copy_size: wgpu_types::Extent3d,
) -> Result<WebGpuResult, AnyError> {
    let instance = state.borrow::<super::Instance>();
    let command_encoder_resource = state
        .resource_table
        .get::<WebGpuCommandEncoder>(command_encoder_rid)?;
    let command_encoder = command_encoder_resource.1;
    let source_texture_resource = state
        .resource_table
        .get::<super::texture::WebGpuTexture>(source.texture)?;
    let destination_buffer_resource = state
        .resource_table
        .get::<super::buffer::WebGpuBuffer>(destination.buffer)?;

    let source = wgpu_core::command::TexelCopyTextureInfo {
        texture: source_texture_resource.id,
        mip_level: source.mip_level,
        origin: source.origin,
        aspect: source.aspect,
    };
    let destination = wgpu_core::command::TexelCopyBufferInfo {
        buffer: destination_buffer_resource.1,
        layout: wgpu_types::TexelCopyBufferLayout {
            offset: destination.offset,
            bytes_per_row: destination.bytes_per_row,
            rows_per_image: destination.rows_per_image,
        },
    };
    gfx_ok!(instance.command_encoder_copy_texture_to_buffer(
        command_encoder,
        &source,
        &destination,
        &copy_size
    ))
}

#[op2]
#[serde]
pub fn op_webgpu_command_encoder_copy_texture_to_texture(
    state: &mut OpState,
    #[smi] command_encoder_rid: ResourceId,
    #[serde] source: GpuTexelCopyTextureInfo,
    #[serde] destination: GpuTexelCopyTextureInfo,
    #[serde] copy_size: wgpu_types::Extent3d,
) -> Result<WebGpuResult, AnyError> {
    let instance = state.borrow::<super::Instance>();
    let command_encoder_resource = state
        .resource_table
        .get::<WebGpuCommandEncoder>(command_encoder_rid)?;
    let command_encoder = command_encoder_resource.1;
    let source_texture_resource = state
        .resource_table
        .get::<super::texture::WebGpuTexture>(source.texture)?;
    let destination_texture_resource = state
        .resource_table
        .get::<super::texture::WebGpuTexture>(destination.texture)?;

    let source = wgpu_core::command::TexelCopyTextureInfo {
        texture: source_texture_resource.id,
        mip_level: source.mip_level,
        origin: source.origin,
        aspect: source.aspect,
    };
    let destination = wgpu_core::command::TexelCopyTextureInfo {
        texture: destination_texture_resource.id,
        mip_level: destination.mip_level,
        origin: destination.origin,
        aspect: destination.aspect,
    };
    gfx_ok!(instance.command_encoder_copy_texture_to_texture(
        command_encoder,
        &source,
        &destination,
        &copy_size
    ))
}

#[op2]
#[serde]
pub fn op_webgpu_command_encoder_clear_buffer(
    state: &mut OpState,
    #[smi] command_encoder_rid: ResourceId,
    #[smi] buffer_rid: ResourceId,
    #[number] offset: u64,
    #[number] size: u64,
) -> Result<WebGpuResult, AnyError> {
    let instance = state.borrow::<super::Instance>();
    let command_encoder_resource = state
        .resource_table
        .get::<WebGpuCommandEncoder>(command_encoder_rid)?;
    let command_encoder = command_encoder_resource.1;
    let destination_resource = state
        .resource_table
        .get::<super::buffer::WebGpuBuffer>(buffer_rid)?;

    gfx_ok!(instance.command_encoder_clear_buffer(
        command_encoder,
        destination_resource.1,
        offset,
        Some(size)
    ))
}

#[op2]
#[serde]
pub fn op_webgpu_command_encoder_push_debug_group(
    state: &mut OpState,
    #[smi] command_encoder_rid: ResourceId,
    #[string] group_label: &str,
) -> Result<WebGpuResult, AnyError> {
    let instance = state.borrow::<super::Instance>();
    let command_encoder_resource = state
        .resource_table
        .get::<WebGpuCommandEncoder>(command_encoder_rid)?;
    let command_encoder = command_encoder_resource.1;

    gfx_ok!(instance.command_encoder_push_debug_group(command_encoder, group_label))
}

#[op2]
#[serde]
pub fn op_webgpu_command_encoder_pop_debug_group(
    state: &mut OpState,
    #[smi] command_encoder_rid: ResourceId,
) -> Result<WebGpuResult, AnyError> {
    let instance = state.borrow::<super::Instance>();
    let command_encoder_resource = state
        .resource_table
        .get::<WebGpuCommandEncoder>(command_encoder_rid)?;
    let command_encoder = command_encoder_resource.1;

    gfx_ok!(instance.command_encoder_pop_debug_group(command_encoder))
}

#[op2]
#[serde]
pub fn op_webgpu_command_encoder_insert_debug_marker(
    state: &mut OpState,
    #[smi] command_encoder_rid: ResourceId,
    #[string] marker_label: &str,
) -> Result<WebGpuResult, AnyError> {
    let instance = state.borrow::<super::Instance>();
    let command_encoder_resource = state
        .resource_table
        .get::<WebGpuCommandEncoder>(command_encoder_rid)?;
    let command_encoder = command_encoder_resource.1;

    gfx_ok!(instance.command_encoder_insert_debug_marker(command_encoder, marker_label))
}

#[op2]
#[serde]
pub fn op_webgpu_command_encoder_write_timestamp(
    state: &mut OpState,
    #[smi] command_encoder_rid: ResourceId,
    #[smi] query_set: ResourceId,
    query_index: u32,
) -> Result<WebGpuResult, AnyError> {
    let instance = state.borrow::<super::Instance>();
    let command_encoder_resource = state
        .resource_table
        .get::<WebGpuCommandEncoder>(command_encoder_rid)?;
    let command_encoder = command_encoder_resource.1;
    let query_set_resource = state
        .resource_table
        .get::<super::WebGpuQuerySet>(query_set)?;

    gfx_ok!(instance.command_encoder_write_timestamp(
        command_encoder,
        query_set_resource.1,
        query_index
    ))
}

#[op2]
#[serde]
pub fn op_webgpu_command_encoder_resolve_query_set(
    state: &mut OpState,
    #[smi] command_encoder_rid: ResourceId,
    #[smi] query_set: ResourceId,
    first_query: u32,
    query_count: u32,
    #[smi] destination: ResourceId,
    #[number] destination_offset: u64,
) -> Result<WebGpuResult, AnyError> {
    let instance = state.borrow::<super::Instance>();
    let command_encoder_resource = state
        .resource_table
        .get::<WebGpuCommandEncoder>(command_encoder_rid)?;
    let command_encoder = command_encoder_resource.1;
    let query_set_resource = state
        .resource_table
        .get::<super::WebGpuQuerySet>(query_set)?;
    let destination_resource = state
        .resource_table
        .get::<super::buffer::WebGpuBuffer>(destination)?;

    gfx_ok!(instance.command_encoder_resolve_query_set(
        command_encoder,
        query_set_resource.1,
        first_query,
        query_count,
        destination_resource.1,
        destination_offset
    ))
}

#[op2]
#[serde]
pub fn op_webgpu_command_encoder_finish(
    state: &mut OpState,
    #[smi] command_encoder_rid: ResourceId,
    #[string] label: Cow<str>,
) -> Result<WebGpuResult, AnyError> {
    let command_encoder_resource = state
        .resource_table
        .take::<WebGpuCommandEncoder>(command_encoder_rid)?;
    let command_encoder = command_encoder_resource.1;
    let instance = state.borrow::<super::Instance>();

    let descriptor = wgpu_types::CommandBufferDescriptor { label: Some(label) };

    let (val, maybe_err) = instance.command_encoder_finish(command_encoder, &descriptor);

    let rid = state.resource_table.add(WebGpuCommandBuffer(
        instance.clone(),
        RefCell::new(Some(val)),
    ));

    Ok(WebGpuResult::rid_err(rid, maybe_err))
}
