use std::fmt::Display;
use std::path::Path;
use std::sync::atomic::{AtomicU32, Ordering};

use image::io::Reader as ImageReader;
use image::{DynamicImage, ImageError, Rgb, RgbImage};

use crate::color::Color;
use crate::data_types::dimens::Dimens;
use crate::opengl::texture::PixelDataFormat;
use crate::util::builder;

static NEXT_ID: AtomicU32 = AtomicU32::new(0);

mod reexports
{
    pub use crate::opengl::texture::{Filtering, Wrapping};
}

pub use reexports::*;

#[derive(Debug, Clone)]
pub struct Texture
{
    id: Id,
    image: DynamicImage,
    pixel_data_format: PixelDataFormat,
    dimensions: Dimens<u32>,
    properties: Properties,
}

impl Texture
{
    pub fn builder() -> Builder
    {
        Builder::default()
    }

    /// Opens a texture image.
    ///
    /// # Errors
    /// Will return `Err` if:
    /// - Opening the image fails
    /// - The image data is not 8-bit/color RGB
    pub fn open(path: &Path) -> Result<Self, Error>
    {
        Self::builder().open(path)
    }

    #[must_use]
    pub fn new_from_color(dimensions: &Dimens<u32>, color: &Color<u8>) -> Self
    {
        Self::builder().build_with_single_color(dimensions, color)
    }

    #[must_use]
    pub fn id(&self) -> Id
    {
        self.id
    }

    #[must_use]
    pub fn properties(&self) -> &Properties
    {
        &self.properties
    }

    pub fn properties_mut(&mut self) -> &mut Properties
    {
        &mut self.properties
    }

    #[must_use]
    pub fn dimensions(&self) -> &Dimens<u32>
    {
        &self.dimensions
    }

    #[must_use]
    pub fn pixel_data_format(&self) -> PixelDataFormat
    {
        self.pixel_data_format
    }

    #[must_use]
    pub fn image(&self) -> &DynamicImage
    {
        &self.image
    }
}

impl Drop for Texture
{
    fn drop(&mut self)
    {
        NEXT_ID.fetch_sub(1, Ordering::Relaxed);
    }
}

/// Texture builder.
#[derive(Debug, Default, Clone)]
pub struct Builder
{
    properties: Properties,
}

impl Builder
{
    pub fn properties(mut self, properties: Properties) -> Self
    {
        self.properties = properties;
        self
    }

    /// Opens a image as a texture.
    ///
    /// # Errors
    /// Will return `Err` if:
    /// - Opening the image fails
    /// - Decoding the image fails
    /// - The image data is in a unsupported format
    pub fn open(&self, path: &(impl AsRef<Path> + ?Sized)) -> Result<Texture, Error>
    {
        let image = ImageReader::open(path)
            .map_err(Error::OpenImageFailed)?
            .decode()
            .map_err(Error::DecodeImageFailed)?;

        let pixel_data_format = match &image {
            DynamicImage::ImageRgb8(_) => PixelDataFormat::Rgb8,
            DynamicImage::ImageRgba8(_) => PixelDataFormat::Rgba8,
            _ => {
                return Err(Error::UnsupportedImageDataFormat);
            }
        };

        let dimensions = Dimens {
            width: image.width(),
            height: image.height(),
        };

        let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);

        Ok(Texture {
            id: Id::new(id),
            image,
            pixel_data_format,
            dimensions,
            properties: self.properties.clone(),
        })
    }

    #[must_use]
    pub fn build_with_single_color(
        &self,
        dimensions: &Dimens<u32>,
        color: &Color<u8>,
    ) -> Texture
    {
        let image = RgbImage::from_pixel(
            dimensions.width,
            dimensions.height,
            Rgb([color.red, color.green, color.blue]),
        );

        let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);

        Texture {
            id: Id::new(id),
            image: image.into(),
            pixel_data_format: PixelDataFormat::Rgb8,
            dimensions: *dimensions,
            properties: self.properties.clone(),
        }
    }
}

/// Texture error.
#[derive(Debug, thiserror::Error)]
pub enum Error
{
    #[error("Failed to open texture image")]
    OpenImageFailed(#[source] std::io::Error),

    #[error("Failed to decode texture image")]
    DecodeImageFailed(#[source] ImageError),

    #[error("Unsupported image data format")]
    UnsupportedImageDataFormat,
}

builder! {
/// Texture properties
#[builder(name = PropertiesBuilder, derives=(Debug, Clone))]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Properties
{
    pub wrap: Wrapping,
    pub magnifying_filter: Filtering,
    pub minifying_filter: Filtering,
}
}

impl Properties
{
    pub fn builder() -> PropertiesBuilder
    {
        PropertiesBuilder::default()
    }
}

impl Default for Properties
{
    fn default() -> Self
    {
        Self {
            wrap: Wrapping::Repeat,
            magnifying_filter: Filtering::Linear,
            minifying_filter: Filtering::Nearest,
        }
    }
}

impl Default for PropertiesBuilder
{
    fn default() -> Self
    {
        Properties::default().into()
    }
}

/// Texture ID.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Id
{
    id: u32,
}

impl Id
{
    fn new(id: u32) -> Self
    {
        Self { id }
    }
}

impl Display for Id
{
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
    {
        self.id.fmt(formatter)
    }
}