// Copyright (c) 2021 Vladyslav Batyrenko
// SPDX-License-Identifier: MIT
//
// This code is vendored from bevy_egui: https://github.com/vladbat00/bevy_egui
// Original author: Vladyslav Batyrenko <vladyslav.batyrenko@gmail.com>

use crate::debug_ui::{
    EguiContextSettings, EguiManagedTextures, EguiRenderOutput, EguiUserTextures,
    RenderComputedScaleFactor,
    render::{
        DrawCommand, DrawPrimitive, EguiBevyPaintCallback, EguiCameraView, EguiDraw, EguiPipeline,
        EguiPipelineKey, EguiViewTarget, PaintCallbackDraw,
    },
};
use bevy::asset::prelude::*;
use bevy::prelude::{Deref, DerefMut};
use bevy::ecs::{prelude::*, system::SystemParam};
use bevy::image::Image;
use bevy::log;
use bevy::math::{URect, UVec2, Vec2};
use bevy::platform::collections::HashMap;
use bevy::render::{
    camera::ExtractedCamera,
    extract_resource::ExtractResource,
    render_asset::RenderAssets,
    render_resource::{
        BindGroup, BindGroupEntry, BindingResource, Buffer, BufferDescriptor, BufferId,
        CachedRenderPipelineId, DynamicUniformBuffer, PipelineCache, SpecializedRenderPipelines,
    },
    renderer::{RenderDevice, RenderQueue},
    sync_world::{MainEntity, RenderEntity},
    texture::GpuImage,
    view::ExtractedView,
};
use bytemuck::cast_slice;
use itertools::Itertools;
use wgpu_types::{BufferAddress, BufferUsages};

/// Extracted Egui settings.
#[derive(Resource, Deref, DerefMut, Default)]
pub struct ExtractedEguiSettings(pub EguiContextSettings);

/// The extracted version of [`EguiManagedTextures`].
#[derive(Debug, Resource)]
pub struct ExtractedEguiManagedTextures(pub HashMap<(Entity, u64), Handle<Image>>);
impl ExtractResource for ExtractedEguiManagedTextures {
    type Source = EguiManagedTextures;

    fn extract_resource(source: &Self::Source) -> Self {
        Self(source.iter().map(|(k, v)| (*k, v.handle.clone())).collect())
    }
}

/// Corresponds to Egui's [`egui::TextureId`].
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum EguiTextureId {
    /// Textures allocated via Egui.
    Managed(MainEntity, u64),
    /// Textures allocated via Bevy.
    User(u64),
}

/// Extracted Egui textures.
#[derive(SystemParam)]
pub struct ExtractedEguiTextures<'w> {
    /// Maps Egui managed texture ids to Bevy image handles.
    pub egui_textures: Res<'w, ExtractedEguiManagedTextures>,
    /// Maps Bevy managed texture handles to Egui user texture ids.
    pub user_textures: Res<'w, EguiUserTextures>,
}

impl ExtractedEguiTextures<'_> {
    /// Returns an iterator over all textures (both Egui and Bevy managed).
    pub fn handles(&self) -> impl Iterator<Item = (EguiTextureId, AssetId<Image>)> + '_ {
        self.egui_textures
            .0
            .iter()
            .map(|(&(window, texture_id), managed_tex)| {
                (
                    EguiTextureId::Managed(MainEntity::from(window), texture_id),
                    managed_tex.id(),
                )
            })
            .chain(
                self.user_textures
                    .textures
                    .iter()
                    .map(|(handle, (_, id))| (EguiTextureId::User(*id), *handle)),
            )
    }
}

/// Describes the transform buffer.
#[derive(Resource, Default)]
pub struct EguiTransforms {
    /// Uniform buffer.
    pub buffer: DynamicUniformBuffer<EguiTransform>,
    /// The Entity is from the main world.
    pub offsets: HashMap<MainEntity, u32>,
    /// Bind group.
    pub bind_group: Option<(BufferId, BindGroup)>,
}

/// Scale and translation for rendering Egui shapes. Is needed to transform Egui coordinates from
/// the screen space with the center at (0, 0) to the normalised viewport space.
#[derive(bevy::render::render_resource::ShaderType, Default)]
pub struct EguiTransform {
    /// Is affected by render target size, scale factor and [`EguiContextSettings::scale_factor`].
    pub scale: Vec2,
    /// Normally equals `[-1.0, 1.0]`.
    pub translation: Vec2,
}

impl EguiTransform {
    /// Calculates the transform from target size and target scale factor multiplied by [`EguiContextSettings::scale_factor`].
    pub fn new(target_size: Vec2, scale_factor: f32) -> Self {
        EguiTransform {
            scale: Vec2::new(
                2.0 / (target_size.x / scale_factor),
                -2.0 / (target_size.y / scale_factor),
            ),
            translation: Vec2::new(-1.0, 1.0),
        }
    }
}

/// Prepares Egui transforms.
pub fn prepare_egui_transforms_system(
    mut egui_transforms: ResMut<EguiTransforms>,
    views: Query<&RenderComputedScaleFactor>,
    render_targets: Query<(&ExtractedView, &ExtractedCamera, &EguiCameraView)>,
    render_device: Res<RenderDevice>,
    render_queue: Res<RenderQueue>,
    egui_pipeline: Res<EguiPipeline>,
) -> Result {
    egui_transforms.buffer.clear();
    egui_transforms.offsets.clear();

    for (view, camera, egui_camera_view) in render_targets.iter() {
        let Some(target_size) = camera.physical_target_size else {
            continue;
        };

        let &RenderComputedScaleFactor { scale_factor } = views.get(egui_camera_view.0)?;
        let transform = EguiTransform::new(target_size.as_vec2(), scale_factor);
        let offset = egui_transforms
            .buffer
            .push(&transform);
        egui_transforms
            .offsets
            .insert(view.retained_view_entity.main_entity, offset);
    }

    egui_transforms
        .buffer
        .write_buffer(&render_device, &render_queue);

    if let Some(buffer) = egui_transforms.buffer.buffer() {
        match egui_transforms.bind_group {
            Some((id, _)) if buffer.id() == id => {}
            _ => {
                let transform_bind_group = render_device.create_bind_group(
                    Some("egui transform bind group"),
                    &egui_pipeline.transform_bind_group_layout,
                    &[BindGroupEntry {
                        binding: 0,
                        resource: egui_transforms.buffer.binding().unwrap(),
                    }],
                );
                egui_transforms.bind_group = Some((buffer.id(), transform_bind_group));
            }
        };
    }

    Ok(())
}

/// Maps Egui textures to bind groups.
#[derive(Resource, Deref, DerefMut, Default)]
pub struct EguiTextureBindGroups(pub HashMap<EguiTextureId, (BindGroup, Option<u32>)>);

/// Queues bind groups.
pub fn queue_bind_groups_system(
    mut commands: Commands,
    egui_textures: ExtractedEguiTextures,
    render_device: Res<RenderDevice>,
    gpu_images: Res<RenderAssets<GpuImage>>,
    egui_pipeline: Res<EguiPipeline>,
) {
    let egui_texture_iterator = egui_textures.handles().filter_map(|(texture, handle_id)| {
        let gpu_image = gpu_images.get(handle_id)?;
        Some((texture, gpu_image))
    });

    let bind_groups = if let Some(bindless) = egui_pipeline.bindless {
        let bindless = u32::from(bindless) as usize;
        let mut bind_groups = HashMap::new();

        let mut texture_array = Vec::new();
        let mut sampler_array = Vec::new();
        let mut egui_texture_ids = Vec::new();

        for textures in egui_texture_iterator.chunks(bindless).into_iter() {
            texture_array.clear();
            sampler_array.clear();
            egui_texture_ids.clear();

            for (egui_texture_id, gpu_image) in textures {
                egui_texture_ids.push(egui_texture_id);
                // Dereference needed to convert from bevy to wgpu type
                texture_array.push(&*gpu_image.texture_view);
                sampler_array.push(&*gpu_image.sampler);
            }

            let bind_group = render_device.create_bind_group(
                None,
                &egui_pipeline.texture_bind_group_layout,
                &[
                    BindGroupEntry {
                        binding: 0,
                        resource: BindingResource::TextureViewArray(texture_array.as_slice()),
                    },
                    BindGroupEntry {
                        binding: 1,
                        resource: BindingResource::SamplerArray(sampler_array.as_slice()),
                    },
                ],
            );

            // Simply assign bind group to egui texture
            // Additional code is not needed because bevy RenderPass set_bind_group
            // removes redundant switching between bind groups
            for (offset, egui_texture_id) in egui_texture_ids.drain(..).enumerate() {
                bind_groups.insert(egui_texture_id, (bind_group.clone(), Some(offset as u32)));
            }
        }
        bind_groups
    } else {
        egui_texture_iterator
            .map(|(texture, gpu_image)| {
                let bind_group = render_device.create_bind_group(
                    None,
                    &egui_pipeline.texture_bind_group_layout,
                    &[
                        BindGroupEntry {
                            binding: 0,
                            resource: BindingResource::TextureView(&gpu_image.texture_view),
                        },
                        BindGroupEntry {
                            binding: 1,
                            resource: BindingResource::Sampler(&gpu_image.sampler),
                        },
                    ],
                );
                (texture, (bind_group, None::<u32>))
            })
            .collect()
    };
    commands.insert_resource(EguiTextureBindGroups(bind_groups))
}

/// Cached Pipeline IDs for the specialized instances of `EguiPipeline`.
#[derive(Resource)]
pub struct EguiPipelines(pub HashMap<MainEntity, CachedRenderPipelineId>);

/// Queue [`EguiPipeline`] instances.
pub fn queue_pipelines_system(
    mut commands: Commands,
    pipeline_cache: Res<PipelineCache>,
    mut specialized_pipelines: ResMut<SpecializedRenderPipelines<EguiPipeline>>,
    egui_pipeline: Res<EguiPipeline>,
    egui_views: Query<&EguiViewTarget, With<ExtractedView>>,
    camera_views: Query<(&MainEntity, &ExtractedCamera)>,
) {
    let pipelines: HashMap<MainEntity, CachedRenderPipelineId> = egui_views
        .iter()
        .filter_map(|egui_camera_view| {
            let (main_entity, extracted_camera) = camera_views.get(egui_camera_view.0).ok()?;

            let pipeline_id = specialized_pipelines.specialize(
                &pipeline_cache,
                &egui_pipeline,
                EguiPipelineKey {
                    hdr: extracted_camera.hdr,
                },
            );
            Some((*main_entity, pipeline_id))
        })
        .collect();

    commands.insert_resource(EguiPipelines(pipelines));
}

/// Cached Pipeline IDs for the specialized instances of `EguiPipeline`.
#[derive(Default, Resource)]
pub struct EguiRenderData(pub(crate) HashMap<MainEntity, EguiRenderTargetData>);

pub(crate) struct EguiRenderTargetData {
    keep: bool,
    pub(crate) render_entity: RenderEntity,
    pub(crate) vertex_data: Vec<u8>,
    pub(crate) vertex_buffer_capacity: usize,
    pub(crate) vertex_buffer: Option<Buffer>,
    pub(crate) index_data: Vec<u32>,
    pub(crate) index_buffer_capacity: usize,
    pub(crate) index_buffer: Option<Buffer>,
    pub(crate) draw_commands: Vec<DrawCommand>,
    pub(crate) postponed_updates: Vec<(egui::Rect, PaintCallbackDraw)>,
    pub(crate) pixels_per_point: f32,
    pub(crate) target_size: UVec2,
    pub(crate) key: Option<EguiPipelineKey>,
}

impl Default for EguiRenderTargetData {
    fn default() -> Self {
        Self {
            keep: false,
            render_entity: RenderEntity::from(Entity::PLACEHOLDER),
            vertex_data: Vec::new(),
            vertex_buffer_capacity: 0,
            vertex_buffer: None,
            index_data: Vec::new(),
            index_buffer_capacity: 0,
            index_buffer: None,
            draw_commands: Vec::new(),
            postponed_updates: Vec::new(),
            pixels_per_point: 0.0,
            target_size: UVec2::ZERO,
            key: None,
        }
    }
}

/// Prepares Egui transforms.
pub fn prepare_egui_render_target_data_system(
    mut render_data: ResMut<EguiRenderData>,
    render_targets: Query<(
        Entity,
        &ExtractedView,
        &RenderComputedScaleFactor,
        &EguiViewTarget,
        &EguiRenderOutput,
    )>,
    extracted_cameras: Query<&ExtractedCamera>,
    render_device: Res<RenderDevice>,
    render_queue: Res<RenderQueue>,
) {
    let render_data = &mut render_data.0;
    render_data.retain(|_, data| {
        let keep = data.keep;
        data.keep = false;
        keep
    });

    for (render_entity, view, computed_scale_factor, egui_view_target, render_output) in
        render_targets.iter()
    {
        let data = render_data
            .entry(view.retained_view_entity.main_entity)
            .or_default();

        data.keep = true;
        data.render_entity = render_entity.into();

        // Construct a pipeline key based on a render target.
        let Ok(extracted_camera) = extracted_cameras.get(egui_view_target.0) else {
            // This is ok when a window is minimized.
            log::trace!("ExtractedCamera entity doesn't exist for the Egui view");
            continue;
        };
        data.key = Some(EguiPipelineKey {
            hdr: extracted_camera.hdr,
        });

        data.pixels_per_point = computed_scale_factor.scale_factor;
        if extracted_camera
            .physical_viewport_size
            .is_none_or(|size| size.x < 1 || size.y < 1)
        {
            continue;
        }

        let mut index_offset = 0;

        data.draw_commands.clear();
        data.vertex_data.clear();
        data.index_data.clear();
        data.postponed_updates.clear();

        for egui::epaint::ClippedPrimitive {
            clip_rect,
            primitive,
        } in render_output.paint_jobs.as_slice()
        {
            let clip_rect = *clip_rect;

            let clip_urect = URect {
                min: UVec2 {
                    x: (clip_rect.min.x * data.pixels_per_point).round() as u32,
                    y: (clip_rect.min.y * data.pixels_per_point).round() as u32,
                },
                max: UVec2 {
                    x: (clip_rect.max.x * data.pixels_per_point).round() as u32,
                    y: (clip_rect.max.y * data.pixels_per_point).round() as u32,
                },
            };

            if clip_urect
                .intersect(URect::new(
                    view.viewport.x,
                    view.viewport.y,
                    view.viewport.x + view.viewport.z,
                    view.viewport.y + view.viewport.w,
                ))
                .is_empty()
            {
                continue;
            }

            let mesh = match primitive {
                egui::epaint::Primitive::Mesh(mesh) => mesh,
                egui::epaint::Primitive::Callback(paint_callback) => {
                    let callback = match paint_callback
                        .callback
                        .clone()
                        .downcast::<EguiBevyPaintCallback>()
                    {
                        Ok(callback) => callback,
                        Err(err) => {
                            log::error!("Unsupported Egui paint callback type: {err:?}");
                            continue;
                        }
                    };

                    data.postponed_updates.push((
                        clip_rect,
                        PaintCallbackDraw {
                            callback: callback.clone(),
                            rect: paint_callback.rect,
                        },
                    ));

                    data.draw_commands.push(DrawCommand {
                        primitive: DrawPrimitive::PaintCallback(PaintCallbackDraw {
                            callback,
                            rect: paint_callback.rect,
                        }),
                        clip_rect,
                    });
                    continue;
                }
            };

            data.vertex_data
                .extend_from_slice(cast_slice::<_, u8>(mesh.vertices.as_slice()));
            data.index_data
                .extend(mesh.indices.iter().map(|i| i + index_offset));
            index_offset += mesh.vertices.len() as u32;

            let texture_handle = match mesh.texture_id {
                egui::TextureId::Managed(id) => {
                    EguiTextureId::Managed(view.retained_view_entity.main_entity, id)
                }
                egui::TextureId::User(id) => EguiTextureId::User(id),
            };

            data.draw_commands.push(DrawCommand {
                primitive: DrawPrimitive::Egui(EguiDraw {
                    vertices_count: mesh.indices.len(),
                    egui_texture: texture_handle,
                }),
                clip_rect,
            });
        }

        if data.vertex_data.len() > data.vertex_buffer_capacity {
            data.vertex_buffer_capacity = data.vertex_data.len().next_power_of_two();
            data.vertex_buffer = Some(render_device.create_buffer(&BufferDescriptor {
                label: Some("egui vertex buffer"),
                size: data.vertex_buffer_capacity as BufferAddress,
                usage: BufferUsages::COPY_DST | BufferUsages::VERTEX,
                mapped_at_creation: false,
            }));
        }

        let index_data_size = data.index_data.len() * std::mem::size_of::<u32>();
        if index_data_size > data.index_buffer_capacity {
            data.index_buffer_capacity = index_data_size.next_power_of_two();
            data.index_buffer = Some(render_device.create_buffer(&BufferDescriptor {
                label: Some("egui index buffer"),
                size: data.index_buffer_capacity as BufferAddress,
                usage: BufferUsages::COPY_DST | BufferUsages::INDEX,
                mapped_at_creation: false,
            }));
        }

        let (vertex_buffer, index_buffer) = match (&data.vertex_buffer, &data.index_buffer) {
            (Some(vertex), Some(index)) => (vertex, index),
            _ => {
                continue;
            }
        };

        render_queue.write_buffer(vertex_buffer, 0, &data.vertex_data);
        render_queue.write_buffer(index_buffer, 0, cast_slice(&data.index_data));
    }
}
