use std::collections::BTreeMap;

use sge_vectors::{Mat3, Mat4, Vec2, Vec3, Vec4};
use glium::uniforms::{SamplerBehavior, UniformValue};
use sge_color::Color;
use sge_macros::gen_ref_type;
use sge_programs::{GOURAUD_3D_PROGRAM, ProgramRef};
use sge_textures::TextureRef;

pub const DEFAULT_MATERIAL: MaterialRef = MaterialRef(0);

pub struct Material {
    pub(crate) program: ProgramRef,
    pub(crate) uniforms: BTreeMap<String, UniformData>,
    pub draw_param_overrides: Option<glium::DrawParameters<'static>>,
}

#[derive(Clone, Copy, PartialEq)]
pub enum UniformData {
    Float(f32),
    Vec2(Vec2),
    Vec3(Vec3),
    Vec4(Vec4),
    Color(Color),
    Texture(TextureRef),
    Mat4(Mat4),
    Mat3(Mat3),
}

impl Material {
    pub fn new(program: ProgramRef) -> Self {
        Self {
            program,
            uniforms: BTreeMap::new(),
            draw_param_overrides: None,
        }
    }

    pub fn with_draw_param_overrides(mut self, params: glium::DrawParameters<'static>) -> Self {
        self.draw_param_overrides = Some(params);
        self
    }

    pub fn with_float(mut self, name: impl Into<String>, value: f32) -> Self {
        self.uniforms.insert(name.into(), UniformData::Float(value));
        self
    }

    pub fn with_vec2(mut self, name: impl Into<String>, value: Vec2) -> Self {
        self.uniforms.insert(name.into(), UniformData::Vec2(value));
        self
    }

    pub fn with_vec3(mut self, name: impl Into<String>, value: Vec3) -> Self {
        self.uniforms.insert(name.into(), UniformData::Vec3(value));
        self
    }

    pub fn with_vec4(mut self, name: impl Into<String>, value: Vec4) -> Self {
        self.uniforms.insert(name.into(), UniformData::Vec4(value));
        self
    }

    pub fn with_mat4(mut self, name: impl Into<String>, value: Mat4) -> Self {
        self.uniforms.insert(name.into(), UniformData::Mat4(value));
        self
    }

    pub fn with_mat3(mut self, name: impl Into<String>, value: Mat3) -> Self {
        self.uniforms.insert(name.into(), UniformData::Mat3(value));
        self
    }

    pub fn with_texture(mut self, name: impl Into<String>, texture: TextureRef) -> Self {
        self.uniforms
            .insert(name.into(), UniformData::Texture(texture));
        self
    }

    pub fn with_color(mut self, name: impl Into<String>, color: Color) -> Self {
        self.uniforms.insert(name.into(), UniformData::Color(color));
        self
    }

    // ----------------------------------------------------------------------------------

    pub fn set_draw_param_overrides(&mut self, params: glium::DrawParameters<'static>) {
        self.draw_param_overrides = Some(params);
    }

    pub fn set_float(&mut self, name: impl Into<String>, value: f32) {
        self.uniforms.insert(name.into(), UniformData::Float(value));
    }

    pub fn set_vec2(&mut self, name: impl Into<String>, value: Vec2) {
        self.uniforms.insert(name.into(), UniformData::Vec2(value));
    }

    pub fn set_vec3(&mut self, name: impl Into<String>, value: Vec3) {
        self.uniforms.insert(name.into(), UniformData::Vec3(value));
    }

    pub fn set_vec4(&mut self, name: impl Into<String>, value: Vec4) {
        self.uniforms.insert(name.into(), UniformData::Vec4(value));
    }

    pub fn set_mat4(&mut self, name: impl Into<String>, value: Mat4) {
        self.uniforms.insert(name.into(), UniformData::Mat4(value));
    }

    pub fn set_mat3(&mut self, name: impl Into<String>, value: Mat3) {
        self.uniforms.insert(name.into(), UniformData::Mat3(value));
    }

    pub fn set_texture(&mut self, name: impl Into<String>, texture: TextureRef) {
        self.uniforms
            .insert(name.into(), UniformData::Texture(texture));
    }

    pub fn set_color(&mut self, name: impl Into<String>, color: Color) {
        self.uniforms.insert(name.into(), UniformData::Color(color));
    }

    pub fn get_uniform(&self, name: impl Into<String>) -> Option<UniformData> {
        self.uniforms.get(&name.into()).copied()
    }
}

impl UniformData {
    fn to_gpu<'a>(self) -> UniformValue<'a> {
        match self {
            Self::Float(f) => UniformValue::Float(f),
            Self::Mat3(m) => UniformValue::Mat3(m.to_cols_array_2d()),
            Self::Mat4(m) => UniformValue::Mat4(m.to_cols_array_2d()),
            Self::Texture(texture) => {
                let texture = texture.get();
                let behaviour = SamplerBehavior {
                    magnify_filter: texture.magnify_filter,
                    minify_filter: texture.minify_filter,
                    ..Default::default()
                };

                UniformValue::Texture2d(&texture.gl_texture, Some(behaviour))
            }
            Self::Vec2(v) => UniformValue::Vec2(v.into()),
            Self::Vec3(v) => UniformValue::Vec3(v.into()),
            Self::Vec4(v) => UniformValue::Vec4(v.into()),
            Self::Color(c) => UniformValue::Vec4(c.for_gpu()),
        }
    }
}

impl glium::uniforms::Uniforms for Material {
    fn visit_values<'a, F: FnMut(&str, UniformValue<'a>)>(&'a self, mut add: F) {
        for key in self.uniforms.keys() {
            let value = self.uniforms.get(key).unwrap().to_gpu();
            add(key, value);
        }
    }
}

gen_ref_type!(Material, MaterialRef, materials);

pub(crate) fn init_materials() {
    init_materials_storage();
    let regular_color = Color::hex(0xBBBDBD);
    let dark_color = Color::hex(0x333333);
    let light_pos = Vec3::new(0.0, 3.0, 1.0);

    Material::new(GOURAUD_3D_PROGRAM)
        .with_color("regular_color", regular_color)
        .with_color("dark_color", dark_color)
        .with_vec3("light_pos", light_pos)
        .create();
}
