use alloc::{
    borrow::{Cow, ToOwned},
    boxed::Box,
    string::String,
    sync::{Arc, Weak},
    vec::Vec,
};
use core::{fmt, mem::ManuallyDrop, ops::Range};

use arrayvec::ArrayVec;
use thiserror::Error;

#[cfg(feature = "serde")]
use serde::Deserialize;
#[cfg(feature = "serde")]
use serde::Serialize;

use crate::{
    device::{
        bgl, Device, DeviceError, MissingDownlevelFlags, MissingFeatures, SHADER_STAGE_COUNT,
    },
    id::{BindGroupLayoutId, BufferId, SamplerId, TextureViewId, TlasId},
    init_tracker::{BufferInitTrackerAction, TextureInitTrackerAction},
    pipeline::{ComputePipeline, RenderPipeline},
    resource::{
        Buffer, DestroyedResourceError, InvalidResourceError, Labeled, MissingBufferUsageError,
        MissingTextureUsageError, ResourceErrorIdent, Sampler, TextureView, Tlas, TrackingData,
    },
    resource_log,
    snatch::{SnatchGuard, Snatchable},
    track::{BindGroupStates, ResourceUsageCompatibilityError},
    Label,
};

#[derive(Clone, Debug, Error)]
#[non_exhaustive]
pub enum BindGroupLayoutEntryError {
    #[error("Cube dimension is not expected for texture storage")]
    StorageTextureCube,
    #[error("Atomic storage textures are not allowed by baseline webgpu, they require the native only feature TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES")]
    StorageTextureAtomic,
    #[error("Arrays of bindings unsupported for this type of binding")]
    ArrayUnsupported,
    #[error("Multisampled binding with sample type `TextureSampleType::Float` must have filterable set to false.")]
    SampleTypeFloatFilterableBindingMultisampled,
    #[error("Multisampled texture binding view dimension must be 2d, got {0:?}")]
    Non2DMultisampled(wgt::TextureViewDimension),
    #[error(transparent)]
    MissingFeatures(#[from] MissingFeatures),
    #[error(transparent)]
    MissingDownlevelFlags(#[from] MissingDownlevelFlags),
}

#[derive(Clone, Debug, Error)]
#[non_exhaustive]
pub enum CreateBindGroupLayoutError {
    #[error(transparent)]
    Device(#[from] DeviceError),
    #[error("Conflicting binding at index {0}")]
    ConflictBinding(u32),
    #[error("Binding {binding} entry is invalid")]
    Entry {
        binding: u32,
        #[source]
        error: BindGroupLayoutEntryError,
    },
    #[error(transparent)]
    TooManyBindings(BindingTypeMaxCountError),
    #[error("Bind groups may not contain both a binding array and a dynamically offset buffer")]
    ContainsBothBindingArrayAndDynamicOffsetArray,
    #[error("Bind groups may not contain both a binding array and a uniform buffer")]
    ContainsBothBindingArrayAndUniformBuffer,
    #[error("Binding index {binding} is greater than the maximum number {maximum}")]
    InvalidBindingIndex { binding: u32, maximum: u32 },
    #[error("Invalid visibility {0:?}")]
    InvalidVisibility(wgt::ShaderStages),
}

//TODO: refactor this to move out `enum BindingError`.

#[derive(Clone, Debug, Error)]
#[non_exhaustive]
pub enum CreateBindGroupError {
    #[error(transparent)]
    Device(#[from] DeviceError),
    #[error(transparent)]
    DestroyedResource(#[from] DestroyedResourceError),
    #[error(
        "Binding count declared with at most {expected} items, but {actual} items were provided"
    )]
    BindingArrayPartialLengthMismatch { actual: usize, expected: usize },
    #[error(
        "Binding count declared with exactly {expected} items, but {actual} items were provided"
    )]
    BindingArrayLengthMismatch { actual: usize, expected: usize },
    #[error("Array binding provided zero elements")]
    BindingArrayZeroLength,
    #[error("The bound range {range:?} of {buffer} overflows its size ({size})")]
    BindingRangeTooLarge {
        buffer: ResourceErrorIdent,
        range: Range<wgt::BufferAddress>,
        size: u64,
    },
    #[error("Binding size {actual} of {buffer} is less than minimum {min}")]
    BindingSizeTooSmall {
        buffer: ResourceErrorIdent,
        actual: u64,
        min: u64,
    },
    #[error("{0} binding size is zero")]
    BindingZeroSize(ResourceErrorIdent),
    #[error("Number of bindings in bind group descriptor ({actual}) does not match the number of bindings defined in the bind group layout ({expected})")]
    BindingsNumMismatch { actual: usize, expected: usize },
    #[error("Binding {0} is used at least twice in the descriptor")]
    DuplicateBinding(u32),
    #[error("Unable to find a corresponding declaration for the given binding {0}")]
    MissingBindingDeclaration(u32),
    #[error(transparent)]
    MissingBufferUsage(#[from] MissingBufferUsageError),
    #[error(transparent)]
    MissingTextureUsage(#[from] MissingTextureUsageError),
    #[error("Binding declared as a single item, but bind group is using it as an array")]
    SingleBindingExpected,
    #[error("Buffer offset {0} does not respect device's requested `{1}` limit {2}")]
    UnalignedBufferOffset(wgt::BufferAddress, &'static str, u32),
    #[error(
        "Buffer binding {binding} range {given} exceeds `max_*_buffer_binding_size` limit {limit}"
    )]
    BufferRangeTooLarge {
        binding: u32,
        given: u32,
        limit: u32,
    },
    #[error("Binding {binding} has a different type ({actual:?}) than the one in the layout ({expected:?})")]
    WrongBindingType {
        // Index of the binding
        binding: u32,
        // The type given to the function
        actual: wgt::BindingType,
        // Human-readable description of expected types
        expected: &'static str,
    },
    #[error("Texture binding {binding} expects multisampled = {layout_multisampled}, but given a view with samples = {view_samples}")]
    InvalidTextureMultisample {
        binding: u32,
        layout_multisampled: bool,
        view_samples: u32,
    },
    #[error(
        "Texture binding {} expects sample type {:?}, but was given a view with format {:?} (sample type {:?})",
        binding,
        layout_sample_type,
        view_format,
        view_sample_type
    )]
    InvalidTextureSampleType {
        binding: u32,
        layout_sample_type: wgt::TextureSampleType,
        view_format: wgt::TextureFormat,
        view_sample_type: wgt::TextureSampleType,
    },
    #[error("Texture binding {binding} expects dimension = {layout_dimension:?}, but given a view with dimension = {view_dimension:?}")]
    InvalidTextureDimension {
        binding: u32,
        layout_dimension: wgt::TextureViewDimension,
        view_dimension: wgt::TextureViewDimension,
    },
    #[error("Storage texture binding {binding} expects format = {layout_format:?}, but given a view with format = {view_format:?}")]
    InvalidStorageTextureFormat {
        binding: u32,
        layout_format: wgt::TextureFormat,
        view_format: wgt::TextureFormat,
    },
    #[error("Storage texture bindings must have a single mip level, but given a view with mip_level_count = {mip_level_count:?} at binding {binding}")]
    InvalidStorageTextureMipLevelCount { binding: u32, mip_level_count: u32 },
    #[error("Sampler binding {binding} expects comparison = {layout_cmp}, but given a sampler with comparison = {sampler_cmp}")]
    WrongSamplerComparison {
        binding: u32,
        layout_cmp: bool,
        sampler_cmp: bool,
    },
    #[error("Sampler binding {binding} expects filtering = {layout_flt}, but given a sampler with filtering = {sampler_flt}")]
    WrongSamplerFiltering {
        binding: u32,
        layout_flt: bool,
        sampler_flt: bool,
    },
    #[error("TLAS binding {binding} is required to support vertex returns but is missing flag AccelerationStructureFlags::ALLOW_RAY_HIT_VERTEX_RETURN")]
    MissingTLASVertexReturn { binding: u32 },
    #[error("Bound texture views can not have both depth and stencil aspects enabled")]
    DepthStencilAspect,
    #[error("The adapter does not support read access for storage textures of format {0:?}")]
    StorageReadNotSupported(wgt::TextureFormat),
    #[error("The adapter does not support atomics for storage textures of format {0:?}")]
    StorageAtomicNotSupported(wgt::TextureFormat),
    #[error("The adapter does not support write access for storage textures of format {0:?}")]
    StorageWriteNotSupported(wgt::TextureFormat),
    #[error("The adapter does not support read-write access for storage textures of format {0:?}")]
    StorageReadWriteNotSupported(wgt::TextureFormat),
    #[error(transparent)]
    ResourceUsageCompatibility(#[from] ResourceUsageCompatibilityError),
    #[error(transparent)]
    InvalidResource(#[from] InvalidResourceError),
}

#[derive(Clone, Debug, Error)]
pub enum BindingZone {
    #[error("Stage {0:?}")]
    Stage(wgt::ShaderStages),
    #[error("Whole pipeline")]
    Pipeline,
}

#[derive(Clone, Debug, Error)]
#[error("Too many bindings of type {kind:?} in {zone}, limit is {limit}, count was {count}. Check the limit `{}` passed to `Adapter::request_device`", .kind.to_config_str())]
pub struct BindingTypeMaxCountError {
    pub kind: BindingTypeMaxCountErrorKind,
    pub zone: BindingZone,
    pub limit: u32,
    pub count: u32,
}

#[derive(Clone, Debug)]
pub enum BindingTypeMaxCountErrorKind {
    DynamicUniformBuffers,
    DynamicStorageBuffers,
    SampledTextures,
    Samplers,
    StorageBuffers,
    StorageTextures,
    UniformBuffers,
    BindingArrayElements,
    BindingArraySamplerElements,
}

impl BindingTypeMaxCountErrorKind {
    fn to_config_str(&self) -> &'static str {
        match self {
            BindingTypeMaxCountErrorKind::DynamicUniformBuffers => {
                "max_dynamic_uniform_buffers_per_pipeline_layout"
            }
            BindingTypeMaxCountErrorKind::DynamicStorageBuffers => {
                "max_dynamic_storage_buffers_per_pipeline_layout"
            }
            BindingTypeMaxCountErrorKind::SampledTextures => {
                "max_sampled_textures_per_shader_stage"
            }
            BindingTypeMaxCountErrorKind::Samplers => "max_samplers_per_shader_stage",
            BindingTypeMaxCountErrorKind::StorageBuffers => "max_storage_buffers_per_shader_stage",
            BindingTypeMaxCountErrorKind::StorageTextures => {
                "max_storage_textures_per_shader_stage"
            }
            BindingTypeMaxCountErrorKind::UniformBuffers => "max_uniform_buffers_per_shader_stage",
            BindingTypeMaxCountErrorKind::BindingArrayElements => {
                "max_binding_array_elements_per_shader_stage"
            }
            BindingTypeMaxCountErrorKind::BindingArraySamplerElements => {
                "max_binding_array_elements_per_shader_stage"
            }
        }
    }
}

#[derive(Debug, Default)]
pub(crate) struct PerStageBindingTypeCounter {
    vertex: u32,
    fragment: u32,
    compute: u32,
}

impl PerStageBindingTypeCounter {
    pub(crate) fn add(&mut self, stage: wgt::ShaderStages, count: u32) {
        if stage.contains(wgt::ShaderStages::VERTEX) {
            self.vertex += count;
        }
        if stage.contains(wgt::ShaderStages::FRAGMENT) {
            self.fragment += count;
        }
        if stage.contains(wgt::ShaderStages::COMPUTE) {
            self.compute += count;
        }
    }

    pub(crate) fn max(&self) -> (BindingZone, u32) {
        let max_value = self.vertex.max(self.fragment.max(self.compute));
        let mut stage = wgt::ShaderStages::NONE;
        if max_value == self.vertex {
            stage |= wgt::ShaderStages::VERTEX
        }
        if max_value == self.fragment {
            stage |= wgt::ShaderStages::FRAGMENT
        }
        if max_value == self.compute {
            stage |= wgt::ShaderStages::COMPUTE
        }
        (BindingZone::Stage(stage), max_value)
    }

    pub(crate) fn merge(&mut self, other: &Self) {
        self.vertex = self.vertex.max(other.vertex);
        self.fragment = self.fragment.max(other.fragment);
        self.compute = self.compute.max(other.compute);
    }

    pub(crate) fn validate(
        &self,
        limit: u32,
        kind: BindingTypeMaxCountErrorKind,
    ) -> Result<(), BindingTypeMaxCountError> {
        let (zone, count) = self.max();
        if limit < count {
            Err(BindingTypeMaxCountError {
                kind,
                zone,
                limit,
                count,
            })
        } else {
            Ok(())
        }
    }
}

#[derive(Debug, Default)]
pub(crate) struct BindingTypeMaxCountValidator {
    dynamic_uniform_buffers: u32,
    dynamic_storage_buffers: u32,
    sampled_textures: PerStageBindingTypeCounter,
    samplers: PerStageBindingTypeCounter,
    storage_buffers: PerStageBindingTypeCounter,
    storage_textures: PerStageBindingTypeCounter,
    uniform_buffers: PerStageBindingTypeCounter,
    acceleration_structures: PerStageBindingTypeCounter,
    binding_array_elements: PerStageBindingTypeCounter,
    binding_array_sampler_elements: PerStageBindingTypeCounter,
    has_bindless_array: bool,
}

impl BindingTypeMaxCountValidator {
    pub(crate) fn add_binding(&mut self, binding: &wgt::BindGroupLayoutEntry) {
        let count = binding.count.map_or(1, |count| count.get());

        if binding.count.is_some() {
            self.binding_array_elements.add(binding.visibility, count);
            self.has_bindless_array = true;

            if let wgt::BindingType::Sampler(_) = binding.ty {
                self.binding_array_sampler_elements
                    .add(binding.visibility, count);
            }
        } else {
            match binding.ty {
                wgt::BindingType::Buffer {
                    ty: wgt::BufferBindingType::Uniform,
                    has_dynamic_offset,
                    ..
                } => {
                    self.uniform_buffers.add(binding.visibility, count);
                    if has_dynamic_offset {
                        self.dynamic_uniform_buffers += count;
                    }
                }
                wgt::BindingType::Buffer {
                    ty: wgt::BufferBindingType::Storage { .. },
                    has_dynamic_offset,
                    ..
                } => {
                    self.storage_buffers.add(binding.visibility, count);
                    if has_dynamic_offset {
                        self.dynamic_storage_buffers += count;
                    }
                }
                wgt::BindingType::Sampler { .. } => {
                    self.samplers.add(binding.visibility, count);
                }
                wgt::BindingType::Texture { .. } => {
                    self.sampled_textures.add(binding.visibility, count);
                }
                wgt::BindingType::StorageTexture { .. } => {
                    self.storage_textures.add(binding.visibility, count);
                }
                wgt::BindingType::AccelerationStructure { .. } => {
                    self.acceleration_structures.add(binding.visibility, count);
                }
            }
        }
    }

    pub(crate) fn merge(&mut self, other: &Self) {
        self.dynamic_uniform_buffers += other.dynamic_uniform_buffers;
        self.dynamic_storage_buffers += other.dynamic_storage_buffers;
        self.sampled_textures.merge(&other.sampled_textures);
        self.samplers.merge(&other.samplers);
        self.storage_buffers.merge(&other.storage_buffers);
        self.storage_textures.merge(&other.storage_textures);
        self.uniform_buffers.merge(&other.uniform_buffers);
        self.acceleration_structures
            .merge(&other.acceleration_structures);
        self.binding_array_elements
            .merge(&other.binding_array_elements);
        self.binding_array_sampler_elements
            .merge(&other.binding_array_sampler_elements);
    }

    pub(crate) fn validate(&self, limits: &wgt::Limits) -> Result<(), BindingTypeMaxCountError> {
        if limits.max_dynamic_uniform_buffers_per_pipeline_layout < self.dynamic_uniform_buffers {
            return Err(BindingTypeMaxCountError {
                kind: BindingTypeMaxCountErrorKind::DynamicUniformBuffers,
                zone: BindingZone::Pipeline,
                limit: limits.max_dynamic_uniform_buffers_per_pipeline_layout,
                count: self.dynamic_uniform_buffers,
            });
        }
        if limits.max_dynamic_storage_buffers_per_pipeline_layout < self.dynamic_storage_buffers {
            return Err(BindingTypeMaxCountError {
                kind: BindingTypeMaxCountErrorKind::DynamicStorageBuffers,
                zone: BindingZone::Pipeline,
                limit: limits.max_dynamic_storage_buffers_per_pipeline_layout,
                count: self.dynamic_storage_buffers,
            });
        }
        self.sampled_textures.validate(
            limits.max_sampled_textures_per_shader_stage,
            BindingTypeMaxCountErrorKind::SampledTextures,
        )?;
        self.samplers.validate(
            limits.max_samplers_per_shader_stage,
            BindingTypeMaxCountErrorKind::Samplers,
        )?;
        self.storage_buffers.validate(
            limits.max_storage_buffers_per_shader_stage,
            BindingTypeMaxCountErrorKind::StorageBuffers,
        )?;
        self.storage_textures.validate(
            limits.max_storage_textures_per_shader_stage,
            BindingTypeMaxCountErrorKind::StorageTextures,
        )?;
        self.uniform_buffers.validate(
            limits.max_uniform_buffers_per_shader_stage,
            BindingTypeMaxCountErrorKind::UniformBuffers,
        )?;
        self.binding_array_elements.validate(
            limits.max_binding_array_elements_per_shader_stage,
            BindingTypeMaxCountErrorKind::BindingArrayElements,
        )?;
        self.binding_array_sampler_elements.validate(
            limits.max_binding_array_sampler_elements_per_shader_stage,
            BindingTypeMaxCountErrorKind::BindingArraySamplerElements,
        )?;
        Ok(())
    }

    /// Validate that the bind group layout does not contain both a binding array and a dynamic offset array.
    ///
    /// This allows us to use `UPDATE_AFTER_BIND` on vulkan for bindless arrays. Vulkan does not allow
    /// `UPDATE_AFTER_BIND` on dynamic offset arrays. See <https://github.com/gfx-rs/wgpu/issues/6737>
    pub(crate) fn validate_binding_arrays(&self) -> Result<(), CreateBindGroupLayoutError> {
        let has_dynamic_offset_array =
            self.dynamic_uniform_buffers > 0 || self.dynamic_storage_buffers > 0;
        let has_uniform_buffer = self.uniform_buffers.max().1 > 0;
        if self.has_bindless_array && has_dynamic_offset_array {
            return Err(CreateBindGroupLayoutError::ContainsBothBindingArrayAndDynamicOffsetArray);
        }
        if self.has_bindless_array && has_uniform_buffer {
            return Err(CreateBindGroupLayoutError::ContainsBothBindingArrayAndUniformBuffer);
        }
        Ok(())
    }
}

/// Bindable resource and the slot to bind it to.
/// cbindgen:ignore
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BindGroupEntry<'a, B = BufferId, S = SamplerId, TV = TextureViewId, TLAS = TlasId>
where
    [BufferBinding<B>]: ToOwned,
    [S]: ToOwned,
    [TV]: ToOwned,
    <[BufferBinding<B>] as ToOwned>::Owned: fmt::Debug,
    <[S] as ToOwned>::Owned: fmt::Debug,
    <[TV] as ToOwned>::Owned: fmt::Debug,
{
    /// Slot for which binding provides resource. Corresponds to an entry of the same
    /// binding index in the [`BindGroupLayoutDescriptor`].
    pub binding: u32,
    #[cfg_attr(
        feature = "serde",
        serde(bound(deserialize = "BindingResource<'a, B, S, TV, TLAS>: Deserialize<'de>"))
    )]
    /// Resource to attach to the binding
    pub resource: BindingResource<'a, B, S, TV, TLAS>,
}

/// cbindgen:ignore
pub type ResolvedBindGroupEntry<'a> =
    BindGroupEntry<'a, Arc<Buffer>, Arc<Sampler>, Arc<TextureView>, Arc<Tlas>>;

/// Describes a group of bindings and the resources to be bound.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BindGroupDescriptor<
    'a,
    BGL = BindGroupLayoutId,
    B = BufferId,
    S = SamplerId,
    TV = TextureViewId,
    TLAS = TlasId,
> where
    [BufferBinding<B>]: ToOwned,
    [S]: ToOwned,
    [TV]: ToOwned,
    <[BufferBinding<B>] as ToOwned>::Owned: fmt::Debug,
    <[S] as ToOwned>::Owned: fmt::Debug,
    <[TV] as ToOwned>::Owned: fmt::Debug,
    [BindGroupEntry<'a, B, S, TV, TLAS>]: ToOwned,
    <[BindGroupEntry<'a, B, S, TV, TLAS>] as ToOwned>::Owned: fmt::Debug,
{
    /// Debug label of the bind group.
    ///
    /// This will show up in graphics debuggers for easy identification.
    pub label: Label<'a>,
    /// The [`BindGroupLayout`] that corresponds to this bind group.
    pub layout: BGL,
    #[cfg_attr(
        feature = "serde",
        serde(bound(
            deserialize = "<[BindGroupEntry<'a, B, S, TV, TLAS>] as ToOwned>::Owned: Deserialize<'de>"
        ))
    )]
    /// The resources to bind to this bind group.
    pub entries: Cow<'a, [BindGroupEntry<'a, B, S, TV, TLAS>]>,
}

/// cbindgen:ignore
pub type ResolvedBindGroupDescriptor<'a> = BindGroupDescriptor<
    'a,
    Arc<BindGroupLayout>,
    Arc<Buffer>,
    Arc<Sampler>,
    Arc<TextureView>,
    Arc<Tlas>,
>;

/// Describes a [`BindGroupLayout`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BindGroupLayoutDescriptor<'a> {
    /// Debug label of the bind group layout.
    ///
    /// This will show up in graphics debuggers for easy identification.
    pub label: Label<'a>,
    /// Array of entries in this BindGroupLayout
    pub entries: Cow<'a, [wgt::BindGroupLayoutEntry]>,
}

/// Used by [`BindGroupLayout`]. It indicates whether the BGL must be
/// used with a specific pipeline. This constraint only happens when
/// the BGLs have been derived from a pipeline without a layout.
#[derive(Debug)]
pub(crate) enum ExclusivePipeline {
    None,
    Render(Weak<RenderPipeline>),
    Compute(Weak<ComputePipeline>),
}

impl fmt::Display for ExclusivePipeline {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ExclusivePipeline::None => f.write_str("None"),
            ExclusivePipeline::Render(p) => {
                if let Some(p) = p.upgrade() {
                    p.error_ident().fmt(f)
                } else {
                    f.write_str("RenderPipeline")
                }
            }
            ExclusivePipeline::Compute(p) => {
                if let Some(p) = p.upgrade() {
                    p.error_ident().fmt(f)
                } else {
                    f.write_str("ComputePipeline")
                }
            }
        }
    }
}

/// Bind group layout.
#[derive(Debug)]
pub struct BindGroupLayout {
    pub(crate) raw: ManuallyDrop<Box<dyn hal::DynBindGroupLayout>>,
    pub(crate) device: Arc<Device>,
    pub(crate) entries: bgl::EntryMap,
    /// It is very important that we know if the bind group comes from the BGL pool.
    ///
    /// If it does, then we need to remove it from the pool when we drop it.
    ///
    /// We cannot unconditionally remove from the pool, as BGLs that don't come from the pool
    /// (derived BGLs) must not be removed.
    pub(crate) origin: bgl::Origin,
    pub(crate) exclusive_pipeline: crate::OnceCellOrLock<ExclusivePipeline>,
    #[allow(unused)]
    pub(crate) binding_count_validator: BindingTypeMaxCountValidator,
    /// The `label` from the descriptor used to create the resource.
    pub(crate) label: String,
}

impl Drop for BindGroupLayout {
    fn drop(&mut self) {
        resource_log!("Destroy raw {}", self.error_ident());
        if matches!(self.origin, bgl::Origin::Pool) {
            self.device.bgl_pool.remove(&self.entries);
        }
        // SAFETY: We are in the Drop impl and we don't use self.raw anymore after this point.
        let raw = unsafe { ManuallyDrop::take(&mut self.raw) };
        unsafe {
            self.device.raw().destroy_bind_group_layout(raw);
        }
    }
}

crate::impl_resource_type!(BindGroupLayout);
crate::impl_labeled!(BindGroupLayout);
crate::impl_parent_device!(BindGroupLayout);
crate::impl_storage_item!(BindGroupLayout);

impl BindGroupLayout {
    pub(crate) fn raw(&self) -> &dyn hal::DynBindGroupLayout {
        self.raw.as_ref()
    }
}

#[derive(Clone, Debug, Error)]
#[non_exhaustive]
pub enum CreatePipelineLayoutError {
    #[error(transparent)]
    Device(#[from] DeviceError),
    #[error(
        "Push constant at index {index} has range bound {bound} not aligned to {}",
        wgt::PUSH_CONSTANT_ALIGNMENT
    )]
    MisalignedPushConstantRange { index: usize, bound: u32 },
    #[error(transparent)]
    MissingFeatures(#[from] MissingFeatures),
    #[error("Push constant range (index {index}) provides for stage(s) {provided:?} but there exists another range that provides stage(s) {intersected:?}. Each stage may only be provided by one range")]
    MoreThanOnePushConstantRangePerStage {
        index: usize,
        provided: wgt::ShaderStages,
        intersected: wgt::ShaderStages,
    },
    #[error("Push constant at index {index} has range {}..{} which exceeds device push constant size limit 0..{max}", range.start, range.end)]
    PushConstantRangeTooLarge {
        index: usize,
        range: Range<u32>,
        max: u32,
    },
    #[error(transparent)]
    TooManyBindings(BindingTypeMaxCountError),
    #[error("Bind group layout count {actual} exceeds device bind group limit {max}")]
    TooManyGroups { actual: usize, max: usize },
    #[error(transparent)]
    InvalidResource(#[from] InvalidResourceError),
}

#[derive(Clone, Debug, Error)]
#[non_exhaustive]
pub enum PushConstantUploadError {
    #[error("Provided push constant with indices {offset}..{end_offset} overruns matching push constant range at index {idx}, with stage(s) {:?} and indices {:?}", range.stages, range.range)]
    TooLarge {
        offset: u32,
        end_offset: u32,
        idx: usize,
        range: wgt::PushConstantRange,
    },
    #[error("Provided push constant is for stage(s) {actual:?}, stage with a partial match found at index {idx} with stage(s) {matched:?}, however push constants must be complete matches")]
    PartialRangeMatch {
        actual: wgt::ShaderStages,
        idx: usize,
        matched: wgt::ShaderStages,
    },
    #[error("Provided push constant is for stage(s) {actual:?}, but intersects a push constant range (at index {idx}) with stage(s) {missing:?}. Push constants must provide the stages for all ranges they intersect")]
    MissingStages {
        actual: wgt::ShaderStages,
        idx: usize,
        missing: wgt::ShaderStages,
    },
    #[error("Provided push constant is for stage(s) {actual:?}, however the pipeline layout has no push constant range for the stage(s) {unmatched:?}")]
    UnmatchedStages {
        actual: wgt::ShaderStages,
        unmatched: wgt::ShaderStages,
    },
    #[error("Provided push constant offset {0} does not respect `PUSH_CONSTANT_ALIGNMENT`")]
    Unaligned(u32),
}

/// Describes a pipeline layout.
///
/// A `PipelineLayoutDescriptor` can be used to create a pipeline layout.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(bound = "BGL: Serialize"))]
pub struct PipelineLayoutDescriptor<'a, BGL = BindGroupLayoutId>
where
    [BGL]: ToOwned,
    <[BGL] as ToOwned>::Owned: fmt::Debug,
{
    /// Debug label of the pipeline layout.
    ///
    /// This will show up in graphics debuggers for easy identification.
    pub label: Label<'a>,
    /// Bind groups that this pipeline uses. The first entry will provide all the bindings for
    /// "set = 0", second entry will provide all the bindings for "set = 1" etc.
    #[cfg_attr(
        feature = "serde",
        serde(bound(deserialize = "<[BGL] as ToOwned>::Owned: Deserialize<'de>"))
    )]
    pub bind_group_layouts: Cow<'a, [BGL]>,
    /// Set of push constant ranges this pipeline uses. Each shader stage that
    /// uses push constants must define the range in push constant memory that
    /// corresponds to its single `layout(push_constant)` uniform block.
    ///
    /// If this array is non-empty, the
    /// [`Features::PUSH_CONSTANTS`](wgt::Features::PUSH_CONSTANTS) feature must
    /// be enabled.
    pub push_constant_ranges: Cow<'a, [wgt::PushConstantRange]>,
}

/// cbindgen:ignore
pub type ResolvedPipelineLayoutDescriptor<'a> = PipelineLayoutDescriptor<'a, Arc<BindGroupLayout>>;

#[derive(Debug)]
pub struct PipelineLayout {
    pub(crate) raw: ManuallyDrop<Box<dyn hal::DynPipelineLayout>>,
    pub(crate) device: Arc<Device>,
    /// The `label` from the descriptor used to create the resource.
    pub(crate) label: String,
    pub(crate) bind_group_layouts: ArrayVec<Arc<BindGroupLayout>, { hal::MAX_BIND_GROUPS }>,
    pub(crate) push_constant_ranges: ArrayVec<wgt::PushConstantRange, { SHADER_STAGE_COUNT }>,
}

impl Drop for PipelineLayout {
    fn drop(&mut self) {
        resource_log!("Destroy raw {}", self.error_ident());
        // SAFETY: We are in the Drop impl and we don't use self.raw anymore after this point.
        let raw = unsafe { ManuallyDrop::take(&mut self.raw) };
        unsafe {
            self.device.raw().destroy_pipeline_layout(raw);
        }
    }
}

impl PipelineLayout {
    pub(crate) fn raw(&self) -> &dyn hal::DynPipelineLayout {
        self.raw.as_ref()
    }

    pub(crate) fn get_binding_maps(&self) -> ArrayVec<&bgl::EntryMap, { hal::MAX_BIND_GROUPS }> {
        self.bind_group_layouts
            .iter()
            .map(|bgl| &bgl.entries)
            .collect()
    }

    /// Validate push constants match up with expected ranges.
    pub(crate) fn validate_push_constant_ranges(
        &self,
        stages: wgt::ShaderStages,
        offset: u32,
        end_offset: u32,
    ) -> Result<(), PushConstantUploadError> {
        // Don't need to validate size against the push constant size limit here,
        // as push constant ranges are already validated to be within bounds,
        // and we validate that they are within the ranges.

        if offset % wgt::PUSH_CONSTANT_ALIGNMENT != 0 {
            return Err(PushConstantUploadError::Unaligned(offset));
        }

        // Push constant validation looks very complicated on the surface, but
        // the problem can be range-reduced pretty well.
        //
        // Push constants require (summarized from the vulkan spec):
        // 1. For each byte in the range and for each shader stage in stageFlags,
        //    there must be a push constant range in the layout that includes that
        //    byte and that stage.
        // 2. For each byte in the range and for each push constant range that overlaps that byte,
        //    `stage` must include all stages in that push constant range’s `stage`.
        //
        // However there are some additional constraints that help us:
        // 3. All push constant ranges are the only range that can access that stage.
        //    i.e. if one range has VERTEX, no other range has VERTEX
        //
        // Therefore we can simplify the checks in the following ways:
        // - Because 3 guarantees that the push constant range has a unique stage,
        //   when we check for 1, we can simply check that our entire updated range
        //   is within a push constant range. i.e. our range for a specific stage cannot
        //   intersect more than one push constant range.
        let mut used_stages = wgt::ShaderStages::NONE;
        for (idx, range) in self.push_constant_ranges.iter().enumerate() {
            // contains not intersects due to 2
            if stages.contains(range.stages) {
                if !(range.range.start <= offset && end_offset <= range.range.end) {
                    return Err(PushConstantUploadError::TooLarge {
                        offset,
                        end_offset,
                        idx,
                        range: range.clone(),
                    });
                }
                used_stages |= range.stages;
            } else if stages.intersects(range.stages) {
                // Will be caught by used stages check below, but we can do this because of 1
                // and is more helpful to the user.
                return Err(PushConstantUploadError::PartialRangeMatch {
                    actual: stages,
                    idx,
                    matched: range.stages,
                });
            }

            // The push constant range intersects range we are uploading
            if offset < range.range.end && range.range.start < end_offset {
                // But requires stages we don't provide
                if !stages.contains(range.stages) {
                    return Err(PushConstantUploadError::MissingStages {
                        actual: stages,
                        idx,
                        missing: stages,
                    });
                }
            }
        }
        if used_stages != stages {
            return Err(PushConstantUploadError::UnmatchedStages {
                actual: stages,
                unmatched: stages - used_stages,
            });
        }
        Ok(())
    }
}

crate::impl_resource_type!(PipelineLayout);
crate::impl_labeled!(PipelineLayout);
crate::impl_parent_device!(PipelineLayout);
crate::impl_storage_item!(PipelineLayout);

#[repr(C)]
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BufferBinding<B = BufferId> {
    pub buffer: B,
    pub offset: wgt::BufferAddress,
    pub size: Option<wgt::BufferSize>,
}

pub type ResolvedBufferBinding = BufferBinding<Arc<Buffer>>;

// Note: Duplicated in `wgpu-rs` as `BindingResource`
// They're different enough that it doesn't make sense to share a common type
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BindingResource<'a, B = BufferId, S = SamplerId, TV = TextureViewId, TLAS = TlasId>
where
    [BufferBinding<B>]: ToOwned,
    [S]: ToOwned,
    [TV]: ToOwned,
    <[BufferBinding<B>] as ToOwned>::Owned: fmt::Debug,
    <[S] as ToOwned>::Owned: fmt::Debug,
    <[TV] as ToOwned>::Owned: fmt::Debug,
{
    Buffer(BufferBinding<B>),
    #[cfg_attr(
        feature = "serde",
        serde(bound(deserialize = "<[BufferBinding<B>] as ToOwned>::Owned: Deserialize<'de>"))
    )]
    BufferArray(Cow<'a, [BufferBinding<B>]>),
    Sampler(S),
    #[cfg_attr(
        feature = "serde",
        serde(bound(deserialize = "<[S] as ToOwned>::Owned: Deserialize<'de>"))
    )]
    SamplerArray(Cow<'a, [S]>),
    TextureView(TV),
    #[cfg_attr(
        feature = "serde",
        serde(bound(deserialize = "<[TV] as ToOwned>::Owned: Deserialize<'de>"))
    )]
    TextureViewArray(Cow<'a, [TV]>),
    AccelerationStructure(TLAS),
}

pub type ResolvedBindingResource<'a> =
    BindingResource<'a, Arc<Buffer>, Arc<Sampler>, Arc<TextureView>, Arc<Tlas>>;

#[derive(Clone, Debug, Error)]
#[non_exhaustive]
pub enum BindError {
    #[error(
        "{bind_group} {group} expects {expected} dynamic offset{s0}. However {actual} dynamic offset{s1} were provided.",
        s0 = if *.expected >= 2 { "s" } else { "" },
        s1 = if *.actual >= 2 { "s" } else { "" },
    )]
    MismatchedDynamicOffsetCount {
        bind_group: ResourceErrorIdent,
        group: u32,
        actual: usize,
        expected: usize,
    },
    #[error(
        "Dynamic binding index {idx} (targeting {bind_group} {group}, binding {binding}) with value {offset}, does not respect device's requested `{limit_name}` limit: {alignment}"
    )]
    UnalignedDynamicBinding {
        bind_group: ResourceErrorIdent,
        idx: usize,
        group: u32,
        binding: u32,
        offset: u32,
        alignment: u32,
        limit_name: &'static str,
    },
    #[error(
        "Dynamic binding offset index {idx} with offset {offset} would overrun the buffer bound to {bind_group} {group} -> binding {binding}. \
         Buffer size is {buffer_size} bytes, the binding binds bytes {binding_range:?}, meaning the maximum the binding can be offset is {maximum_dynamic_offset} bytes",
    )]
    DynamicBindingOutOfBounds {
        bind_group: ResourceErrorIdent,
        idx: usize,
        group: u32,
        binding: u32,
        offset: u32,
        buffer_size: wgt::BufferAddress,
        binding_range: Range<wgt::BufferAddress>,
        maximum_dynamic_offset: wgt::BufferAddress,
    },
}

#[derive(Debug)]
pub struct BindGroupDynamicBindingData {
    /// The index of the binding.
    ///
    /// Used for more descriptive errors.
    pub(crate) binding_idx: u32,
    /// The size of the buffer.
    ///
    /// Used for more descriptive errors.
    pub(crate) buffer_size: wgt::BufferAddress,
    /// The range that the binding covers.
    ///
    /// Used for more descriptive errors.
    pub(crate) binding_range: Range<wgt::BufferAddress>,
    /// The maximum value the dynamic offset can have before running off the end of the buffer.
    pub(crate) maximum_dynamic_offset: wgt::BufferAddress,
    /// The binding type.
    pub(crate) binding_type: wgt::BufferBindingType,
}

pub(crate) fn buffer_binding_type_alignment(
    limits: &wgt::Limits,
    binding_type: wgt::BufferBindingType,
) -> (u32, &'static str) {
    match binding_type {
        wgt::BufferBindingType::Uniform => (
            limits.min_uniform_buffer_offset_alignment,
            "min_uniform_buffer_offset_alignment",
        ),
        wgt::BufferBindingType::Storage { .. } => (
            limits.min_storage_buffer_offset_alignment,
            "min_storage_buffer_offset_alignment",
        ),
    }
}

pub(crate) fn buffer_binding_type_bounds_check_alignment(
    alignments: &hal::Alignments,
    binding_type: wgt::BufferBindingType,
) -> wgt::BufferAddress {
    match binding_type {
        wgt::BufferBindingType::Uniform => alignments.uniform_bounds_check_alignment.get(),
        wgt::BufferBindingType::Storage { .. } => wgt::COPY_BUFFER_ALIGNMENT,
    }
}

#[derive(Debug)]
pub struct BindGroup {
    pub(crate) raw: Snatchable<Box<dyn hal::DynBindGroup>>,
    pub(crate) device: Arc<Device>,
    pub(crate) layout: Arc<BindGroupLayout>,
    /// The `label` from the descriptor used to create the resource.
    pub(crate) label: String,
    pub(crate) tracking_data: TrackingData,
    pub(crate) used: BindGroupStates,
    pub(crate) used_buffer_ranges: Vec<BufferInitTrackerAction>,
    pub(crate) used_texture_ranges: Vec<TextureInitTrackerAction>,
    pub(crate) dynamic_binding_info: Vec<BindGroupDynamicBindingData>,
    /// Actual binding sizes for buffers that don't have `min_binding_size`
    /// specified in BGL. Listed in the order of iteration of `BGL.entries`.
    pub(crate) late_buffer_binding_sizes: Vec<wgt::BufferSize>,
}

impl Drop for BindGroup {
    fn drop(&mut self) {
        if let Some(raw) = self.raw.take() {
            resource_log!("Destroy raw {}", self.error_ident());
            unsafe {
                self.device.raw().destroy_bind_group(raw);
            }
        }
    }
}

impl BindGroup {
    pub(crate) fn try_raw<'a>(
        &'a self,
        guard: &'a SnatchGuard,
    ) -> Result<&'a dyn hal::DynBindGroup, DestroyedResourceError> {
        // Clippy insist on writing it this way. The idea is to return None
        // if any of the raw buffer is not valid anymore.
        for buffer in &self.used_buffer_ranges {
            buffer.buffer.try_raw(guard)?;
        }
        for texture in &self.used_texture_ranges {
            texture.texture.try_raw(guard)?;
        }

        self.raw
            .get(guard)
            .map(|raw| raw.as_ref())
            .ok_or_else(|| DestroyedResourceError(self.error_ident()))
    }

    pub(crate) fn validate_dynamic_bindings(
        &self,
        bind_group_index: u32,
        offsets: &[wgt::DynamicOffset],
    ) -> Result<(), BindError> {
        if self.dynamic_binding_info.len() != offsets.len() {
            return Err(BindError::MismatchedDynamicOffsetCount {
                bind_group: self.error_ident(),
                group: bind_group_index,
                expected: self.dynamic_binding_info.len(),
                actual: offsets.len(),
            });
        }

        for (idx, (info, &offset)) in self
            .dynamic_binding_info
            .iter()
            .zip(offsets.iter())
            .enumerate()
        {
            let (alignment, limit_name) =
                buffer_binding_type_alignment(&self.device.limits, info.binding_type);
            if offset as wgt::BufferAddress % alignment as u64 != 0 {
                return Err(BindError::UnalignedDynamicBinding {
                    bind_group: self.error_ident(),
                    group: bind_group_index,
                    binding: info.binding_idx,
                    idx,
                    offset,
                    alignment,
                    limit_name,
                });
            }

            if offset as wgt::BufferAddress > info.maximum_dynamic_offset {
                return Err(BindError::DynamicBindingOutOfBounds {
                    bind_group: self.error_ident(),
                    group: bind_group_index,
                    binding: info.binding_idx,
                    idx,
                    offset,
                    buffer_size: info.buffer_size,
                    binding_range: info.binding_range.clone(),
                    maximum_dynamic_offset: info.maximum_dynamic_offset,
                });
            }
        }

        Ok(())
    }
}

crate::impl_resource_type!(BindGroup);
crate::impl_labeled!(BindGroup);
crate::impl_parent_device!(BindGroup);
crate::impl_storage_item!(BindGroup);
crate::impl_trackable!(BindGroup);

#[derive(Clone, Debug, Error)]
#[non_exhaustive]
pub enum GetBindGroupLayoutError {
    #[error("Invalid group index {0}")]
    InvalidGroupIndex(u32),
    #[error(transparent)]
    InvalidResource(#[from] InvalidResourceError),
}

#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[error("Buffer is bound with size {bound_size} where the shader expects {shader_size} in group[{group_index}] compact index {compact_index}")]
pub struct LateMinBufferBindingSizeMismatch {
    pub group_index: u32,
    pub compact_index: usize,
    pub shader_size: wgt::BufferAddress,
    pub bound_size: wgt::BufferAddress,
}
