use crate::{
    material::{
        common::{MaterialCompilationState, MaterialCompile, MaterialDataType, MaterialValueType},
        graph::MaterialGraph,
        MaterialError,
    },
    math::vek::*,
    resources::material_library::MaterialLibrary,
    StringBuffer,
};
use core::Ignite;
use serde::{Deserialize, Serialize};

#[derive(Ignite, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum MaterialFunctionContent {
    Graph(MaterialGraph),
    Code(String),
    BuiltIn(String),
}

impl Default for MaterialFunctionContent {
    fn default() -> Self {
        Self::Graph(Default::default())
    }
}

#[derive(Ignite, Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct MaterialFunctionInput {
    #[serde(default)]
    pub name: String,
    #[serde(default)]
    pub value_type: MaterialValueType,
}

#[derive(Ignite, Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct MaterialFunction {
    #[serde(default)]
    pub name: String,
    #[serde(default)]
    pub inputs: Vec<MaterialFunctionInput>,
    #[serde(default)]
    pub output: MaterialValueType,
    #[serde(default)]
    pub content: MaterialFunctionContent,
}

impl MaterialFunction {
    pub fn call_name(&self) -> &str {
        match &self.content {
            MaterialFunctionContent::BuiltIn(name) => name,
            _ => &self.name,
        }
    }

    pub fn validate(&self, library: &MaterialLibrary) -> Result<(), MaterialError> {
        if let MaterialFunctionContent::Graph(graph) = &self.content {
            graph.validate(library)?;
            if !graph.outputs().any(|(_, node)| {
                node.name == "result"
                    && node.value_type == self.output
                    && node.data_type == MaterialDataType::BuiltIn
            }) {
                return Err(MaterialError::FunctionOutputHasNoNode);
            }
            let inputs = graph
                .inputs()
                .map(|(_, node)| (&node.name, &node.value_type, node.data_type))
                .collect::<Vec<_>>();
            for input in &self.inputs {
                if !inputs.iter().any(|(n, t, d)| {
                    *n == &input.name && *t == &input.value_type && *d == MaterialDataType::BuiltIn
                }) {
                    return Err(MaterialError::FunctionInputHasNoNode(input.name.to_owned()));
                }
            }
        }
        Ok(())
    }

    pub fn can_be_compiled(&self) -> bool {
        !matches!(self.content, MaterialFunctionContent::BuiltIn(_))
    }

    fn compile_declaration(&self, output: &mut StringBuffer) -> std::io::Result<()> {
        if matches!(self.content, MaterialFunctionContent::BuiltIn(_)) {
            return Ok(());
        }
        output.write_str(self.output.to_string())?;
        output.write_space()?;
        output.write_str(&self.name)?;
        output.write_str("(")?;
        for (index, input) in self.inputs.iter().enumerate() {
            if index > 0 {
                output.write_str(", ")?;
            }
            output.write_str(input.value_type.to_string())?;
            output.write_space()?;
            output.write_str(&input.name)?;
        }
        output.write_str(");\n")?;
        Ok(())
    }

    fn compile_definition(&self, output: &mut StringBuffer) -> std::io::Result<()> {
        let content = match &self.content {
            MaterialFunctionContent::Graph(graph) => {
                graph.compile(MaterialCompilationState::FunctionBody)?
            }
            MaterialFunctionContent::Code(content) => content.to_owned(),
            MaterialFunctionContent::BuiltIn(_) => return Ok(()),
        };
        output.write_str(self.output.to_string())?;
        output.write_space()?;
        output.write_str(&self.name)?;
        output.write_str("(")?;
        for (index, input) in self.inputs.iter().enumerate() {
            if index > 0 {
                output.write_str(", ")?;
            }
            output.write_str(input.value_type.to_string())?;
            output.write_space()?;
            output.write_str(&input.name)?;
        }
        output.write_str(") {\n")?;
        output.write_str(content)?;
        output.write_str("\n}\n")?;
        Ok(())
    }
}

impl MaterialCompile<StringBuffer, String, MaterialCompilationState<'_>> for MaterialFunction {
    fn compile_to(
        &self,
        output: &mut StringBuffer,
        state: MaterialCompilationState,
    ) -> std::io::Result<()> {
        match state {
            MaterialCompilationState::FunctionDeclaration => {
                self.compile_declaration(output)?;
            }
            MaterialCompilationState::FunctionDefinition => {
                self.compile_definition(output)?;
            }
            _ => {}
        }
        Ok(())
    }
}
