use crate::{
    ffi::graphics as ffi,
    graphics::{BlendMode, Shader, Texture, Transform},
};
use std::{marker::PhantomData, ptr};

/// Define the states used for drawing to a [`RenderTarget`].
///
/// There are four global states that can be applied to the drawn objects:
///
/// - the blend mode: how pixels of the object are blended with the background
/// - the transform: how the object is positioned/rotated/scaled
/// - the texture: what image is mapped to the object
/// - the shader: what custom effect is applied to the object
///
/// High-level objects such as sprites or text force some of these states when they are drawn.
/// For example, a sprite will set its own texture, so that you don't have to care about it
/// when drawing the sprite.
///
/// The transform is a special case: sprites, texts and shapes
/// (and it's a good idea to do it with your own drawable classes too) combine their transform
/// with the one that is passed in the `RenderStates` structure.
/// So that you can use a "global" transform on top of each object's transform.
///
/// Most objects, especially high-level drawables, can be drawn directly without defining
/// render states explicitly – the default set of states is ok in most cases.
///
/// ```no_run
/// # use sfml::graphics::RenderTarget;
/// # let window: sfml::graphics::RenderWindow = unimplemented!();
/// # let sprite: sfml::graphics::Sprite = unimplemented!();
/// window.draw(&sprite);
/// ```
///
/// To draw with a specific render state, use [`RenderTarget::draw_with_renderstates`].
///
/// ```no_run
/// # use sfml::graphics::*;
/// # let mut window: RenderWindow = unimplemented!();
/// # let shader: Shader = unimplemented!();
/// # let sprite: Sprite = unimplemented!();
/// let mut states = RenderStates::default();
/// states.set_shader(Some(&shader));
/// window.draw_with_renderstates(&sprite, &states);
/// ```
///
/// When you're inside the `draw` function of a drawable object (implementing [`Drawable`]),
/// you can either pass the render states unmodified, or change some of them.
/// For example, a transformable object will combine the current transform with its own transform.
/// A sprite will set its texture. Etc.
///
/// [`RenderTarget`]: crate::graphics::RenderTarget
/// [`RenderTarget::draw_with_renderstates`]: crate::graphics::RenderTarget::draw_with_renderstates
/// [`Drawable`]: crate::graphics::Drawable
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct RenderStates<'texture, 'shader, 'shader_texture: 'shader> {
    /// The blending mode
    pub blend_mode: BlendMode,
    /// The transform
    pub transform: Transform,
    texture: *const ffi::sfTexture,
    shader: *const ffi::sfShader,
    _texture: PhantomData<&'texture Texture>,
    _shader: PhantomData<&'shader Shader<'shader_texture>>,
}

impl<'texture, 'shader, 'shader_texture> RenderStates<'texture, 'shader, 'shader_texture> {
    /// Create a new `RenderStates`.
    ///
    /// # Arguments
    /// * `blend_mode` - The `BlendMode`
    /// * transform - The transform
    /// * texture - Some(texture) if there is a texture, None otherwise
    /// * shader - Some(shader) if there is a shader, None otherwise
    ///
    /// Return a new default `RenderStates`
    #[must_use]
    pub fn new(
        blend_mode: BlendMode,
        transform: Transform,
        texture: Option<&'texture Texture>,
        shader: Option<&'shader Shader<'shader_texture>>,
    ) -> Self {
        Self {
            blend_mode,
            transform,
            texture: match texture {
                Some(tex) => tex.raw(),
                None => ptr::null(),
            },
            shader: match shader {
                Some(shader) => shader.raw(),
                None => ptr::null(),
            },
            _texture: PhantomData,
            _shader: PhantomData,
        }
    }
    /// Sets the texture
    pub fn set_texture(&mut self, texture: Option<&'texture Texture>) {
        self.texture = match texture {
            None => ptr::null(),
            Some(tex) => tex.raw(),
        };
    }
    /// Sets the shader
    pub fn set_shader(&mut self, shader: Option<&'shader Shader<'shader_texture>>) {
        self.shader = match shader {
            None => ptr::null(),
            Some(shader) => shader.raw(),
        };
    }
}

impl RenderStates<'static, 'static, 'static> {
    /// The default render state.
    ///
    /// This can be used in a const context, unlike the [`Default`] implementation.
    pub const DEFAULT: Self = Self {
        blend_mode: BlendMode::ALPHA,
        transform: Transform::IDENTITY,
        texture: ptr::null(),
        shader: ptr::null(),
        _texture: PhantomData,
        _shader: PhantomData,
    };
}

impl Default for RenderStates<'static, 'static, 'static> {
    fn default() -> Self {
        Self::DEFAULT
    }
}
