use super::{
    WgpuLive2dRenderer,
    buffers::WgpuMeshBuffers,
    clipping::{WgpuClippingResources, WgpuMaskRenderTarget},
    texture::{WgpuTexture, WgpuTransform},
};

#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum WgpuRenderError {
    #[error("invalid texture index {texture_index}")]
    InvalidTextureIndex { texture_index: i32 },
    #[error("missing texture bind group {texture_index}")]
    MissingTexture { texture_index: i32 },
    #[error("missing drawable {drawable_index}")]
    MissingDrawable { drawable_index: usize },
    #[error("drawable {drawable_index} has clipping masks but no prepared clipping context")]
    MissingClippingContext { drawable_index: usize },
    #[error(
        "drawable {drawable_index} uses {mask_count} clipping masks, but clipping is not implemented"
    )]
    UnsupportedClippingMasks {
        drawable_index: usize,
        mask_count: usize,
    },
}

impl WgpuLive2dRenderer {
    pub fn draw(
        &self,
        render_pass: &mut wgpu::RenderPass<'_>,
        mesh_buffers: &WgpuMeshBuffers,
        texture_bind_groups: &[wgpu::BindGroup],
    ) -> Result<u32, WgpuRenderError> {
        self.draw_with_bind_groups_and_transform(
            render_pass,
            mesh_buffers,
            texture_bind_groups,
            &self.identity_transform,
        )
    }

    pub fn draw_with_bind_groups_and_transform(
        &self,
        render_pass: &mut wgpu::RenderPass<'_>,
        mesh_buffers: &WgpuMeshBuffers,
        texture_bind_groups: &[wgpu::BindGroup],
        transform: &WgpuTransform,
    ) -> Result<u32, WgpuRenderError> {
        self.draw_with_bind_group_provider(render_pass, mesh_buffers, transform, |texture_index| {
            texture_bind_group_at(texture_bind_groups, texture_index)
        })
    }

    pub fn draw_with_textures(
        &self,
        render_pass: &mut wgpu::RenderPass<'_>,
        mesh_buffers: &WgpuMeshBuffers,
        textures: &[WgpuTexture],
    ) -> Result<u32, WgpuRenderError> {
        self.draw_with_textures_and_transform(
            render_pass,
            mesh_buffers,
            textures,
            &self.identity_transform,
        )
    }

    pub fn draw_with_textures_and_transform(
        &self,
        render_pass: &mut wgpu::RenderPass<'_>,
        mesh_buffers: &WgpuMeshBuffers,
        textures: &[WgpuTexture],
        transform: &WgpuTransform,
    ) -> Result<u32, WgpuRenderError> {
        self.draw_with_bind_group_provider(render_pass, mesh_buffers, transform, |texture_index| {
            texture_bind_group_from_textures(textures, texture_index)
        })
    }

    pub fn draw_with_textures_and_clipping(
        &self,
        render_pass: &mut wgpu::RenderPass<'_>,
        mesh_buffers: &WgpuMeshBuffers,
        textures: &[WgpuTexture],
        clipping_resources: &WgpuClippingResources,
        mask_target: &WgpuMaskRenderTarget,
    ) -> Result<u32, WgpuRenderError> {
        self.draw_with_textures_clipping_and_transform(
            render_pass,
            mesh_buffers,
            textures,
            clipping_resources,
            mask_target,
            &self.identity_transform,
        )
    }

    pub fn draw_with_textures_clipping_and_transform(
        &self,
        render_pass: &mut wgpu::RenderPass<'_>,
        mesh_buffers: &WgpuMeshBuffers,
        textures: &[WgpuTexture],
        clipping_resources: &WgpuClippingResources,
        mask_target: &WgpuMaskRenderTarget,
        transform: &WgpuTransform,
    ) -> Result<u32, WgpuRenderError> {
        self.draw_with_clipping_bind_group_provider(
            render_pass,
            mesh_buffers,
            clipping_resources,
            mask_target.bind_group(),
            transform,
            |texture_index| texture_bind_group_from_textures(textures, texture_index),
        )
    }

    pub fn draw_masks(
        &self,
        render_pass: &mut wgpu::RenderPass<'_>,
        mesh_buffers: &WgpuMeshBuffers,
        clipping_resources: &WgpuClippingResources,
        texture_bind_groups: &[wgpu::BindGroup],
    ) -> Result<u32, WgpuRenderError> {
        self.draw_masks_with_bind_group_provider(
            render_pass,
            mesh_buffers,
            clipping_resources,
            |texture_index| texture_bind_group_at(texture_bind_groups, texture_index),
        )
    }

    pub fn draw_masks_with_textures(
        &self,
        render_pass: &mut wgpu::RenderPass<'_>,
        mesh_buffers: &WgpuMeshBuffers,
        clipping_resources: &WgpuClippingResources,
        textures: &[WgpuTexture],
    ) -> Result<u32, WgpuRenderError> {
        self.draw_masks_with_bind_group_provider(
            render_pass,
            mesh_buffers,
            clipping_resources,
            |texture_index| texture_bind_group_from_textures(textures, texture_index),
        )
    }

    fn draw_with_bind_group_provider<'a>(
        &self,
        render_pass: &mut wgpu::RenderPass<'_>,
        mesh_buffers: &WgpuMeshBuffers,
        transform: &WgpuTransform,
        mut bind_group_for_texture: impl FnMut(i32) -> Result<&'a wgpu::BindGroup, WgpuRenderError>,
    ) -> Result<u32, WgpuRenderError> {
        let mut drawn = 0;
        for &drawable_index in mesh_buffers.draw_order_indices() {
            let drawable = mesh_buffers
                .drawables()
                .get(drawable_index)
                .ok_or(WgpuRenderError::MissingDrawable { drawable_index })?;
            if !drawable.is_visible() {
                continue;
            }
            if !drawable.masks().is_empty() {
                return Err(WgpuRenderError::UnsupportedClippingMasks {
                    drawable_index,
                    mask_count: drawable.masks().len(),
                });
            }
            let texture_bind_group = bind_group_for_texture(drawable.texture_index())?;

            render_pass.set_pipeline(self.pipeline_for_blend_mode(drawable.blend_mode()));
            render_pass.set_bind_group(0, texture_bind_group, &[]);
            render_pass.set_bind_group(1, transform.bind_group(), &[]);
            render_pass.set_vertex_buffer(0, drawable.vertex_buffer().slice(..));
            render_pass
                .set_index_buffer(drawable.index_buffer().slice(..), wgpu::IndexFormat::Uint16);
            render_pass.draw_indexed(0..drawable.index_count(), 0, 0..1);
            drawn += 1;
        }

        Ok(drawn)
    }

    fn draw_masks_with_bind_group_provider<'a>(
        &self,
        render_pass: &mut wgpu::RenderPass<'_>,
        mesh_buffers: &WgpuMeshBuffers,
        clipping_resources: &WgpuClippingResources,
        mut bind_group_for_texture: impl FnMut(i32) -> Result<&'a wgpu::BindGroup, WgpuRenderError>,
    ) -> Result<u32, WgpuRenderError> {
        let mut drawn = 0;
        for context in clipping_resources.contexts() {
            for &drawable_index in context.mask_drawable_indices() {
                let drawable = mesh_buffers
                    .drawables()
                    .get(drawable_index)
                    .ok_or(WgpuRenderError::MissingDrawable { drawable_index })?;
                if drawable.is_empty() {
                    continue;
                }
                let texture_bind_group = bind_group_for_texture(drawable.texture_index())?;

                render_pass.set_pipeline(&self.mask_pipeline);
                render_pass.set_bind_group(0, texture_bind_group, &[]);
                render_pass.set_bind_group(1, context.mask_transform().bind_group(), &[]);
                render_pass.set_bind_group(2, context.mask_params().bind_group(), &[]);
                render_pass.set_vertex_buffer(0, drawable.vertex_buffer().slice(..));
                render_pass
                    .set_index_buffer(drawable.index_buffer().slice(..), wgpu::IndexFormat::Uint16);
                render_pass.draw_indexed(0..drawable.index_count(), 0, 0..1);
                drawn += 1;
            }
        }

        Ok(drawn)
    }

    fn draw_with_clipping_bind_group_provider<'a>(
        &self,
        render_pass: &mut wgpu::RenderPass<'_>,
        mesh_buffers: &WgpuMeshBuffers,
        clipping_resources: &WgpuClippingResources,
        mask_bind_group: &wgpu::BindGroup,
        transform: &WgpuTransform,
        mut bind_group_for_texture: impl FnMut(i32) -> Result<&'a wgpu::BindGroup, WgpuRenderError>,
    ) -> Result<u32, WgpuRenderError> {
        let mut drawn = 0;
        for &drawable_index in mesh_buffers.draw_order_indices() {
            let drawable = mesh_buffers
                .drawables()
                .get(drawable_index)
                .ok_or(WgpuRenderError::MissingDrawable { drawable_index })?;
            if !drawable.is_visible() {
                continue;
            }
            let texture_bind_group = bind_group_for_texture(drawable.texture_index())?;

            if drawable.masks().is_empty() {
                render_pass.set_pipeline(self.pipeline_for_blend_mode(drawable.blend_mode()));
                render_pass.set_bind_group(0, texture_bind_group, &[]);
                render_pass.set_bind_group(1, transform.bind_group(), &[]);
            } else {
                let context = clipping_resources
                    .context_for_drawable(drawable_index)
                    .ok_or(WgpuRenderError::MissingClippingContext { drawable_index })?;
                render_pass
                    .set_pipeline(self.masked_pipeline_for_blend_mode(drawable.blend_mode()));
                render_pass.set_bind_group(0, texture_bind_group, &[]);
                render_pass.set_bind_group(1, transform.bind_group(), &[]);
                render_pass.set_bind_group(2, mask_bind_group, &[]);
                render_pass.set_bind_group(3, context.clip_params().bind_group(), &[]);
            }

            render_pass.set_vertex_buffer(0, drawable.vertex_buffer().slice(..));
            render_pass
                .set_index_buffer(drawable.index_buffer().slice(..), wgpu::IndexFormat::Uint16);
            render_pass.draw_indexed(0..drawable.index_count(), 0, 0..1);
            drawn += 1;
        }

        Ok(drawn)
    }
}

fn texture_bind_group_at(
    texture_bind_groups: &[wgpu::BindGroup],
    texture_index: i32,
) -> Result<&wgpu::BindGroup, WgpuRenderError> {
    let texture_index_usize = usize::try_from(texture_index)
        .map_err(|_| WgpuRenderError::InvalidTextureIndex { texture_index })?;
    texture_bind_groups
        .get(texture_index_usize)
        .ok_or(WgpuRenderError::MissingTexture { texture_index })
}

fn texture_bind_group_from_textures(
    textures: &[WgpuTexture],
    texture_index: i32,
) -> Result<&wgpu::BindGroup, WgpuRenderError> {
    let texture_index_usize = usize::try_from(texture_index)
        .map_err(|_| WgpuRenderError::InvalidTextureIndex { texture_index })?;
    textures
        .get(texture_index_usize)
        .map(WgpuTexture::bind_group)
        .ok_or(WgpuRenderError::MissingTexture { texture_index })
}
