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

use std::cell::Cell;
use std::cmp::max;

#[derive(Debug)]
pub struct Texture2D {
    id: TextureId,
    bindless_id: TextureBindlessId,

    pub size: Texture2DSize,
    pub internal_format: TextureInternalFormat,
    pub format: TextureFormat,
    pub data_type: TextureDataType,

    bindless_counter: Cell<usize>,
}

#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Texture2DSize(pub usize, pub usize);

#[derive(Debug, Copy, Clone, PartialEq, Default)]
pub struct Texture2DParameters {
    pub min: TextureTexelFilter,
    pub mag: TextureTexelFilter,
    pub mipmap: TextureMipmapFilter,
    pub wrap_s: TextureWrapMode,
    pub wrap_t: TextureWrapMode,
}

impl Texture for Texture2D {
    fn get_id(&self) -> TextureId {
        self.id
    }

    fn get_bindless_texture_id(&self) -> TextureBindlessId {
        self.bindless_id
    }

    fn make_resident_texture(&self) -> ResidentTexture {
        ResidentTexture::new(self.get_bindless_texture_id(), &self.bindless_counter)
    }

    fn get_target(&self) -> TextureTarget {
        TextureTarget::Texture2D
    }
}

impl Texture2D {
    pub fn new<T: TextureDataPrimitive>(
        size: Texture2DSize,
        internal_format: TextureInternalFormat,
        format: TextureFormat,
        data_type: TextureDataType,
        parameters: Texture2DParameters,
        levels: usize,
        data: &[T],
    ) -> Result<Texture2D> {
        let mut texture =
            Texture2D::new_empty(size, internal_format, format, data_type, parameters, levels)
                .chain_err(|| "Could not create texture")?;

        texture.set_data(data).chain_err(
            || "Could not set data of texture",
        )?;

        Ok(texture)
    }

    pub fn new_empty(
        size: Texture2DSize,
        internal_format: TextureInternalFormat,
        format: TextureFormat,
        data_type: TextureDataType,
        parameters: Texture2DParameters,
        levels: usize,
    ) -> Result<Texture2D> {
        unsafe {
            let id = create_texture(TextureTarget::Texture2D);

            let mut texture = Texture2D {
                id: id,
                bindless_id: TextureBindlessId::empty(), // placeholder

                size: size,
                internal_format: internal_format,
                format: format,
                data_type: data_type,

                bindless_counter: Cell::new(0),
            };

            texture_storage_2d(id, levels, internal_format, (size.0, size.1))
                .chain_err(|| "Unable to allocate memory for texture")?;

            texture.set_parameters(parameters).chain_err(
                || "Could not set parameters",
            )?;

            texture.bindless_id = get_texture_handle(id.into()).chain_err(
                || "Could not create bindless id",
            )?;

            Ok(texture)
        }
    }

    pub fn make_resident_image(
        &self,
        level: usize,
        internal_format: ImageInternalFormat,
        access: ImageAccess,
    ) -> Result<ResidentImage> {
        let bindless_id = unsafe {
            get_image_handle(self.id, level, Some(0), internal_format)
                .chain_err(|| "Could not create image handle")?
        };

        ResidentImage::new(bindless_id, access)
    }

    pub fn set_data<T: TextureDataPrimitive>(&mut self, data: &[T]) -> Result<()> {
        if self.size.0 * self.size.1 * self.format.n_components() != data.len() {
            bail!("Data length does not match texture size");
        }
        unsafe {
            texture_subimage_2d(
                self.id,
                0,
                (0, 0),
                (self.size.0, self.size.1),
                self.format.into(),
                self.data_type.into(),
                data,
            ).chain_err(|| "Could not set texture data")?;
        }
        Ok(())
    }

    pub fn set_subdata<T: TextureDataPrimitive>(
        &mut self,
        size: Texture2DSize,
        offset: Texture2DSize,
        data: &[T],
    ) -> Result<()> {
        if size.0 * size.1 * self.format.n_components() != data.len() {
            bail!("Data length does not match proclaimed size");
        }
        if size.0 + offset.0 >= self.size.0 || size.1 + offset.1 >= self.size.1 {
            bail!("Size + offset is outside texture bounds");
        }
        unsafe {
            texture_subimage_2d(
                self.id,
                0,
                (offset.0, offset.1),
                (size.0, size.1),
                self.format.into(),
                self.data_type.into(),
                data,
            ).chain_err(|| "Could not set texture data")?;
        }
        Ok(())
    }

    pub fn set_data_mipmap<T: TextureDataPrimitive>(
        &mut self,
        level: usize,
        data: &[T],
    ) -> Result<()> {
        if discrete_floored_log2(max(self.size.0, self.size.1)) < level {
            bail!("Mipmap level references greater level than texture has");
        }
        if texture_size_mipmap(self.size.0, level) * texture_size_mipmap(self.size.1, level) *
            self.format.n_components() !=
            data.len()
        {
            bail!("Data length does not match texture size");
        }
        unsafe {
            texture_subimage_2d(
                self.id,
                level,
                (0, 0),
                (self.size.0, self.size.1),
                self.format.into(),
                self.data_type.into(),
                data,
            ).chain_err(|| "Could not set texture data")?;
        }
        Ok(())
    }

    pub fn set_subdata_mipmap<T: TextureDataPrimitive>(
        &mut self,
        level: usize,
        size: Texture2DSize,
        offset: Texture2DSize,
        data: &[T],
    ) -> Result<()> {
        if discrete_floored_log2(max(self.size.0, self.size.1)) < level {
            bail!("Mipmap level references greater level than texture has");
        }
        if texture_size_mipmap(size.0, level) * texture_size_mipmap(size.1, level) *
            self.format.n_components() != data.len()
        {
            bail!("Data length does not match proclaimed size");
        }
        if size.0 + offset.0 >= texture_size_mipmap(self.size.0, level) ||
            size.1 + offset.1 >= texture_size_mipmap(self.size.1, level)
        {
            bail!("Size + offset is outside texture bounds");
        }
        unsafe {
            texture_subimage_2d(
                self.id,
                level,
                (offset.0, offset.1),
                (size.0, size.1),
                self.format.into(),
                self.data_type.into(),
                data,
            ).chain_err(|| "Could not set texture data")?;
        }
        Ok(())
    }

    fn set_parameters(&mut self, parameters: Texture2DParameters) -> Result<()> {

        let min = TextureParameterOption::MinFilter(parameters.min, parameters.mipmap);
        let mag = TextureParameterOption::MagFilter(parameters.mag);
        let wrap_s = TextureParameterOption::WrapS(parameters.wrap_s);
        let wrap_t = TextureParameterOption::WrapT(parameters.wrap_t);

        unsafe {
            texture_parameter_i(self.id, min).chain_err(
                || "Could not set min parameter",
            )?;
            texture_parameter_i(self.id, mag).chain_err(
                || "Could not set mag parameter",
            )?;
            texture_parameter_i(self.id, wrap_s).chain_err(
                || "Could not set wrap s parameter",
            )?;
            texture_parameter_i(self.id, wrap_t).chain_err(
                || "Could not set wrap t parameter",
            )?;
        }

        Ok(())
    }
}

impl Drop for Texture2D {
    fn drop(&mut self) {
        unsafe { delete_texture(self.id) };
    }
}
