// Copyright 2018-2026 the Deno authors. MIT license.

use std::ops::BitOr;
use std::rc::Rc;

use deno_core::GarbageCollected;
use deno_core::OpState;
use deno_core::V8TaskSpawner;
use deno_core::WebIDL;
use deno_core::cppgc::SameObject;
use deno_core::op2;
use deno_core::v8;

use super::device::DEVICE_EXTERNAL_MEMORY_SIZE;
use super::device::GPUDevice;
use super::queue::GPUQueue;
use crate::Instance;
use crate::error::GPUGenericError;
use crate::webidl::GPUFeatureName;

#[derive(WebIDL)]
#[webidl(dictionary)]
pub struct GPURequestAdapterOptions {
  #[webidl(default = "core".into())]
  pub feature_level: String,
  pub power_preference: Option<GPUPowerPreference>,
  #[webidl(default = false)]
  pub force_fallback_adapter: bool,
}

#[derive(WebIDL)]
#[webidl(enum)]
pub enum GPUPowerPreference {
  LowPower,
  HighPerformance,
}

#[derive(WebIDL)]
#[webidl(dictionary)]
struct GPUDeviceDescriptor {
  #[webidl(default = String::new())]
  label: String,

  #[webidl(default = vec![])]
  required_features: Vec<GPUFeatureName>,
  #[webidl(default = Default::default())]
  #[options(enforce_range = true)]
  required_limits: indexmap::IndexMap<String, Option<u64>>,
}

pub struct GPUAdapter {
  pub instance: Instance,
  pub id: wgpu_core::id::AdapterId,

  pub features: SameObject<GPUSupportedFeatures>,
  pub limits: SameObject<GPUSupportedLimits>,
  pub info: Rc<SameObject<GPUAdapterInfo>>,
}

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

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

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

#[op2]
impl GPUAdapter {
  #[constructor]
  #[cppgc]
  fn constructor(_: bool) -> Result<GPUAdapter, GPUGenericError> {
    Err(GPUGenericError::InvalidConstructor)
  }

  #[getter]
  fn info(&self, scope: &mut v8::PinScope<'_, '_>) -> v8::Global<v8::Object> {
    self.info.get(scope, |_| {
      let info = self.instance.adapter_get_info(self.id);

      GPUAdapterInfo { info }
    })
  }

  #[getter]
  fn features(
    &self,
    scope: &mut v8::PinScope<'_, '_>,
  ) -> v8::Global<v8::Object> {
    self.features.get(scope, |scope| {
      let features = self.instance.adapter_features(self.id);
      // Only expose WebGPU features, not wgpu native-only features
      let features = features & wgpu_types::Features::all_webgpu_mask();
      GPUSupportedFeatures::new(scope, features)
    })
  }

  #[getter]
  fn limits(&self, scope: &mut v8::PinScope<'_, '_>) -> v8::Global<v8::Object> {
    self.limits.get(scope, |_| {
      let adapter_limits = self.instance.adapter_limits(self.id);
      GPUSupportedLimits(adapter_limits)
    })
  }

  #[async_method(fake)]
  fn request_device(
    &self,
    state: &mut OpState,
    scope: &mut v8::PinScope<'_, '_>,
    #[webidl] descriptor: GPUDeviceDescriptor,
  ) -> Result<v8::Global<v8::Value>, CreateDeviceError> {
    let supported_features = self.instance.adapter_features(self.id);
    let required_features = descriptor
      .required_features
      .iter()
      .map(|f| wgpu_types::Features::from(*f))
      .fold(wgpu_types::Features::empty(), BitOr::bitor);

    // External textures are a required part of WebGPU, and `external-texture`
    // is not a WebGPU-defined feature. `wgpu` has it behind a feature for now,
    // because support is not complete. Allow applications to request that
    // feature even though it is not reported as an adapter-supported feature.
    //
    // There is probably not anything useful that Deno applications can do with
    // external textures, but it is useful to be able to enable it in
    // `cts_runner`.
    if !required_features
      .difference(supported_features | wgpu_types::Features::EXTERNAL_TEXTURE)
      .is_empty()
    {
      return Err(CreateDeviceError::RequiredFeaturesNotASubset);
    }

    // When support for compatibility mode is added, this will need to look
    // at whether the adapter is "compatibility-defaulting" or
    // "core-defaulting", and choose the appropriate set of defaults.
    //
    // Support for compatibility mode is tracked in
    // https://github.com/gfx-rs/wgpu/issues/8124.
    let required_limits = serde_json::from_value::<wgpu_types::Limits>(
      serde_json::to_value(descriptor.required_limits)?,
    )?
    .or_better_values_from(&wgpu_types::Limits::default());

    let trace = std::env::var_os("DENO_WEBGPU_TRACE")
      .map(|path| wgpu_types::Trace::Directory(std::path::PathBuf::from(path)))
      .unwrap_or_default();

    let wgpu_descriptor = wgpu_types::DeviceDescriptor {
      label: crate::transform_label(descriptor.label.clone()),
      required_features,
      required_limits,
      experimental_features: wgpu_types::ExperimentalFeatures::disabled(),
      memory_hints: Default::default(),
      trace,
    };

    let (device, queue) = self.instance.adapter_request_device(
      self.id,
      &wgpu_descriptor,
      None,
      None,
    )?;

    // Associate external memory with the device to encourage V8 to garbage
    // collect devices promptly.
    scope
      .adjust_amount_of_external_allocated_memory(DEVICE_EXTERNAL_MEMORY_SIZE);

    let spawner = state.borrow::<V8TaskSpawner>().clone();
    let lost_resolver = v8::PromiseResolver::new(scope).unwrap();
    let lost_promise = lost_resolver.get_promise(scope);
    let error_handler = Rc::new(super::error::DeviceErrorHandler::new(
      v8::Global::new(scope, lost_resolver),
      spawner,
    ));

    // Create the queue object eagerly so that the wgpu-core queue resource
    // gets cleaned up when the device is garbage collected, even if JS code
    // never accesses the queue property.
    let queue_obj = deno_core::cppgc::make_cppgc_object(
      scope,
      GPUQueue {
        instance: self.instance.clone(),
        error_handler: error_handler.clone(),
        label: descriptor.label.clone(),
        id: queue,
        device,
      },
    );
    let queue_obj = v8::Global::new(scope, queue_obj);

    let device = GPUDevice {
      instance: self.instance.clone(),
      id: device,
      label: descriptor.label,
      queue_obj,
      adapter_info: self.info.clone(),
      error_handler,
      adapter: self.id,
      queue,
      lost_promise: v8::Global::new(scope, lost_promise),
      limits: SameObject::new(),
      features: SameObject::new(),
      weak: std::sync::OnceLock::new(),
      has_active_capture: std::cell::RefCell::new(false),
    };
    let device = deno_core::cppgc::make_cppgc_object(scope, device);
    let weak_device = v8::Weak::new(scope, device);
    let event_target_setup = state.borrow::<crate::EventTargetSetup>();
    let webidl_brand = v8::Local::new(scope, event_target_setup.brand.clone());
    device.set(scope, webidl_brand, webidl_brand);
    let set_event_target_data =
      v8::Local::new(scope, event_target_setup.set_event_target_data.clone())
        .cast::<v8::Function>();
    let null = v8::null(scope);
    set_event_target_data.call(scope, null.into(), &[device.into()]);

    let finalizer = v8::Weak::with_finalizer(
      scope,
      device,
      Box::new(move |isolate| {
        isolate.adjust_amount_of_external_allocated_memory(
          -DEVICE_EXTERNAL_MEMORY_SIZE,
        );
      }),
    );

    // Now that the device is fully constructed, give the error handler a
    // weak reference to it, and store the finalizer weak reference.
    let device = device.cast::<v8::Value>();
    let device_ref =
      deno_core::cppgc::try_unwrap_cppgc_object::<GPUDevice>(scope, device)
        .unwrap();
    device_ref.error_handler.set_device(weak_device);
    device_ref.weak.set(finalizer).unwrap();

    Ok(v8::Global::new(scope, device))
  }
}

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum CreateDeviceError {
  #[class(type)]
  #[error("requiredFeatures must be a subset of the adapter features")]
  RequiredFeaturesNotASubset,
  #[class(inherit)]
  #[error(transparent)]
  Serde(#[from] serde_json::Error),
  #[class("DOMExceptionOperationError")]
  #[error(transparent)]
  Device(#[from] wgpu_core::instance::RequestDeviceError),
}

pub struct GPUSupportedLimits(pub wgpu_types::Limits);

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

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

#[op2]
impl GPUSupportedLimits {
  #[constructor]
  #[cppgc]
  fn constructor(_: bool) -> Result<GPUSupportedLimits, GPUGenericError> {
    Err(GPUGenericError::InvalidConstructor)
  }

  #[getter]
  fn maxTextureDimension1D(&self) -> u32 {
    self.0.max_texture_dimension_1d
  }

  #[getter]
  fn maxTextureDimension2D(&self) -> u32 {
    self.0.max_texture_dimension_2d
  }

  #[getter]
  fn maxTextureDimension3D(&self) -> u32 {
    self.0.max_texture_dimension_3d
  }

  #[getter]
  fn maxTextureArrayLayers(&self) -> u32 {
    self.0.max_texture_array_layers
  }

  #[getter]
  fn maxBindGroups(&self) -> u32 {
    self.0.max_bind_groups
  }

  // TODO(@crowlKats): support max_bind_groups_plus_vertex_buffers

  #[getter]
  fn maxBindingsPerBindGroup(&self) -> u32 {
    self.0.max_bindings_per_bind_group
  }

  #[getter]
  fn maxDynamicUniformBuffersPerPipelineLayout(&self) -> u32 {
    self.0.max_dynamic_uniform_buffers_per_pipeline_layout
  }

  #[getter]
  fn maxDynamicStorageBuffersPerPipelineLayout(&self) -> u32 {
    self.0.max_dynamic_storage_buffers_per_pipeline_layout
  }

  #[getter]
  fn maxSampledTexturesPerShaderStage(&self) -> u32 {
    self.0.max_sampled_textures_per_shader_stage
  }

  #[getter]
  fn maxSamplersPerShaderStage(&self) -> u32 {
    self.0.max_samplers_per_shader_stage
  }

  #[getter]
  fn maxStorageBuffersPerShaderStage(&self) -> u32 {
    self.0.max_storage_buffers_per_shader_stage
  }

  #[getter]
  fn maxStorageBuffersInVertexStage(&self) -> u32 {
    // TODO(https://github.com/gfx-rs/wgpu/issues/8748): InVertexStage limit
    // not implemented; return the PerShaderStage limit.
    self.0.max_storage_buffers_per_shader_stage
  }

  #[getter]
  fn maxStorageBuffersInFragmentStage(&self) -> u32 {
    // TODO(https://github.com/gfx-rs/wgpu/issues/8748): InFragmentStage limit
    // not implemented; return the PerShaderStage limit.
    self.0.max_storage_buffers_per_shader_stage
  }

  #[getter]
  fn maxStorageTexturesPerShaderStage(&self) -> u32 {
    self.0.max_storage_textures_per_shader_stage
  }

  #[getter]
  fn maxStorageTexturesInVertexStage(&self) -> u32 {
    // TODO(https://github.com/gfx-rs/wgpu/issues/8748): InVertexStage limit
    // not implemented; return the PerShaderStage limit.
    self.0.max_storage_textures_per_shader_stage
  }

  #[getter]
  fn maxStorageTexturesInFragmentStage(&self) -> u32 {
    // TODO(https://github.com/gfx-rs/wgpu/issues/8748): InFragmentStage limit
    // not implemented; return the PerShaderStage limit.
    self.0.max_storage_textures_per_shader_stage
  }

  #[getter]
  fn maxUniformBuffersPerShaderStage(&self) -> u32 {
    self.0.max_uniform_buffers_per_shader_stage
  }

  #[getter]
  #[number]
  fn maxUniformBufferBindingSize(&self) -> u64 {
    self.0.max_uniform_buffer_binding_size
  }

  #[getter]
  #[number]
  fn maxStorageBufferBindingSize(&self) -> u64 {
    self.0.max_storage_buffer_binding_size
  }

  #[getter]
  fn minUniformBufferOffsetAlignment(&self) -> u32 {
    self.0.min_uniform_buffer_offset_alignment
  }

  #[getter]
  fn minStorageBufferOffsetAlignment(&self) -> u32 {
    self.0.min_storage_buffer_offset_alignment
  }

  #[getter]
  fn maxVertexBuffers(&self) -> u32 {
    self.0.max_vertex_buffers
  }

  #[getter]
  #[number]
  fn maxBufferSize(&self) -> u64 {
    self.0.max_buffer_size
  }

  #[getter]
  fn maxVertexAttributes(&self) -> u32 {
    self.0.max_vertex_attributes
  }

  #[getter]
  fn maxVertexBufferArrayStride(&self) -> u32 {
    self.0.max_vertex_buffer_array_stride
  }

  #[getter]
  fn maxInterStageShaderVariables(&self) -> u32 {
    self.0.max_inter_stage_shader_variables
  }

  #[getter]
  fn maxColorAttachments(&self) -> u32 {
    self.0.max_color_attachments
  }

  #[getter]
  fn maxColorAttachmentBytesPerSample(&self) -> u32 {
    self.0.max_color_attachment_bytes_per_sample
  }

  #[getter]
  fn maxComputeWorkgroupStorageSize(&self) -> u32 {
    self.0.max_compute_workgroup_storage_size
  }

  #[getter]
  fn maxComputeInvocationsPerWorkgroup(&self) -> u32 {
    self.0.max_compute_invocations_per_workgroup
  }

  #[getter]
  fn maxComputeWorkgroupSizeX(&self) -> u32 {
    self.0.max_compute_workgroup_size_x
  }

  #[getter]
  fn maxComputeWorkgroupSizeY(&self) -> u32 {
    self.0.max_compute_workgroup_size_y
  }

  #[getter]
  fn maxComputeWorkgroupSizeZ(&self) -> u32 {
    self.0.max_compute_workgroup_size_z
  }

  #[getter]
  fn maxComputeWorkgroupsPerDimension(&self) -> u32 {
    self.0.max_compute_workgroups_per_dimension
  }
}

pub struct GPUSupportedFeatures(v8::Global<v8::Value>);

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

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

impl GPUSupportedFeatures {
  pub fn new(
    scope: &mut v8::PinScope<'_, '_>,
    features: wgpu_types::Features,
  ) -> Self {
    let set = v8::Set::new(scope);

    for feature in features.iter() {
      let key = v8::String::new(scope, feature.as_str().unwrap()).unwrap();
      set.add(scope, key.into());
    }

    Self(v8::Global::new(scope, <v8::Local<v8::Value>>::from(set)))
  }
}

#[op2]
impl GPUSupportedFeatures {
  #[constructor]
  #[cppgc]
  fn constructor(_: bool) -> Result<GPUSupportedFeatures, GPUGenericError> {
    Err(GPUGenericError::InvalidConstructor)
  }

  #[symbol("setlike_set")]
  fn set(&self) -> v8::Global<v8::Value> {
    self.0.clone()
  }
}

pub struct GPUAdapterInfo {
  pub info: wgpu_types::AdapterInfo,
}

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

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

#[op2]
impl GPUAdapterInfo {
  #[constructor]
  #[cppgc]
  fn constructor(_: bool) -> Result<GPUAdapterInfo, GPUGenericError> {
    Err(GPUGenericError::InvalidConstructor)
  }

  #[getter]
  #[string]
  fn vendor(&self) -> String {
    self.info.vendor.to_string()
  }

  #[getter]
  #[string]
  fn architecture(&self) -> &'static str {
    "" // TODO(https://github.com/gfx-rs/wgpu/issues/8649): implement when wgpu has architecture detection
  }

  #[getter]
  #[string]
  fn device(&self) -> String {
    self.info.device.to_string()
  }

  #[getter]
  #[string]
  fn description(&self) -> String {
    self.info.name.clone()
  }

  #[getter]
  fn subgroup_min_size(&self) -> u32 {
    self.info.subgroup_min_size
  }

  #[getter]
  fn subgroup_max_size(&self) -> u32 {
    self.info.subgroup_max_size
  }

  #[getter]
  fn is_fallback_adapter(&self) -> bool {
    self.info.device_type == wgpu_types::DeviceType::Cpu
  }
}
