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

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

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

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

    bindless_counter: Cell<usize>,
}

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

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

impl Texture for TextureCubemap {
    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::TextureCubeMapArray
    }
}

// TODO size should not be necessary
impl TextureCubemap {
    pub fn new<T: TextureDataPrimitive>(
        size: TextureCubemapSize,
        internal_format: TextureInternalFormat,
        format: TextureFormat,
        data_type: TextureDataType,
        parameters: TextureCubemapParameters,
        levels: usize,
        data: &[&[T]],
    ) -> Result<TextureCubemap> {
        let mut texture =
            TextureCubemap::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: TextureCubemapSize,
        internal_format: TextureInternalFormat,
        format: TextureFormat,
        data_type: TextureDataType,
        parameters: TextureCubemapParameters,
        levels: usize,
    ) -> Result<TextureCubemap> {
        unsafe {
            let id = create_texture(TextureTarget::TextureCubeMapArray);

            let mut texture = TextureCubemap {
                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_3d(id, levels, internal_format, (size.0, size.1, 6))
                .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 data.len() != 6 {
            bail!("Cubemap needs 6 textures");
        }
        let size_face = data[0].len();
        for i in 1..6 {
            if size_face != data[i].len() {
                bail!("All 6 textures must have the same size");
            }
        }
        if self.size.0 * self.size.1 * self.format.n_components() != size_face {
            bail!("Data length does not match texture size");
        }
        unsafe {
            for i in 0..6 {
                texture_subimage_3d(
                    self.id,
                    0,
                    (0, 0, i),
                    (self.size.0, self.size.1, 1),
                    self.format.into(),
                    self.data_type.into(),
                    &data[i],
                ).chain_err(|| "Could not update texture")?;
            }
        }
        Ok(())
    }

    pub fn set_subdata<T: TextureDataPrimitive>(
        &mut self,
        face: usize,
        size: Texture2DSize,
        offset: Texture2DSize,
        data: &[T],
    ) -> Result<()> {
        if face >= 6 {
            bail!("Face must be between 0 and 5(inclusive)")
        }
        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_3d(
                self.id,
                0,
                (offset.0, offset.1, face),
                (size.0, size.1, 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 data.len() != 6 {
            bail!("Cubemap needs 6 textures");
        }
        if discrete_floored_log2(max(self.size.0, self.size.1)) < level {
            bail!("Mipmap level references greater level than texture has");
        }
        let size_face = data[0].len();
        for i in 1..6 {
            if size_face != data[i].len() {
                bail!("All 6 textures must have the same size");
            }
        }
        if texture_size_mipmap(self.size.0, level) * texture_size_mipmap(self.size.1, level) *
            self.format.n_components() !=
            size_face
        {
            bail!("Data length does not match texture size");
        }
        unsafe {
            for i in 0..6 {
                texture_subimage_3d(
                    self.id,
                    level,
                    (0, 0, i),
                    (self.size.0, self.size.1, 1),
                    self.format.into(),
                    self.data_type.into(),
                    &data[i],
                ).chain_err(|| "Could not update texture")?;
            }
        }
        Ok(())
    }

    pub fn set_subdata_mipmap<T: TextureDataPrimitive>(
        &mut self,
        level: usize,
        face: usize,
        size: Texture2DSize,
        offset: Texture2DSize,
        data: &[T],
    ) -> Result<()> {
        if face >= 6 {
            bail!("Face must be between 0 and 5(inclusive)")
        }
        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");
        }
        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_3d(
                self.id,
                level,
                (offset.0, offset.1, face),
                (size.0, size.1, 1),
                self.format.into(),
                self.data_type.into(),
                data,
            ).chain_err(|| "Could not set texture data")?;
        }
        Ok(())
    }

    fn set_parameters(&mut self, parameters: TextureCubemapParameters) -> Result<()> {
        let min = TextureParameterOption::MinFilter(parameters.min, parameters.mipmap);
        let mag = TextureParameterOption::MagFilter(parameters.mag);
        let seamless = TextureParameterOption::CubemapSeamless(parameters.seamless);
        let wrap_s = TextureParameterOption::WrapS(parameters.wrap_s);
        let wrap_t = TextureParameterOption::WrapT(parameters.wrap_t);
        let wrap_r = TextureParameterOption::WrapR(parameters.wrap_r);


        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, seamless).chain_err(
                || "Could not set seamless 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",
            )?;
            texture_parameter_i(self.id, wrap_r).chain_err(
                || "Could not set wrap r parameter",
            )?;
        }

        Ok(())
    }
}

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