// Copyright 2018-2026 the Deno authors. MIT license.
use std::cell::RefCell;
use std::rc::Rc;

use deno_core::GarbageCollected;
use deno_core::WebIDL;
use deno_core::cppgc::Ref;
use deno_core::op2;
use deno_core::v8;
use deno_core::webidl::WebIdlConverter;
use deno_error::JsErrorBox;
use deno_image::image::DynamicImage;
use deno_image::image::GenericImageView;
use wgpu_core::resource::TextureDescriptor;
use wgpu_types::CompositeAlphaMode;
use wgpu_types::Extent3d;
use wgpu_types::SurfaceConfiguration;
use wgpu_types::SurfaceStatus;

use crate::Instance;
use crate::device::GPUDevice;
use crate::error::GPUError;
use crate::texture::GPUTexture;
use crate::texture::GPUTextureFormat;
use crate::webidl::GPUTextureUsageFlags;

pub enum ContextData {
  Canvas(Rc<RefCell<DynamicImage>>),
  Surface(Rc<RefCell<SurfaceData>>),
}

pub struct SurfaceData {
  pub width: u32,
  pub height: u32,
  pub id: wgpu_core::id::SurfaceId,
  pub instance: Instance,
}

impl Drop for SurfaceData {
  fn drop(&mut self) {
    self.instance.surface_drop(self.id);
  }
}

pub enum Descriptor {
  Texture(TextureDescriptor<'static>),
  Surface(SurfaceConfiguration<Vec<wgpu_types::TextureFormat>>),
}

pub struct GPUCanvasContext {
  canvas: v8::Global<v8::Object>,
  data: ContextData,

  pub texture_descriptor: RefCell<Option<Descriptor>>,
  pub configuration: RefCell<Option<GPUCanvasConfiguration>>,
  pub configuration_obj: RefCell<Option<v8::Global<v8::Object>>>,

  pub current_texture: RefCell<Option<v8::Global<v8::Object>>>,
}

// SAFETY: we're sure this can be GCed
unsafe impl GarbageCollected for GPUCanvasContext {
  fn trace(&self, _visitor: &mut v8::cppgc::Visitor) {}

  fn get_name(&self) -> &'static std::ffi::CStr {
    c"GPUCanvasContext"
  }
}

#[op2]
impl GPUCanvasContext {
  #[getter]
  fn canvas(&self) -> v8::Global<v8::Object> {
    self.canvas.clone()
  }

  fn configure<'s>(
    &self,
    scope: &mut v8::PinScope<'s, '_>,
    #[webidl] configuration_value: v8::Local<'s, v8::Value>,
  ) -> Result<(), JsErrorBox> {
    let configuration = GPUCanvasConfiguration::convert(
      scope,
      configuration_value,
      "Failed to execute 'configure' on 'GPUCanvasContext'".into(),
      (|| "Argument 1".into()).into(),
      &Default::default(),
    )
    .map_err(JsErrorBox::from_err)?;
    let configuration_obj: v8::Local<v8::Object> = configuration_value
      .try_into()
      .map_err(|_| JsErrorBox::type_error("expected object"))?;
    let configuration_obj = v8::Global::new(scope, configuration_obj);
    if !matches!(
      configuration.format,
      GPUTextureFormat::Bgra8unorm
        | GPUTextureFormat::Rgba8unorm
        | GPUTextureFormat::Rgba16float
    ) {
      return Err(JsErrorBox::type_error(format!(
        "The format '{}' is not supported",
        configuration.format.as_str()
      )));
    }

    let descriptor = self.get_descriptor_for_configuration(&configuration)?;

    match &descriptor {
      Descriptor::Texture(_) => {}
      Descriptor::Surface(surface) => {
        let ContextData::Surface(surface_data) = &self.data else {
          unreachable!()
        };

        let err = configuration.device.instance.surface_configure(
          surface_data.borrow().id,
          configuration.device.id,
          surface,
        );
        configuration.device.error_handler.push_error(err);
      }
    }

    self.configuration.replace(Some(configuration));
    self.configuration_obj.replace(Some(configuration_obj));
    self.texture_descriptor.replace(Some(descriptor));

    Ok(())
  }

  #[fast]
  fn unconfigure(&self, scope: &mut v8::PinScope<'_, '_>) {
    self.configuration.take();
    self.configuration_obj.take();
    self.texture_descriptor.take();
    self.replace_drawing_buffer(scope);
  }

  fn get_configuration<'s>(
    &self,
    scope: &mut v8::PinScope<'s, '_>,
  ) -> Option<v8::Local<'s, v8::Object>> {
    self
      .configuration_obj
      .borrow()
      .as_ref()
      .map(|obj| v8::Local::new(scope, obj))
  }

  fn get_current_texture(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
  ) -> Result<v8::Global<v8::Object>, JsErrorBox> {
    let configuration = self.configuration.borrow();
    let configuration = configuration.as_ref().ok_or_else(|| {
      JsErrorBox::type_error("GPUCanvasContext has not been configured")
    })?;
    let texture_descriptor = self.texture_descriptor.borrow();
    let texture_descriptor = texture_descriptor.as_ref().unwrap();
    let device = &configuration.device;

    let mut current_texture = self.current_texture.borrow_mut();

    if let Some(texture) = current_texture.as_ref() {
      Ok(texture.clone())
    } else {
      let texture = match texture_descriptor {
        Descriptor::Texture(texture_descriptor) => {
          let (id, err) = device.instance.device_create_texture(
            device.id,
            texture_descriptor,
            None,
          );
          device.error_handler.push_error(err);

          GPUTexture {
            instance: device.instance.clone(),
            error_handler: device.error_handler.clone(),
            id,
            device_id: device.id,
            queue_id: device.queue,
            default_view_id: Default::default(),
            label: texture_descriptor.label.as_ref().unwrap().to_string(),
            size: texture_descriptor.size,
            mip_level_count: texture_descriptor.mip_level_count,
            sample_count: texture_descriptor.sample_count,
            dimension: crate::texture::GPUTextureDimension::D2,
            format: configuration.format.clone(),
            usage: configuration.usage,
          }
        }
        Descriptor::Surface(surface) => {
          let ContextData::Surface(surface_data) = &self.data else {
            unreachable!()
          };

          let output = configuration
            .device
            .instance
            .surface_get_current_texture(surface_data.borrow().id, None)
            .map_err(|e| JsErrorBox::generic(e.to_string()))?;

          match output.status {
            SurfaceStatus::Good | SurfaceStatus::Suboptimal => {
              let id = output.texture.unwrap();

              GPUTexture {
                instance: configuration.device.instance.clone(),
                error_handler: configuration.device.error_handler.clone(),
                id,
                device_id: configuration.device.id,
                queue_id: configuration.device.queue,
                default_view_id: Default::default(),
                label: "".to_string(),
                size: Extent3d {
                  width: surface.width,
                  height: surface.height,
                  depth_or_array_layers: 1,
                },
                mip_level_count: 0,
                sample_count: 0,
                dimension: crate::texture::GPUTextureDimension::D2,
                format: configuration.format.clone(),
                usage: configuration.usage,
              }
            }
            _ => return Err(JsErrorBox::generic("Invalid Surface Status")),
          }
        }
      };

      let texture_obj = deno_core::cppgc::make_cppgc_object(scope, texture);
      let texture_obj = v8::Global::new(scope, texture_obj);

      *current_texture = Some(texture_obj.clone());

      Ok(texture_obj)
    }
  }
}

impl GPUCanvasContext {
  fn get_descriptor_for_configuration(
    &self,
    configuration: &GPUCanvasConfiguration,
  ) -> Result<Descriptor, JsErrorBox> {
    let usage = configuration.usage.0;
    let view_formats = configuration
      .view_formats
      .clone()
      .into_iter()
      .map(Into::into)
      .collect();

    match &self.data {
      ContextData::Canvas(image) => {
        let (width, height) = image.borrow().dimensions();

        Ok(Descriptor::Texture(TextureDescriptor {
          label: Some("GPUCanvasContext".into()),
          size: Extent3d {
            width,
            height,
            depth_or_array_layers: 1,
          },
          mip_level_count: 1,
          sample_count: 1,
          dimension: wgpu_types::TextureDimension::D2,
          format: configuration.format.clone().into(),
          usage: usage | wgpu_types::TextureUsages::COPY_SRC,
          view_formats,
        }))
      }
      ContextData::Surface(surface_data) => {
        let SurfaceData { width, height, .. } = &*surface_data.borrow();

        Ok(Descriptor::Surface(SurfaceConfiguration {
          usage,
          format: configuration.format.clone().into(),
          width: *width,
          height: *height,
          present_mode: configuration
            .present_mode
            .clone()
            .map(Into::into)
            .unwrap_or_default(),
          desired_maximum_frame_latency: 2,
          alpha_mode: configuration.alpha_mode.clone().into(),
          view_formats,
        }))
      }
    }
  }

  pub fn copy_image_contents_to_canvas_data(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
  ) -> Result<(), JsErrorBox> {
    let ContextData::Canvas(image) = &self.data else {
      unreachable!()
    };

    let configuration = self.configuration.borrow();
    let Some(GPUCanvasConfiguration { device, .. }) = configuration.as_ref()
    else {
      image.replace_with(|image| {
        let (width, height) = image.dimensions();
        let image = deno_image::image::RgbaImage::new(width, height);
        DynamicImage::from(image)
      });

      return Ok(());
    };

    let texture_descriptor = self.texture_descriptor.borrow();

    if let Some(texture) = self.current_texture.borrow().as_ref() {
      let Descriptor::Texture(TextureDescriptor { size, .. }) =
        texture_descriptor.as_ref().unwrap()
      else {
        unreachable!()
      };

      let local = v8::Local::new(scope, texture).cast::<v8::Value>();
      let underlying_texture =
        deno_core::cppgc::try_unwrap_cppgc_object::<GPUTexture>(scope, local)
          .unwrap();

      let (command_encoder, err) =
        device.instance.device_create_command_encoder(
          device.id,
          &wgpu_types::CommandEncoderDescriptor {
            label: Some("GPUCanvasContext".into()),
          },
          None,
        );
      if let Some(err) = err {
        return Err(JsErrorBox::from_err(GPUError::from(err)));
      }

      let data = copy_texture_to_vec(
        &device.instance,
        device.id,
        device.queue,
        command_encoder,
        underlying_texture.id,
        size,
      )?;

      image.replace_with(|image| {
        let (width, height) = image.dimensions();
        let image =
          deno_image::image::RgbaImage::from_raw(width, height, data).unwrap();
        DynamicImage::from(image)
      });
    }

    Ok(())
  }

  fn expire_current_texture(&self, scope: &mut v8::PinScope<'_, '_>) {
    if let Some(texture) = self.current_texture.borrow().as_ref() {
      let local = v8::Local::new(scope, texture).cast::<v8::Value>();
      let underlying_texture =
        deno_core::cppgc::try_unwrap_cppgc_object::<GPUTexture>(scope, local)
          .unwrap();

      underlying_texture
        .instance
        .texture_destroy(underlying_texture.id);
    }
  }

  fn replace_drawing_buffer(&self, scope: &mut v8::PinScope<'_, '_>) {
    self.expire_current_texture(scope);
    self.current_texture.borrow_mut().take();
  }

  pub fn resize(&self, scope: &mut v8::PinScope<'_, '_>) {
    self.replace_drawing_buffer(scope);
    if let Some(configuration) = self.configuration.borrow().as_ref() {
      self.texture_descriptor.replace(Some(
        self
          .get_descriptor_for_configuration(configuration)
          .unwrap(),
      ));

      match &self.data {
        ContextData::Canvas(_) => {}
        ContextData::Surface(surface_data) => {
          let texture_descriptor = self.texture_descriptor.borrow();

          let Descriptor::Surface(descriptor) =
            texture_descriptor.as_ref().unwrap()
          else {
            unreachable!()
          };

          let err = configuration.device.instance.surface_configure(
            surface_data.borrow().id,
            configuration.device.id,
            descriptor,
          );

          configuration.device.error_handler.push_error(err);
        }
      }
    }
  }

  pub fn bitmap_read_hook(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
  ) -> Result<(), JsErrorBox> {
    self.copy_image_contents_to_canvas_data(scope)
  }

  pub fn post_transfer_to_image_bitmap_hook(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
  ) {
    self.replace_drawing_buffer(scope);
  }
}

#[derive(WebIDL)]
#[webidl(dictionary)]
pub struct GPUCanvasConfiguration {
  pub device: Ref<GPUDevice>,
  pub format: GPUTextureFormat,
  #[webidl(default = GPUTextureUsageFlags(wgpu_types::TextureUsages::RENDER_ATTACHMENT))]
  usage: GPUTextureUsageFlags,
  #[webidl(default = vec![])]
  pub view_formats: Vec<GPUTextureFormat>,
  pub tone_mapping: GPUCanvasToneMapping, // TODO(@crowlkats): support
  #[webidl(default = PredefinedColorSpace::Srgb)]
  pub color_space: PredefinedColorSpace, // TODO(@crowlkats): support
  #[webidl(default = GPUCanvasAlphaMode::Opaque)]
  pub alpha_mode: GPUCanvasAlphaMode,

  // Extended from spec
  pub present_mode: Option<GPUPresentMode>,
}

#[derive(WebIDL, Clone)]
#[webidl(dictionary)]
pub struct GPUCanvasToneMapping {
  #[webidl(default = GPUCanvasToneMappingMode::Standard)]
  pub mode: GPUCanvasToneMappingMode, // TODO(@crowlkats): support
}

#[derive(WebIDL, Clone)]
#[webidl(enum)]
pub enum GPUCanvasToneMappingMode {
  Standard,
  Extended,
}

#[derive(WebIDL, Clone)]
#[webidl(enum)]
pub enum PredefinedColorSpace {
  Srgb,
  SrgbLinear,
  DisplayP3,
  DisplayP3Linear,
}

#[derive(WebIDL, Clone)]
#[webidl(enum)]
pub enum GPUCanvasAlphaMode {
  Opaque,
  Premultiplied,
}

impl From<GPUCanvasAlphaMode> for CompositeAlphaMode {
  fn from(value: GPUCanvasAlphaMode) -> Self {
    match value {
      GPUCanvasAlphaMode::Opaque => CompositeAlphaMode::Opaque,
      GPUCanvasAlphaMode::Premultiplied => CompositeAlphaMode::PreMultiplied,
    }
  }
}

// Extended from spec
#[derive(WebIDL, Clone)]
#[webidl(enum)]
pub enum GPUPresentMode {
  AutoVsync,
  AutoNoVsync,
  Fifo,
  FifoRelaxed,
  Immediate,
  Mailbox,
}

impl From<GPUPresentMode> for wgpu_types::PresentMode {
  fn from(value: GPUPresentMode) -> Self {
    match value {
      GPUPresentMode::AutoVsync => Self::AutoVsync,
      GPUPresentMode::AutoNoVsync => Self::AutoNoVsync,
      GPUPresentMode::Fifo => Self::Fifo,
      GPUPresentMode::FifoRelaxed => Self::FifoRelaxed,
      GPUPresentMode::Immediate => Self::Immediate,
      GPUPresentMode::Mailbox => Self::Mailbox,
    }
  }
}

pub struct PaddedSize {
  pub padded_bytes_per_row: u32,
  pub unpadded_bytes_per_row: u32,
}

pub fn copy_texture_to_vec(
  instance: &Instance,
  device: wgpu_core::id::DeviceId,
  queue: wgpu_core::id::QueueId,
  command_encoder: wgpu_core::id::CommandEncoderId,
  texture: wgpu_core::id::TextureId,
  size: &Extent3d,
) -> Result<Vec<u8>, JsErrorBox> {
  // We only support the 8 bit per pixel formats with 4 channels
  // as such a pixel has 4 bytes
  const BYTES_PER_PIXEL: u32 = 4;

  let overflow =
    || JsErrorBox::range_error("Texture dimensions overflow buffer-size math");

  let unpadded_bytes_per_row = size
    .width
    .checked_mul(BYTES_PER_PIXEL)
    .ok_or_else(overflow)?;
  let padded_bytes_per_row_padding = (wgpu_types::COPY_BYTES_PER_ROW_ALIGNMENT
    - (unpadded_bytes_per_row % wgpu_types::COPY_BYTES_PER_ROW_ALIGNMENT))
    % wgpu_types::COPY_BYTES_PER_ROW_ALIGNMENT;
  let padded_bytes_per_row = unpadded_bytes_per_row
    .checked_add(padded_bytes_per_row_padding)
    .ok_or_else(overflow)?;

  let buffer_size = (padded_bytes_per_row as u64)
    .checked_mul(size.height as u64)
    .ok_or_else(overflow)?;
  let unpadded_total = (unpadded_bytes_per_row as u64)
    .checked_mul(size.height as u64)
    .ok_or_else(overflow)?;

  let (buffer, maybe_err) = instance.device_create_buffer(
    device,
    &wgpu_types::BufferDescriptor {
      label: None,
      size: buffer_size,
      usage: wgpu_types::BufferUsages::MAP_READ
        | wgpu_types::BufferUsages::COPY_DST,
      mapped_at_creation: false,
    },
    None,
  );

  if let Some(maybe_err) = maybe_err {
    return Err(JsErrorBox::from_err::<GPUError>(maybe_err.into()));
  }

  instance
    .command_encoder_copy_texture_to_buffer(
      command_encoder,
      &wgpu_types::TexelCopyTextureInfo {
        texture,
        mip_level: 0,
        origin: Default::default(),
        aspect: Default::default(),
      },
      &wgpu_types::TexelCopyBufferInfo {
        buffer,
        layout: wgpu_types::TexelCopyBufferLayout {
          offset: 0,
          bytes_per_row: Some(padded_bytes_per_row),
          rows_per_image: None,
        },
      },
      size,
    )
    .map_err(|e| JsErrorBox::from_err::<GPUError>(e.into()))?;

  let (command_buffer, maybe_err) = instance.command_encoder_finish(
    command_encoder,
    &wgpu_types::CommandBufferDescriptor { label: None },
    None,
  );
  if let Some((_, maybe_err)) = maybe_err {
    return Err(JsErrorBox::from_err::<GPUError>(maybe_err.into()));
  }

  let maybe_err = instance.queue_submit(queue, &[command_buffer]).err();
  if let Some((_, maybe_err)) = maybe_err {
    return Err(JsErrorBox::from_err::<GPUError>(maybe_err.into()));
  }

  let index = instance
    .buffer_map_async(
      buffer,
      0,
      None,
      wgpu_core::resource::BufferMapOperation {
        host: wgpu_core::device::HostMap::Read,
        callback: None,
      },
    )
    .map_err(|e| JsErrorBox::from_err::<GPUError>(e.into()))?;

  instance
    .device_poll(
      device,
      wgpu_types::PollType::Wait {
        submission_index: Some(index),
        timeout: None,
      },
    )
    .unwrap();

  let (slice_pointer, range_size) = instance
    .buffer_get_mapped_range(buffer, 0, None)
    .map_err(|e| JsErrorBox::from_err::<GPUError>(e.into()))?;

  let data = {
    // SAFETY: creating a slice from pointer and length provided by wgpu and
    // then dropping it before unmapping
    let slice = unsafe {
      std::slice::from_raw_parts(slice_pointer.as_ptr(), range_size as usize)
    };

    let mut unpadded = Vec::with_capacity(unpadded_total as usize);

    let padded = padded_bytes_per_row as usize;
    let unpadded_row = unpadded_bytes_per_row as usize;
    for i in 0..size.height as usize {
      let start = i * padded;
      unpadded.extend_from_slice(&slice[start..start + padded][..unpadded_row]);
    }

    unpadded
  };

  instance
    .buffer_unmap(buffer)
    .map_err(|e| JsErrorBox::from_err::<GPUError>(e.into()))?;
  instance.buffer_drop(buffer);

  Ok(data)
}

pub const CONTEXT_ID: &str = "webgpu";

pub fn create<'s>(
  _instance: Option<Instance>,
  canvas: v8::Global<v8::Object>,
  data: ContextData,
  scope: &mut v8::PinScope<'s, '_>,
  _options: v8::Local<'s, v8::Value>,
  _prefix: &'static str,
  _context: &'static str,
) -> Result<v8::Global<v8::Value>, JsErrorBox> {
  let obj = deno_core::cppgc::make_cppgc_object(
    scope,
    GPUCanvasContext {
      canvas,
      data,
      texture_descriptor: RefCell::new(None),
      configuration: RefCell::new(None),
      configuration_obj: RefCell::new(None),
      current_texture: RefCell::new(None),
    },
  );

  Ok(v8::Global::new(scope, obj.cast()))
}
