use crate::data_types::dimens::Dimens;
use crate::texture::Properties;

#[derive(Debug)]
pub struct Texture
{
    texture: gl::types::GLuint,
}

impl Texture
{
    pub fn new() -> Self
    {
        let mut texture = gl::types::GLuint::default();

        unsafe {
            gl::CreateTextures(gl::TEXTURE_2D, 1, &mut texture);
        };

        Self { texture }
    }

    pub fn bind(&self)
    {
        unsafe {
            gl::BindTexture(gl::TEXTURE_2D, self.texture);
        }
    }

    pub fn generate(
        &mut self,
        dimens: Dimens<u32>,
        data: &[u8],
        pixel_data_format: PixelDataFormat,
    )
    {
        self.alloc_image(pixel_data_format, dimens, data);

        unsafe {
            gl::GenerateTextureMipmap(self.texture);
        }
    }

    pub fn apply_properties(&mut self, properties: &Properties)
    {
        self.set_wrap(properties.wrap);
        self.set_magnifying_filter(properties.magnifying_filter);
        self.set_minifying_filter(properties.minifying_filter);
    }

    pub fn set_wrap(&mut self, wrapping: Wrapping)
    {
        let wrapping_gl = wrapping.to_gl();

        #[allow(clippy::cast_possible_wrap)]
        unsafe {
            gl::TextureParameteri(self.texture, gl::TEXTURE_WRAP_S, wrapping_gl as i32);
            gl::TextureParameteri(self.texture, gl::TEXTURE_WRAP_T, wrapping_gl as i32);
        }
    }

    pub fn set_magnifying_filter(&mut self, filtering: Filtering)
    {
        let filtering_gl = filtering.to_gl();

        #[allow(clippy::cast_possible_wrap)]
        unsafe {
            gl::TextureParameteri(
                self.texture,
                gl::TEXTURE_MAG_FILTER,
                filtering_gl as i32,
            );
        }
    }

    pub fn set_minifying_filter(&mut self, filtering: Filtering)
    {
        let filtering_gl = filtering.to_gl();

        #[allow(clippy::cast_possible_wrap)]
        unsafe {
            gl::TextureParameteri(
                self.texture,
                gl::TEXTURE_MIN_FILTER,
                filtering_gl as i32,
            );
        }
    }

    fn alloc_image(
        &mut self,
        pixel_data_format: PixelDataFormat,
        dimens: Dimens<u32>,
        data: &[u8],
    )
    {
        unsafe {
            #[allow(clippy::cast_possible_wrap)]
            gl::TextureStorage2D(
                self.texture,
                1,
                pixel_data_format.to_sized_internal_format(),
                dimens.width as i32,
                dimens.height as i32,
            );

            #[allow(clippy::cast_possible_wrap)]
            gl::TextureSubImage2D(
                self.texture,
                0,
                0,
                0,
                dimens.width as i32,
                dimens.height as i32,
                pixel_data_format.to_format(),
                gl::UNSIGNED_BYTE,
                data.as_ptr().cast(),
            );
        }
    }
}

impl Drop for Texture
{
    fn drop(&mut self)
    {
        unsafe {
            gl::DeleteTextures(1, &self.texture);
        }
    }
}

/// Texture wrapping.
#[derive(Debug, Clone, Copy)]
pub enum Wrapping
{
    Repeat,
    MirroredRepeat,
    ClampToEdge,
    ClampToBorder,
}

impl Wrapping
{
    fn to_gl(self) -> gl::types::GLenum
    {
        match self {
            Self::Repeat => gl::REPEAT,
            Self::MirroredRepeat => gl::MIRRORED_REPEAT,
            Self::ClampToEdge => gl::CLAMP_TO_EDGE,
            Self::ClampToBorder => gl::CLAMP_TO_BORDER,
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub enum Filtering
{
    Nearest,
    Linear,
}

impl Filtering
{
    fn to_gl(self) -> gl::types::GLenum
    {
        match self {
            Self::Linear => gl::LINEAR,
            Self::Nearest => gl::NEAREST,
        }
    }
}

/// Texture pixel data format.
#[derive(Debug, Clone, Copy)]
pub enum PixelDataFormat
{
    Rgb8,
    Rgba8,
}

impl PixelDataFormat
{
    fn to_sized_internal_format(self) -> gl::types::GLenum
    {
        match self {
            Self::Rgb8 => gl::RGB8,
            Self::Rgba8 => gl::RGBA8,
        }
    }

    fn to_format(self) -> gl::types::GLenum
    {
        match self {
            Self::Rgb8 => gl::RGB,
            Self::Rgba8 => gl::RGBA,
        }
    }
}

pub fn set_active_texture_unit(texture_unit: TextureUnit)
{
    unsafe {
        gl::ActiveTexture(texture_unit.into_gl());
    }
}

macro_rules! texture_unit_enum {
    (cnt=$cnt: literal) => {
        seq_macro::seq!(N in 0..$cnt {
            #[derive(Debug, Clone, Copy)]
            pub enum TextureUnit {
                #(
                    No~N,
                )*
            }

            impl TextureUnit {
                fn into_gl(self) -> gl::types::GLenum {
                    match self {
                        #(
                            Self::No~N => gl::TEXTURE~N,
                        )*
                    }
                }

                pub fn from_num(num: usize) -> Option<Self> {
                    match num {
                        #(
                            N => Some(Self::No~N),
                        )*
                        _ => None
                    }
                }
            }
        });
    };
}

texture_unit_enum!(cnt = 31);