use crate::{
    image::{ImageInstanceReference, ImageResourceMapping},
    math::vek::*,
    mesh::VertexLayout,
    render_target::RenderTarget,
    resources::material_library::MaterialLibrary,
};
use core::Ignite;
use serde::{Deserialize, Serialize};
use std::{
    collections::{hash_map::DefaultHasher, HashMap},
    hash::{Hash, Hasher},
    io::Write,
};

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MaterialMeshSignature(Vec<(String, usize)>);

impl MaterialMeshSignature {
    pub fn new(vertex_layout: &VertexLayout) -> Self {
        Self(
            vertex_layout
                .vertex_attribs()
                .map(|(_, id, chunk)| (id.to_owned(), chunk.location()))
                .collect(),
        )
    }

    /// # Safety
    /// Constructing signature from raw data might cause invalid signature.
    /// Consider using safe constructors.
    pub unsafe fn from_raw(data: Vec<(String, usize)>) -> Self {
        Self(data)
    }

    pub fn sources(&self) -> impl Iterator<Item = (&str, usize)> {
        self.0.iter().map(|(id, loc)| (id.as_str(), *loc))
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MaterialRenderTargetSignature(Vec<String>);

impl MaterialRenderTargetSignature {
    pub fn new(render_target: &RenderTarget) -> Self {
        Self(
            render_target
                .fragment_buffers()
                .map(|id| id.to_owned())
                .collect(),
        )
    }

    /// # Safety
    /// Constructing signature from raw data might cause invalid signature.
    /// Consider using safe constructors.
    pub unsafe fn from_raw(data: Vec<String>) -> Self {
        Self(data)
    }

    pub fn targets(&self) -> impl Iterator<Item = (&str, usize)> {
        self.0
            .iter()
            .enumerate()
            .map(|(index, id)| (id.as_str(), index))
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MaterialSignature {
    mesh: MaterialMeshSignature,
    render_target: MaterialRenderTargetSignature,
    domain: Option<String>,
}

impl MaterialSignature {
    pub fn new(
        mesh: MaterialMeshSignature,
        render_target: MaterialRenderTargetSignature,
        domain: Option<String>,
    ) -> Self {
        Self {
            mesh,
            render_target,
            domain,
        }
    }

    pub fn from_objects(
        vertex_layout: &VertexLayout,
        render_target: &RenderTarget,
        domain: Option<String>,
    ) -> Self {
        let mesh = MaterialMeshSignature::new(vertex_layout);
        let render_target = MaterialRenderTargetSignature::new(render_target);
        Self::new(mesh, render_target, domain)
    }

    /// # Safety
    /// Constructing signature from raw data might cause invalid signature.
    /// Consider using safe constructors.
    pub unsafe fn from_raw(
        vertex_layout: Vec<(String, usize)>,
        render_target: Vec<String>,
        domain: Option<String>,
    ) -> Self {
        let mesh = MaterialMeshSignature::from_raw(vertex_layout);
        let render_target = MaterialRenderTargetSignature::from_raw(render_target);
        Self::new(mesh, render_target, domain)
    }

    pub fn hashed(&self) -> MaterialHashedSignature {
        let mut hasher = DefaultHasher::new();
        self.mesh.hash(&mut hasher);
        self.render_target.hash(&mut hasher);
        MaterialHashedSignature(hasher.finish())
    }

    pub fn sources(&self) -> impl Iterator<Item = (&str, usize)> {
        self.mesh.sources()
    }

    pub fn targets(&self) -> impl Iterator<Item = (&str, usize)> {
        self.render_target.targets()
    }

    pub fn domain(&self) -> Option<&str> {
        self.domain.as_deref()
    }
}

#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct MaterialHashedSignature(u64);

pub trait MaterialCompile<S, R, T>
where
    S: Write,
{
    fn compile_to(&self, output: &mut S, state: T) -> std::io::Result<()>;

    fn compile(&self, state: T) -> std::io::Result<R>
    where
        S: Default + Into<std::io::Result<R>>,
    {
        let mut buffer = S::default();
        self.compile_to(&mut buffer, state)?;
        buffer.into()
    }
}

pub enum MaterialCompilationState<'a> {
    FunctionDeclaration,
    FunctionDefinition,
    FunctionBody,
    Main {
        shader_type: MaterialShaderType,
        signature: &'a MaterialSignature,
        library: &'a MaterialLibrary,
    },
    GraphBody {
        shader_type: MaterialShaderType,
        library: &'a MaterialLibrary,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BakedMaterialShaders {
    pub vertex: String,
    pub fragment: String,
    pub uniforms: HashMap<String, MaterialValueType>,
    pub samplers: Vec<String>,
}

#[derive(Ignite, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MaterialDataType {
    BuiltIn,
    Constant,
    Attribute,
    Uniform,
    BufferOutput,
    Domain,
}

impl Default for MaterialDataType {
    fn default() -> Self {
        Self::Constant
    }
}

#[derive(Ignite, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MaterialValueType {
    Bool,
    Vec2B,
    Vec3B,
    Vec4B,
    Mat2B,
    Mat3B,
    Mat4B,
    Scalar,
    Vec2F,
    Vec3F,
    Vec4F,
    Mat2F,
    Mat3F,
    Mat4F,
    Integer,
    Vec2I,
    Vec3I,
    Vec4I,
    Mat2I,
    Mat3I,
    Mat4I,
    Sampler2D,
    Array(Box<MaterialValueType>, Option<usize>),
}

impl Default for MaterialValueType {
    fn default() -> Self {
        Self::Scalar
    }
}

impl ToString for MaterialValueType {
    fn to_string(&self) -> String {
        match self {
            Self::Bool => "bool".to_owned(),
            Self::Vec2B => "bvec2".to_owned(),
            Self::Vec3B => "bvec3".to_owned(),
            Self::Vec4B => "bvec4".to_owned(),
            Self::Mat2B => "bmat2".to_owned(),
            Self::Mat3B => "bmat3".to_owned(),
            Self::Mat4B => "bmat4".to_owned(),
            Self::Scalar => "float".to_owned(),
            Self::Vec2F => "vec2".to_owned(),
            Self::Vec3F => "vec3".to_owned(),
            Self::Vec4F => "vec4".to_owned(),
            Self::Mat2F => "mat2".to_owned(),
            Self::Mat3F => "mat3".to_owned(),
            Self::Mat4F => "mat4".to_owned(),
            Self::Integer => "int".to_owned(),
            Self::Vec2I => "ivec2".to_owned(),
            Self::Vec3I => "ivec3".to_owned(),
            Self::Vec4I => "ivec4".to_owned(),
            Self::Mat2I => "imat2".to_owned(),
            Self::Mat3I => "imat3".to_owned(),
            Self::Mat4I => "imat4".to_owned(),
            Self::Sampler2D => "sampler2D".to_owned(),
            Self::Array(t, c) => match c {
                Some(c) => format!("{}[{}]", t.to_string(), c.to_string()),
                None => format!("{}[]", t.to_string()),
            },
        }
    }
}

#[derive(Ignite, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum MaterialValue {
    Bool(bool),
    Vec2B(Vec2<bool>),
    Vec3B(Vec3<bool>),
    Vec4B(Vec4<bool>),
    Mat2B(Mat2<bool>),
    Mat3B(Mat3<bool>),
    Mat4B(Mat4<bool>),
    Scalar(f32),
    Vec2F(Vec2<f32>),
    Vec3F(Vec3<f32>),
    Vec4F(Vec4<f32>),
    Mat2F(Mat2<f32>),
    Mat3F(Mat3<f32>),
    Mat4F(Mat4<f32>),
    Integer(i32),
    Vec2I(Vec2<i32>),
    Vec3I(Vec3<i32>),
    Vec4I(Vec4<i32>),
    Mat2I(Mat2<i32>),
    Mat3I(Mat3<i32>),
    Mat4I(Mat4<i32>),
    Sampler2D(ImageInstanceReference),
    Array(Vec<MaterialValue>),
}

impl ToString for MaterialValue {
    #[allow(clippy::many_single_char_names)]
    fn to_string(&self) -> String {
        match self {
            Self::Bool(v) => v.to_string(),
            Self::Vec2B(v) => format!("bvec2({}, {})", v.x, v.y),
            Self::Vec3B(v) => format!("bvec3({}, {}, {})", v.x, v.y, v.z),
            Self::Vec4B(v) => format!("bvec4({}, {}, {}, {})", v.x, v.y, v.z, v.w),
            Self::Mat2B(v) => {
                let [a, b, c, d] = v.into_col_array();
                format!("bmat2({}, {}, {}, {})", a, b, c, d)
            }
            Self::Mat3B(v) => {
                let [a, b, c, d, e, f, g, h, i] = v.into_col_array();
                format!(
                    "bmat3({}, {}, {}, {}, {}, {}, {}, {}, {})",
                    a, b, c, d, e, f, g, h, i
                )
            }
            Self::Mat4B(v) => {
                let [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p] = v.into_col_array();
                format!(
                    "bmat4({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})",
                    a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p
                )
            }
            Self::Scalar(v) => format!("{:.6}", v),
            Self::Vec2F(v) => format!("vec2({:.6}, {:.6})", v.x, v.y),
            Self::Vec3F(v) => format!("vec3({:.6}, {:.6}, {:.6})", v.x, v.y, v.z),
            Self::Vec4F(v) => format!("vec4({:.6}, {:.6}, {:.6}, {:.6})", v.x, v.y, v.z, v.w),
            Self::Mat2F(v) => {
                let [a, b, c, d] = v.into_col_array();
                format!("mat2({:.6}, {:.6}, {:.6}, {:.6})", a, b, c, d)
            }
            Self::Mat3F(v) => {
                let [a, b, c, d, e, f, g, h, i] = v.into_col_array();
                format!(
                    "mat3({:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6})",
                    a, b, c, d, e, f, g, h, i
                )
            }
            Self::Mat4F(v) => {
                let [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p] = v.into_col_array();
                format!(
                    "mat4({:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6}, {:.6})",
                    a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p
                )
            }
            Self::Integer(v) => format!("{}", v),
            Self::Vec2I(v) => format!("ivec2({}, {})", v.x, v.y),
            Self::Vec3I(v) => format!("ivec3({}, {}, {})", v.x, v.y, v.z),
            Self::Vec4I(v) => format!("ivec4({}, {}, {}, {})", v.x, v.y, v.z, v.w),
            Self::Mat2I(v) => {
                let [a, b, c, d] = v.into_col_array();
                format!("imat2({}, {}, {}, {})", a, b, c, d)
            }
            Self::Mat3I(v) => {
                let [a, b, c, d, e, f, g, h, i] = v.into_col_array();
                format!(
                    "imat3({}, {}, {}, {}, {}, {}, {}, {}, {})",
                    a, b, c, d, e, f, g, h, i
                )
            }
            Self::Mat4I(v) => {
                let [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p] = v.into_col_array();
                format!(
                    "imat4({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})",
                    a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p
                )
            }
            Self::Sampler2D(v) => v.to_string(),
            Self::Array(v) => {
                let t = if let Some(item) = v.first() {
                    item.value_type()
                } else {
                    Default::default()
                };
                format!(
                    "{}[]({})",
                    t.to_string(),
                    v.iter()
                        .map(|v| v.to_string())
                        .collect::<Vec<_>>()
                        .join(", ")
                )
            }
        }
    }
}

impl MaterialValue {
    pub fn value_type(&self) -> MaterialValueType {
        match self {
            Self::Bool(_) => MaterialValueType::Bool,
            Self::Vec2B(_) => MaterialValueType::Vec2B,
            Self::Vec3B(_) => MaterialValueType::Vec3B,
            Self::Vec4B(_) => MaterialValueType::Vec4B,
            Self::Mat2B(_) => MaterialValueType::Mat2B,
            Self::Mat3B(_) => MaterialValueType::Mat3B,
            Self::Mat4B(_) => MaterialValueType::Mat4B,
            Self::Scalar(_) => MaterialValueType::Scalar,
            Self::Vec2F(_) => MaterialValueType::Vec2F,
            Self::Vec3F(_) => MaterialValueType::Vec3F,
            Self::Vec4F(_) => MaterialValueType::Vec4F,
            Self::Mat2F(_) => MaterialValueType::Mat2F,
            Self::Mat3F(_) => MaterialValueType::Mat3F,
            Self::Mat4F(_) => MaterialValueType::Mat4F,
            Self::Integer(_) => MaterialValueType::Integer,
            Self::Vec2I(_) => MaterialValueType::Vec2I,
            Self::Vec3I(_) => MaterialValueType::Vec3I,
            Self::Vec4I(_) => MaterialValueType::Vec4I,
            Self::Mat2I(_) => MaterialValueType::Mat2I,
            Self::Mat3I(_) => MaterialValueType::Mat3I,
            Self::Mat4I(_) => MaterialValueType::Mat4I,
            Self::Sampler2D(_) => MaterialValueType::Sampler2D,
            Self::Array(data) => MaterialValueType::Array(
                Box::new(if let Some(item) = data.first() {
                    item.value_type()
                } else {
                    Default::default()
                }),
                Some(data.len()),
            ),
        }
    }

    pub fn update_references(&mut self, image_mapping: &ImageResourceMapping) {
        match self {
            Self::Sampler2D(reference) => match reference {
                ImageInstanceReference::Asset(path) => {
                    if let Some(id) = image_mapping.resource_by_name(path) {
                        *reference = ImageInstanceReference::Id(id);
                    }
                }
                ImageInstanceReference::VirtualAsset(path) => {
                    if let Some((owner, id)) = image_mapping.virtual_resource_by_name(path) {
                        *reference = ImageInstanceReference::VirtualId { owner, id };
                    }
                }
                _ => {}
            },
            Self::Array(data) => {
                for value in data {
                    value.update_references(image_mapping);
                }
            }
            _ => {}
        }
    }
}

macro_rules! impl_value_from {
    ( $( $name:ident ( $type:ty ) , )+ ) => {
        $(
            impl From<$type> for MaterialValue {
                fn from(v: $type) -> Self {
                    Self::$name(v.into())
                }
            }
        )+
    };
}

impl_value_from! {
    Bool(bool),
    Vec2B(Vec2<bool>),
    Vec3B(Vec3<bool>),
    Vec4B(Vec4<bool>),
    Mat2B(Mat2<bool>),
    Mat3B(Mat3<bool>),
    Mat4B(Mat4<bool>),
    Scalar(f32),
    Vec2F(Vec2<f32>),
    Vec3F(Vec3<f32>),
    Vec4F(Vec4<f32>),
    Mat2F(Mat2<f32>),
    Mat3F(Mat3<f32>),
    Mat4F(Mat4<f32>),
    Integer(i32),
    Vec2I(Vec2<i32>),
    Vec3I(Vec3<i32>),
    Vec4I(Vec4<i32>),
    Mat2I(Mat2<i32>),
    Mat3I(Mat3<i32>),
    Mat4I(Mat4<i32>),
    Sampler2D(ImageInstanceReference),
    Array(Vec<MaterialValue>),
}

#[derive(Ignite, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MaterialShaderType {
    Undefined,
    Vertex,
    Fragment,
}

impl Default for MaterialShaderType {
    fn default() -> Self {
        Self::Undefined
    }
}
