use super::*;
use super::errors::*;

use std::os::raw::c_void;

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct TextureId(u32);

impl TextureId {
    pub fn empty() -> TextureId {
        TextureId(0)
    }
}

impl From<TextureId> for u32 {
    fn from(texture_id: TextureId) -> Self {
        texture_id.0
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct TextureBindlessId(u64);

impl TextureBindlessId {
    pub fn empty() -> TextureBindlessId {
        TextureBindlessId(0)
    }
}

impl From<TextureBindlessId> for u64 {
    fn from(texture_bindless_id: TextureBindlessId) -> Self {
        texture_bindless_id.0
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TextureTarget {
    Texture1D,
    Texture2D,
    Texture3D,
    Texture1DArray,
    Texture2DArray,
    TextureRectangle,
    TextureCubeMap,
    TextureCubeMapArray,
    TextureBuffer,
    Texture2DMultisample,
    Texture2DMultisampleArray,
}

impl From<TextureTarget> for u32 {
    fn from(texture_target: TextureTarget) -> Self {
        use self::TextureTarget::*;
        match texture_target {
            Texture1D => gl::TEXTURE_1D,
            Texture2D => gl::TEXTURE_2D,
            Texture3D => gl::TEXTURE_3D,
            Texture1DArray => gl::TEXTURE_1D_ARRAY,
            Texture2DArray => gl::TEXTURE_2D_ARRAY,
            TextureRectangle => gl::TEXTURE_RECTANGLE,
            TextureCubeMap => gl::TEXTURE_CUBE_MAP,
            TextureCubeMapArray => gl::TEXTURE_CUBE_MAP_ARRAY,
            TextureBuffer => gl::TEXTURE_BUFFER,
            Texture2DMultisample => gl::TEXTURE_2D_MULTISAMPLE,
            Texture2DMultisampleArray => gl::TEXTURE_2D_MULTISAMPLE_ARRAY,
        }
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TextureInternalFormat {
    R8,
    R8SN,
    R8I,
    R8UI,
    R16,
    R16F,
    R16SN,
    R16I,
    R16UI,
    R32F,
    R32I,
    R32UI,

    RG8,
    RG8SN,
    RG8I,
    RG8UI,
    RG16,
    RG16F,
    RG16SN,
    RG16I,
    RG16UI,
    RG32F,
    RG32I,
    RG32UI,

    RGB8,
    RGB8SN,
    RGB8I,
    RGB8UI,
    RGB16,
    RGB16F,
    RGB16SN,
    RGB16I,
    RGB16UI,
    RGB32F,
    RGB32I,
    RGB32UI,

    RGBA8,
    RGBA8SN,
    RGBA8I,
    RGBA8UI,
    RGBA16,
    RGBA16F,
    RGBA16SN,
    RGBA16I,
    RGBA16UI,
    RGBA32F,
    RGBA32I,
    RGBA32UI,

    DepthComponent16F,
    DepthComponent24F,
    DepthComponent32F,
    StencilIndex8U,
    Depth24FStencil8U,
    Depth32FStencil8U,
}

impl From<TextureInternalFormat> for gl::types::GLenum {
    fn from(internal_format: TextureInternalFormat) -> gl::types::GLenum {
        use self::TextureInternalFormat::*;
        match internal_format {
            R8 => gl::R8,
            R8SN => gl::R8_SNORM,
            R8I => gl::R8I,
            R8UI => gl::R8UI,
            R16 => gl::R16,
            R16F => gl::R16F,
            R16SN => gl::R16_SNORM,
            R16I => gl::R16I,
            R16UI => gl::R16UI,
            R32F => gl::R32F,
            R32I => gl::R32I,
            R32UI => gl::R32UI,

            RG8 => gl::RG8,
            RG8SN => gl::RG8_SNORM,
            RG8I => gl::RG8I,
            RG8UI => gl::RG8UI,
            RG16 => gl::RG16,
            RG16F => gl::RG16F,
            RG16SN => gl::RG16_SNORM,
            RG16I => gl::RG16I,
            RG16UI => gl::RG16UI,
            RG32F => gl::RG32F,
            RG32I => gl::RG32I,
            RG32UI => gl::RG32UI,

            RGB8 => gl::RGB8,
            RGB8SN => gl::RGB8_SNORM,
            RGB8I => gl::RGB8I,
            RGB8UI => gl::RGB8UI,
            RGB16 => gl::RGB16,
            RGB16F => gl::RGB16F,
            RGB16SN => gl::RGB16_SNORM,
            RGB16I => gl::RGB16I,
            RGB16UI => gl::RGB16UI,
            RGB32F => gl::RGB32F,
            RGB32I => gl::RGB32I,
            RGB32UI => gl::RGB32UI,

            RGBA8 => gl::RGBA8,
            RGBA8SN => gl::RGBA8_SNORM,
            RGBA8I => gl::RGBA8I,
            RGBA8UI => gl::RGBA8UI,
            RGBA16 => gl::RGBA16,
            RGBA16F => gl::RGBA16F,
            RGBA16SN => gl::RGBA16_SNORM,
            RGBA16I => gl::RGBA16I,
            RGBA16UI => gl::RGBA16UI,
            RGBA32F => gl::RGBA32F,
            RGBA32I => gl::RGBA32I,
            RGBA32UI => gl::RGBA32UI,

            DepthComponent16F => gl::DEPTH_COMPONENT16,
            DepthComponent24F => gl::DEPTH_COMPONENT24,
            DepthComponent32F => gl::DEPTH_COMPONENT32F,
            StencilIndex8U => gl::STENCIL_INDEX8,
            Depth24FStencil8U => gl::DEPTH24_STENCIL8,
            Depth32FStencil8U => gl::DEPTH32F_STENCIL8,
        }
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TextureBufferInternalFormat {
    R8,
    R8I,
    R8UI,
    R16,
    R16F,
    R16I,
    R16UI,
    R32F,
    R32I,
    R32UI,

    RG8,
    RG8I,
    RG8UI,
    RG16,
    RG16F,
    RG16I,
    RG16UI,
    RG32F,
    RG32I,
    RG32UI,

    RGB32F,
    RGB32I,
    RGB32UI,

    RGBA8,
    RGBA8I,
    RGBA8UI,
    RGBA16,
    RGBA16F,
    RGBA16I,
    RGBA16UI,
    RGBA32F,
    RGBA32I,
    RGBA32UI,
}

impl From<TextureBufferInternalFormat> for gl::types::GLenum {
    fn from(internal_format: TextureBufferInternalFormat) -> gl::types::GLenum {
        use self::TextureBufferInternalFormat::*;
        match internal_format {
            R8 => gl::R8,
            R8I => gl::R8I,
            R8UI => gl::R8UI,
            R16 => gl::R16,
            R16F => gl::R16F,
            R16I => gl::R16I,
            R16UI => gl::R16UI,
            R32F => gl::R32F,
            R32I => gl::R32I,
            R32UI => gl::R32UI,

            RG8 => gl::RG8,
            RG8I => gl::RG8I,
            RG8UI => gl::RG8UI,
            RG16 => gl::RG16,
            RG16F => gl::RG16F,
            RG16I => gl::RG16I,
            RG16UI => gl::RG16UI,
            RG32F => gl::RG32F,
            RG32I => gl::RG32I,
            RG32UI => gl::RG32UI,

            RGB32F => gl::RGB32F,
            RGB32I => gl::RGB32I,
            RGB32UI => gl::RGB32UI,

            RGBA8 => gl::RGBA8,
            RGBA8I => gl::RGBA8I,
            RGBA8UI => gl::RGBA8UI,
            RGBA16 => gl::RGBA16,
            RGBA16F => gl::RGBA16F,
            RGBA16I => gl::RGBA16I,
            RGBA16UI => gl::RGBA16UI,
            RGBA32F => gl::RGBA32F,
            RGBA32I => gl::RGBA32I,
            RGBA32UI => gl::RGBA32UI,
        }
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TextureDataType {
    UnsignedByte,
    Byte,
    UnsignedShort,
    Short,
    UnsignedInt,
    Int,
    Float,
}

impl From<TextureDataType> for gl::types::GLenum {
    fn from(texture_data_type: TextureDataType) -> Self {
        match texture_data_type {
            TextureDataType::UnsignedByte => gl::UNSIGNED_BYTE,
            TextureDataType::Byte => gl::BYTE,
            TextureDataType::UnsignedShort => gl::UNSIGNED_SHORT,
            TextureDataType::Short => gl::SHORT,
            TextureDataType::UnsignedInt => gl::UNSIGNED_INT,
            TextureDataType::Int => gl::INT,
            TextureDataType::Float => gl::FLOAT,
        }
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TextureFormat {
    Red,
    RG,
    RGB,
    BGR,
    RGBA,
    BGRA,
    DepthComponent,
    StencilIndex,
}

impl From<TextureFormat> for gl::types::GLenum {
    fn from(format: TextureFormat) -> Self {
        match format {
            TextureFormat::Red => gl::RED,
            TextureFormat::RG => gl::RG,
            TextureFormat::RGB => gl::RGB,
            TextureFormat::BGR => gl::BGR,
            TextureFormat::RGBA => gl::RGBA,
            TextureFormat::BGRA => gl::BGRA,
            TextureFormat::DepthComponent => gl::DEPTH_COMPONENT,
            TextureFormat::StencilIndex => gl::STENCIL_INDEX,
        }
    }
}

impl TextureFormat {
    pub fn n_components(&self) -> usize {
        use self::TextureFormat::*;
        match *self {
            Red => 1,
            RG => 2,
            RGB => 3,
            BGR => 3,
            RGBA => 4,
            BGRA => 4,
            DepthComponent => 1,
            StencilIndex => 1,
        }
    }
}

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

impl Default for TextureTexelFilter {
    fn default() -> TextureTexelFilter {
        TextureTexelFilter::Nearest
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TextureWrapMode {
    ClampToEdge,
    ClampToBorder,
    Repeat,
    MirroredRepeat,
    MirrorClampToEdge,
}

impl Default for TextureWrapMode {
    fn default() -> TextureWrapMode {
        TextureWrapMode::Repeat
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TextureMipmapFilter {
    None,
    Nearest,
    Linear,
}

impl Default for TextureMipmapFilter {
    fn default() -> TextureMipmapFilter {
        TextureMipmapFilter::None
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TextureSeamless {
    True,
    False,
}

impl From<TextureSeamless> for bool {
    fn from(seamless: TextureSeamless) -> Self {
        match seamless {
            TextureSeamless::True => true,
            TextureSeamless::False => false,
        }
    }
}

impl Default for TextureSeamless {
    fn default() -> TextureSeamless {
        TextureSeamless::False
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TextureParameterOption {
    MinFilter(TextureTexelFilter, TextureMipmapFilter),
    MagFilter(TextureTexelFilter),
    WrapS(TextureWrapMode),
    WrapT(TextureWrapMode),
    WrapR(TextureWrapMode),
    CubemapSeamless(TextureSeamless),
}

impl From<TextureParameterOption> for (gl::types::GLenum, gl::types::GLenum) {
    fn from(filter: TextureParameterOption) -> Self {
        match filter {
            TextureParameterOption::MinFilter(texel, mipmap) => {
                match (texel, mipmap) {
                    (TextureTexelFilter::Nearest, TextureMipmapFilter::None) => {
                        (gl::TEXTURE_MIN_FILTER, gl::NEAREST)
                    }
                    (TextureTexelFilter::Nearest, TextureMipmapFilter::Nearest) => {
                        (gl::TEXTURE_MIN_FILTER, gl::NEAREST_MIPMAP_NEAREST)
                    }
                    (TextureTexelFilter::Nearest, TextureMipmapFilter::Linear) => {
                        (gl::TEXTURE_MIN_FILTER, gl::NEAREST_MIPMAP_LINEAR)
                    }
                    (TextureTexelFilter::Linear, TextureMipmapFilter::None) => {
                        (gl::TEXTURE_MIN_FILTER, gl::LINEAR)
                    }
                    (TextureTexelFilter::Linear, TextureMipmapFilter::Nearest) => {
                        (gl::TEXTURE_MIN_FILTER, gl::LINEAR_MIPMAP_NEAREST)
                    }
                    (TextureTexelFilter::Linear, TextureMipmapFilter::Linear) => {
                        (gl::TEXTURE_MIN_FILTER, gl::LINEAR_MIPMAP_LINEAR)
                    }
                }
            }
            TextureParameterOption::MagFilter(texel) => {
                match texel {
                    TextureTexelFilter::Nearest => (gl::TEXTURE_MAG_FILTER, gl::NEAREST),
                    TextureTexelFilter::Linear => (gl::TEXTURE_MAG_FILTER, gl::LINEAR),
                }
            }
            TextureParameterOption::WrapS(mode) => {
                match mode {
                    TextureWrapMode::ClampToEdge => (gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE),
                    TextureWrapMode::ClampToBorder => (gl::TEXTURE_WRAP_S, gl::CLAMP_TO_BORDER),
                    TextureWrapMode::Repeat => (gl::TEXTURE_WRAP_S, gl::REPEAT),
                    TextureWrapMode::MirroredRepeat => (gl::TEXTURE_WRAP_S, gl::MIRRORED_REPEAT),
                    TextureWrapMode::MirrorClampToEdge => {
                        (gl::TEXTURE_WRAP_S, gl::MIRROR_CLAMP_TO_EDGE)
                    }
                }
            }
            TextureParameterOption::WrapT(mode) => {
                match mode {
                    TextureWrapMode::ClampToEdge => (gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE),
                    TextureWrapMode::ClampToBorder => (gl::TEXTURE_WRAP_T, gl::CLAMP_TO_BORDER),
                    TextureWrapMode::Repeat => (gl::TEXTURE_WRAP_T, gl::REPEAT),
                    TextureWrapMode::MirroredRepeat => (gl::TEXTURE_WRAP_T, gl::MIRRORED_REPEAT),
                    TextureWrapMode::MirrorClampToEdge => {
                        (gl::TEXTURE_WRAP_T, gl::MIRROR_CLAMP_TO_EDGE)
                    }
                }
            }
            TextureParameterOption::WrapR(mode) => {
                match mode {
                    TextureWrapMode::ClampToEdge => (gl::TEXTURE_WRAP_R, gl::CLAMP_TO_EDGE),
                    TextureWrapMode::ClampToBorder => (gl::TEXTURE_WRAP_R, gl::CLAMP_TO_BORDER),
                    TextureWrapMode::Repeat => (gl::TEXTURE_WRAP_R, gl::REPEAT),
                    TextureWrapMode::MirroredRepeat => (gl::TEXTURE_WRAP_R, gl::MIRRORED_REPEAT),
                    TextureWrapMode::MirrorClampToEdge => {
                        (gl::TEXTURE_WRAP_R, gl::MIRROR_CLAMP_TO_EDGE)
                    }
                }
            }
            TextureParameterOption::CubemapSeamless(seamless) => {
                match seamless {
                    TextureSeamless::True => (gl::TEXTURE_CUBE_MAP_SEAMLESS, gl::TRUE as u32),
                    TextureSeamless::False => (gl::TEXTURE_CUBE_MAP_SEAMLESS, gl::FALSE as u32),
                }
            }
        }
    }
}

pub unsafe fn create_texture(target: TextureTarget) -> TextureId {
    let mut texture_id: u32 = 0;
    let texture_id_ptr: *mut u32 = &mut texture_id;

    gl::CreateTextures(target.into(), 1, texture_id_ptr);

    TextureId(texture_id)
}

pub unsafe fn texture_storage_1d(
    texture: TextureId,
    levels: usize,
    internal_format: TextureInternalFormat,
    width: usize,
) -> Result<()> {
    gl::TextureStorage1D(
        texture.into(),
        levels as i32,
        internal_format.into(),
        width as i32,
    );

    get_error(())
}

pub unsafe fn texture_storage_2d(
    texture: TextureId,
    levels: usize,
    internal_format: TextureInternalFormat,
    dimensions: (usize, usize),
) -> Result<()> {
    gl::TextureStorage2D(
        texture.into(),
        levels as i32,
        internal_format.into(),
        dimensions.0 as i32,
        dimensions.1 as i32,
    );

    get_error(())
}

pub unsafe fn texture_storage_3d(
    texture: TextureId,
    levels: usize,
    internal_format: TextureInternalFormat,
    dimensions: (usize, usize, usize),
) -> Result<()> {
    gl::TextureStorage3D(
        texture.into(),
        levels as i32,
        internal_format.into(),
        dimensions.0 as i32,
        dimensions.1 as i32,
        dimensions.2 as i32,
    );

    get_error(())
}


// TODO create format & type enum
pub unsafe fn texture_subimage_1d<T>(
    texture: TextureId,
    level: usize,
    offset: usize,
    width: usize,
    format: TextureFormat,
    data_type: u32,
    pixels: &[T],
) -> Result<()> {
    let pixels_ptr = pixels.as_ptr() as *const c_void;
    gl::TextureSubImage1D(
        texture.into(),
        level as i32,
        offset as i32,
        width as i32,
        format.into(),
        data_type,
        pixels_ptr,
    );

    get_error(())
}

pub unsafe fn texture_subimage_2d<T>(
    texture: TextureId,
    level: usize,
    offset: (usize, usize),
    dimensions: (usize, usize),
    format: TextureFormat,
    data_type: u32,
    pixels: &[T],
) -> Result<()> {
    let pixels_ptr = pixels.as_ptr() as *const c_void;
    gl::TextureSubImage2D(
        texture.into(),
        level as i32,
        offset.0 as i32,
        offset.1 as i32,
        dimensions.0 as i32,
        dimensions.1 as i32,
        format.into(),
        data_type,
        pixels_ptr,
    );

    get_error(())
}

pub unsafe fn texture_subimage_3d<T>(
    texture: TextureId,
    level: usize,
    offset: (usize, usize, usize),
    dimensions: (usize, usize, usize),
    format: TextureFormat,
    data_type: u32,
    pixels: &[T],
) -> Result<()> {
    let pixels_ptr = pixels.as_ptr() as *const c_void;
    gl::TextureSubImage3D(
        texture.into(),
        level as i32,
        offset.0 as i32,
        offset.1 as i32,
        offset.2 as i32,
        dimensions.0 as i32,
        dimensions.1 as i32,
        dimensions.2 as i32,
        format.into(),
        data_type,
        pixels_ptr,
    );

    get_error(())
}

pub unsafe fn texture_buffer(
    texture_id: TextureId,
    format: TextureBufferInternalFormat,
    buffer_id: BufferId,
) -> Result<()> {
    gl::TextureBuffer(texture_id.into(), format.into(), buffer_id.into());

    get_error(())
}

pub unsafe fn texture_buffer_range(
    texture_id: TextureId,
    format: TextureBufferInternalFormat,
    buffer_id: BufferId,
    offset: usize,
    size: usize,
) -> Result<()> {
    gl::TextureBufferRange(
        texture_id.into(),
        format.into(),
        buffer_id.into(),
        offset as isize,
        size as isize,
    );

    get_error(())
}

pub unsafe fn delete_texture(id: TextureId) {
    gl::DeleteTextures(1, &id.into());
}

pub unsafe fn texture_parameter_i(
    texture: TextureId,
    parameter: TextureParameterOption,
) -> Result<()> {
    let (pname, param) = parameter.into();
    gl::TextureParameteri(texture.into(), pname, param as i32);

    get_error(())
}

pub unsafe fn get_texture_handle(id: TextureId) -> Result<TextureBindlessId> {
    let handle = gl::GetTextureHandleARB(id.into());

    get_error(TextureBindlessId(handle))
}

pub unsafe fn make_texture_handle_resident(id: TextureBindlessId) -> Result<()> {
    gl::MakeTextureHandleResidentARB(id.into());

    get_error(())
}

pub unsafe fn make_texture_handle_non_resident(id: TextureBindlessId) -> Result<()> {
    gl::MakeTextureHandleNonResidentARB(id.into());

    get_error(())
}

pub unsafe fn generate_texture_mipmap(id: TextureId) -> Result<()> {
    gl::GenerateTextureMipmap(id.into());

    get_error(())
}
