use crate::{ImageData, ImageDelta, TextureId};
use ahash::{HashMap, HashSet};
use smallvec::{SmallVec, smallvec};
use std::mem;

// ----------------------------------------------------------------------------

/// Low-level manager for allocating textures.
///
/// Communicates with the painting subsystem using [`Self::take_delta`].
#[derive(Default)]
pub struct TextureManager {
    /// We allocate texture id:s linearly.
    next_id: u64,

    /// Information about currently allocated textures.
    metas: ahash::HashMap<TextureId, TextureMeta>,

    delta: TexturesDelta,
}

impl TextureManager {
    /// Allocate a new texture.
    ///
    /// The given name can be useful for later debugging.
    ///
    /// The returned [`TextureId`] will be [`TextureId::Managed`], with an index
    /// starting from zero and increasing with each call to [`Self::alloc`].
    ///
    /// The first texture you allocate will be `TextureId::Managed(0) == TextureId::default()` and
    /// MUST have a white pixel at (0,0) ([`crate::WHITE_UV`]).
    ///
    /// The texture is given a retain-count of `1`, requiring one call to [`Self::free`] to free it.
    pub fn alloc(&mut self, name: String, image: ImageData, options: TextureOptions) -> TextureId {
        let id = TextureId::Managed(self.next_id);
        self.next_id += 1;

        self.metas.entry(id).or_insert_with(|| TextureMeta {
            name,
            size: image.size(),
            bytes_per_pixel: image.bytes_per_pixel(),
            retain_count: 1,
            options,
        });

        self.delta.push(id, ImageDelta::full(image, options));
        id
    }

    /// Assign a new image to an existing texture,
    /// or update a region of it.
    pub fn set(&mut self, id: TextureId, delta: ImageDelta) {
        if let Some(meta) = self.metas.get_mut(&id) {
            if let Some(pos) = delta.pos {
                debug_assert!(
                    pos[0] + delta.image.width() <= meta.size[0]
                        && pos[1] + delta.image.height() <= meta.size[1],
                    "Partial texture update is outside the bounds of texture {id:?}",
                );
            } else {
                // whole update
                meta.size = delta.image.size();
                meta.bytes_per_pixel = delta.image.bytes_per_pixel();
            }
            self.delta.push(id, delta);
        } else {
            debug_assert!(false, "Tried setting texture {id:?} which is not allocated");
        }
    }

    /// Free an existing texture.
    pub fn free(&mut self, id: TextureId) {
        if let std::collections::hash_map::Entry::Occupied(mut entry) = self.metas.entry(id) {
            let meta = entry.get_mut();
            meta.retain_count -= 1;
            if meta.retain_count == 0 {
                entry.remove();
                self.delta.free(id);
            }
        } else {
            debug_assert!(false, "Tried freeing texture {id:?} which is not allocated");
        }
    }

    /// Increase the retain-count of the given texture.
    ///
    /// For each time you call [`Self::retain`] you must call [`Self::free`] on additional time.
    pub fn retain(&mut self, id: TextureId) {
        if let Some(meta) = self.metas.get_mut(&id) {
            meta.retain_count += 1;
        } else {
            debug_assert!(
                false,
                "Tried retaining texture {id:?} which is not allocated",
            );
        }
    }

    /// Take and reset changes since last frame.
    ///
    /// These should be applied to the painting subsystem each frame.
    pub fn take_delta(&mut self) -> TexturesDelta {
        std::mem::take(&mut self.delta)
    }

    /// Get meta-data about a specific texture.
    pub fn meta(&self, id: TextureId) -> Option<&TextureMeta> {
        self.metas.get(&id)
    }

    /// Get meta-data about all allocated textures in some arbitrary order.
    pub fn allocated(&self) -> impl ExactSizeIterator<Item = (&TextureId, &TextureMeta)> {
        self.metas.iter()
    }

    /// Total number of allocated textures.
    pub fn num_allocated(&self) -> usize {
        self.metas.len()
    }
}

impl Drop for TextureManager {
    fn drop(&mut self) {
        self.delta.clear(); // Prevent a debug panic on application shutdown
    }
}

/// Meta-data about an allocated texture.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TextureMeta {
    /// A human-readable name useful for debugging.
    pub name: String,

    /// width x height
    pub size: [usize; 2],

    /// 4 or 1
    pub bytes_per_pixel: usize,

    /// Free when this reaches zero.
    pub retain_count: usize,

    /// The texture filtering mode to use when rendering.
    pub options: TextureOptions,
}

impl TextureMeta {
    /// Size in bytes.
    /// width x height x [`Self::bytes_per_pixel`].
    pub fn bytes_used(&self) -> usize {
        self.size[0] * self.size[1] * self.bytes_per_pixel
    }
}

// ----------------------------------------------------------------------------

/// How the texture texels are filtered.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct TextureOptions {
    /// How to filter when magnifying (when texels are larger than pixels).
    pub magnification: TextureFilter,

    /// How to filter when minifying (when texels are smaller than pixels).
    pub minification: TextureFilter,

    /// How to wrap the texture when the texture coordinates are outside the [0, 1] range.
    pub wrap_mode: TextureWrapMode,

    /// How to filter between texture mipmaps.
    ///
    /// Mipmaps ensures textures look smooth even when the texture is very small and pixels are much
    /// larger than individual texels.
    ///
    /// # Notes
    ///
    /// - This may not be available on all backends (currently only `egui_glow`).
    pub mipmap_mode: Option<TextureFilter>,
}

impl TextureOptions {
    /// Linear magnification and minification.
    pub const LINEAR: Self = Self {
        magnification: TextureFilter::Linear,
        minification: TextureFilter::Linear,
        wrap_mode: TextureWrapMode::ClampToEdge,
        mipmap_mode: None,
    };

    /// Nearest magnification and minification.
    pub const NEAREST: Self = Self {
        magnification: TextureFilter::Nearest,
        minification: TextureFilter::Nearest,
        wrap_mode: TextureWrapMode::ClampToEdge,
        mipmap_mode: None,
    };

    /// Linear magnification and minification, but with the texture repeated.
    pub const LINEAR_REPEAT: Self = Self {
        magnification: TextureFilter::Linear,
        minification: TextureFilter::Linear,
        wrap_mode: TextureWrapMode::Repeat,
        mipmap_mode: None,
    };

    /// Linear magnification and minification, but with the texture mirrored and repeated.
    pub const LINEAR_MIRRORED_REPEAT: Self = Self {
        magnification: TextureFilter::Linear,
        minification: TextureFilter::Linear,
        wrap_mode: TextureWrapMode::MirroredRepeat,
        mipmap_mode: None,
    };

    /// Nearest magnification and minification, but with the texture repeated.
    pub const NEAREST_REPEAT: Self = Self {
        magnification: TextureFilter::Nearest,
        minification: TextureFilter::Nearest,
        wrap_mode: TextureWrapMode::Repeat,
        mipmap_mode: None,
    };

    /// Nearest magnification and minification, but with the texture mirrored and repeated.
    pub const NEAREST_MIRRORED_REPEAT: Self = Self {
        magnification: TextureFilter::Nearest,
        minification: TextureFilter::Nearest,
        wrap_mode: TextureWrapMode::MirroredRepeat,
        mipmap_mode: None,
    };

    pub const fn with_mipmap_mode(self, mipmap_mode: Option<TextureFilter>) -> Self {
        Self {
            mipmap_mode,
            ..self
        }
    }
}

impl Default for TextureOptions {
    /// The default is linear for both magnification and minification.
    fn default() -> Self {
        Self::LINEAR
    }
}

/// How the texture texels are filtered.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum TextureFilter {
    /// Show the nearest pixel value.
    ///
    /// When zooming in you will get sharp, square pixels/texels.
    /// When zooming out you will get a very crisp (and aliased) look.
    Nearest,

    /// Linearly interpolate the nearest neighbors, creating a smoother look when zooming in and out.
    Linear,
}

/// Defines how textures are wrapped around objects when texture coordinates fall outside the [0, 1] range.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum TextureWrapMode {
    /// Stretches the edge pixels to fill beyond the texture's bounds.
    ///
    /// This is what you want to use for a normal image in a GUI.
    #[default]
    ClampToEdge,

    /// Tiles the texture across the surface, repeating it horizontally and vertically.
    Repeat,

    /// Mirrors the texture with each repetition, creating symmetrical tiling.
    MirroredRepeat,
}

// ----------------------------------------------------------------------------

/// What has been allocated and freed during the last period.
///
/// These are commands given to the integration painter.
#[derive(Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[must_use = "The painter must take care of this"]
pub struct TexturesDelta {
    /// New or changed textures. Apply before painting.
    pub set: HashMap<TextureId, SmallVec<[ImageDelta; 1]>>,

    /// Textures to free after painting.
    pub free: HashSet<TextureId>,
}

impl TexturesDelta {
    pub fn is_empty(&self) -> bool {
        self.set.is_empty() && self.free.is_empty()
    }

    /// Inserts a [`ImageDelta`].
    ///
    /// If this [`TexturesDelta`] already contains this [`TextureId`], and this is a `whole` delta,
    /// the previous deltas for this id are discarded.
    pub fn push(&mut self, id: TextureId, delta: ImageDelta) {
        if delta.is_whole() {
            // It replaces the whole texture, fine to overwrite any previous deltas
            self.set.insert(id, smallvec![delta]);
        } else {
            self.set.entry(id).or_default().push(delta);
        }
    }

    pub fn free(&mut self, id: TextureId) {
        self.free.insert(id);
    }

    #[expect(clippy::iter_over_hash_type)]
    pub fn append(&mut self, mut newer: Self) {
        // Only clear previous entries on append, not on set, since within a frame a texture might
        // be created and immediately removed again.
        for id in &newer.free {
            self.set.remove(id);
        }
        for (id, deltas) in newer.set.drain() {
            for delta in deltas {
                self.push(id, delta);
            }
        }
        self.free.extend(mem::take(&mut newer.free));
    }

    pub fn clear(&mut self) {
        self.set.clear();
        self.free.clear();
    }
}

impl Drop for TexturesDelta {
    fn drop(&mut self) {
        debug_assert!(
            self.is_empty(),
            "Dropped TexturesDelta with {} unapplied deltas. Deltas need to be handled. \
            If you want to drop this intentionally call `clear` before dropping.",
            self.free.len() + self.set.len()
        );
    }
}

impl std::fmt::Debug for TexturesDelta {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        use std::fmt::Write as _;

        let mut debug_struct = f.debug_struct("TexturesDelta");
        if !self.set.is_empty() {
            let mut string = String::new();
            #[expect(clippy::iter_over_hash_type)]
            for (tex_id, deltas) in &self.set {
                for delta in deltas {
                    let size = delta.image.size();
                    if let Some(pos) = delta.pos {
                        write!(
                            string,
                            "{:?} partial ([{} {}] - [{} {}]), ",
                            tex_id,
                            pos[0],
                            pos[1],
                            pos[0] + size[0],
                            pos[1] + size[1]
                        )
                        .ok();
                    } else {
                        write!(string, "{:?} full {}x{}, ", tex_id, size[0], size[1]).ok();
                    }
                }
            }
            debug_struct.field("set", &string);
        }
        if !self.free.is_empty() {
            debug_struct.field("free", &self.free);
        }
        debug_struct.finish()
    }
}
