//! The `texture` module stores structs for interacting with image files
//! and loading them as opengl textures.
//!
//! The main struct is the `Texture` struct.

use core::ffi::c_void;
use image::EncodableLayout as _;

/// This struct stores the opengl id for a given 2D texture.
///
/// It is a wrapper around creating and binding an opengl `TEXTURE_2D`.
///
/// You still need to define attributes in the `VertexBuffer` and as inputs to
/// your vertex and fragment shader for the texture's coordinates.
pub struct Texture {
    /// Stores the opengl id for the 2d texture.
    ///
    /// This is used internally by the `Texture` struct. You can't use this
    /// directly, instead use the `bind` method to bind the texture to opengl's
    /// texture buffer.
    gl_id: u32,
}

impl Texture {
    /// Create a 2D `Texture` from the given slice of bytes.
    /// The texture is by default set to repeat when wrapping.
    ///
    /// ## Errors
    ///
    /// If some garbage data overflows, an error will be returned.
    /// Otherwise, an `Ok` variant containing a `Texture` is returned. You can
    /// bind (enable) this `Texture` each frame using its `bind()` method.
    #[expect(
        clippy::missing_inline_in_public_items,
        reason = "Long function, probably shouldn't be inlined for compile times and binary size reasons."
    )]
    pub fn load_bytes(width: u32, bytes: &[u8]) -> Result<Texture, String> {
        let mut gl_id = 0;

        unsafe {
            gl::GenTextures(1, &raw mut gl_id);
        }
        unsafe {
            gl::BindTexture(gl::TEXTURE_2D, gl_id);
        }
        #[expect(
            clippy::as_conversions,
            clippy::cast_possible_wrap,
            reason = "We are casting constants, impossible to cause issues."
        )]
        unsafe {
            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::REPEAT as i32);
        };
        #[expect(
            clippy::as_conversions,
            clippy::cast_possible_wrap,
            reason = "We are casting constants, impossible to cause issues."
        )]
        unsafe {
            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::REPEAT as i32);
        };
        #[expect(
            clippy::as_conversions,
            clippy::cast_possible_wrap,
            reason = "We are casting constants, impossible to cause issues."
        )]
        unsafe {
            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::NEAREST as i32);
        };
        #[expect(
            clippy::as_conversions,
            clippy::cast_possible_wrap,
            reason = "We are casting constants, impossible to cause issues."
        )]
        unsafe {
            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::NEAREST as i32);
        };

        unsafe {
            #[expect(
                clippy::borrow_as_ptr,
                clippy::indexing_slicing,
                clippy::as_conversions,
                reason = "can't find other way to specify pixels field"
            )]
            gl::TexImage2D(
                gl::TEXTURE_2D,
                0,
                gl::RGB
                    .try_into()
                    .map_err(|_e| "gl::RGB returned garbage data which overflowed")?,
                width
                    .try_into()
                    .map_err(|_e| "image width too large, led to overflow")?,
                width
                    .try_into()
                    .map_err(|_e| "image width too large, led to overflow")?,
                0,
                gl::RGBA,
                gl::UNSIGNED_BYTE,
                (&bytes[0] as *const u8).cast::<c_void>(),
            );
        };
        unsafe {
            gl::GenerateMipmap(gl::TEXTURE_2D);
        };

        Ok(Texture { gl_id })
    }

    /// Create a 2D `Texture` from the given filepath.
    /// The texture is by default set to repeat when wrapping.
    ///
    /// ## Errors
    ///
    /// If the file does not exist or cannot be opened, an `Err` variant
    /// containing a string of what went wrong is returned.
    /// If some garbage data overflows, a different error will be returned.
    /// Otherwise, an `Ok` variant containing a `Texture` is returned. You can
    /// bind (enable) this `Texture` each frame using its `bind()` method.
    ///
    /// ## Example usage:
    ///
    /// ``` rust
    /// let tex = Texture::new("res/texturemap.png");
    /// while w.is_running() {
    ///     tex.bind();
    ///     vertex_buffer.draw();
    /// }
    /// ```
    #[inline]
    pub fn load_file(path: &str) -> Result<Texture, String> {
        use std::path::Path;
        let img = image::open(Path::new(path))
            .map_err(|err| format!("Realms: could not open image file {path}: {err}"))?;
        let img = img.flipv();
        let rgba_data = img.to_rgba8();
        let data = rgba_data.as_bytes();

        Texture::load_bytes(img.width(), data)
    }

    /// Bind the texture. This makes it the 'in-use' texture by the GPU.
    /// While you can swap between many textures if you so wish, this is
    /// expensive. It is recommended to just bind one texture before the loop
    /// and use this texture as an *atlas* of many textures, accessed via
    /// coordinates *from 0.0 to 1.0*.
    ///
    /// ## Example usage:
    ///
    /// ``` rust
    /// let tex = Texture::new("res/texturemap.png");
    /// tex.bind();
    /// while w.is_running() {
    ///     w.new_frame();
    ///     // {...} //
    ///     vertex_buffer.draw();
    /// }
    /// ```
    #[inline]
    pub fn bind(&self) {
        unsafe {
            gl::BindTexture(gl::TEXTURE_2D, self.gl_id);
        }
    }
}
