//! MTL file format parsing.
//!
//! File format documentation: <https://paulbourke.net/dataformats/mtl>

use std::path::Path;

use crate::color::Color;
use crate::file_format::wavefront::common::{
    keyword,
    parse_statement_line,
    ParsingError,
    Statement,
};
use crate::material::{Builder as MaterialBuilder, Material};
use crate::texture::{Error as TextureError, Texture};

/// Parses the content of a Wavefront `.mtl`.
///
/// # Errors
/// Will return `Err` if the `.mtl` content is formatted incorrectly.
pub fn parse(obj_content: &str) -> Result<Vec<NamedMaterial>, Error>
{
    let lines = obj_content
        .lines()
        .enumerate()
        .map(|(line_index, line)| (line_index + 1, line));

    let statements = lines
        .map(|(line_no, line)| (line_no, parse_statement_line::<Keyword>(line, line_no)))
        .filter_map(|(line_no, result)| {
            let opt_statement = match result {
                Ok(opt_statement) => opt_statement,
                Err(err) => {
                    return Some(Err(err));
                }
            };

            Some(Ok((line_no, opt_statement?)))
        })
        .collect::<Result<Vec<_>, _>>()?;

    let material_cnt = statements
        .iter()
        .filter(|(_, statement)| matches!(statement.keyword, Keyword::Newmtl))
        .count();

    #[cfg(feature = "debug")]
    tracing::debug!("Material count: {material_cnt}");

    statements_to_materials(statements, material_cnt)
}

#[derive(Debug, Clone)]
pub struct NamedMaterial
{
    pub name: String,
    pub material: Material,
}

#[derive(Debug, Clone)]
pub struct UnfinishedNamedMaterial
{
    name: String,
    material_builder: MaterialBuilder,
    ready: bool,
}

#[derive(Debug, thiserror::Error)]
pub enum Error
{
    #[error(transparent)]
    ParsingError(#[from] ParsingError),

    #[error("Failed to open texture")]
    TextureError(#[from] TextureError),

    #[error(
        "Unsupported number of arguments ({arg_count}) to {keyword} at line {line_no}"
    )]
    UnsupportedArgumentCount
    {
        keyword: String,
        arg_count: usize,
        line_no: usize,
    },

    #[error("Invalid number of arguments ({arg_count}) to {keyword} at line {line_no}")]
    InvalidArgumentCount
    {
        keyword: String,
        arg_count: usize,
        line_no: usize,
    },
}

#[cfg_attr(feature = "debug", tracing::instrument(skip_all))]
fn statements_to_materials(
    statements: impl IntoIterator<Item = (usize, Statement<Keyword>)>,
    material_cnt: usize,
) -> Result<Vec<NamedMaterial>, Error>
{
    let mut materials = Vec::<NamedMaterial>::with_capacity(material_cnt);

    let mut curr_material = UnfinishedNamedMaterial {
        name: String::new(),
        material_builder: MaterialBuilder::new(),
        ready: false,
    };

    for (line_no, statement) in statements {
        if statement.keyword == Keyword::Newmtl {
            if curr_material.ready {
                #[cfg(feature = "debug")]
                tracing::debug!("Building material");

                let material = curr_material.material_builder.clone().build();

                materials.push(NamedMaterial { name: curr_material.name, material });
            }

            let name = statement.get_text_arg(0, line_no)?;

            curr_material.name = name.to_string();
            curr_material.ready = true;

            continue;
        }

        if !curr_material.ready {
            // Discard statements not belonging to a material
            continue;
        };

        match statement.keyword {
            Keyword::Ka => {
                let color = get_color_from_statement(&statement, line_no)?;

                #[cfg(feature = "debug")]
                tracing::debug!("Adding ambient color");

                curr_material.material_builder =
                    curr_material.material_builder.ambient(color);
            }
            Keyword::Kd => {
                let color = get_color_from_statement(&statement, line_no)?;

                #[cfg(feature = "debug")]
                tracing::debug!("Adding diffuse color");

                curr_material.material_builder =
                    curr_material.material_builder.diffuse(color);
            }
            Keyword::Ks => {
                let color = get_color_from_statement(&statement, line_no)?;

                #[cfg(feature = "debug")]
                tracing::debug!("Adding specular color");

                curr_material.material_builder =
                    curr_material.material_builder.specular(color);
            }
            Keyword::MapKa => {
                if statement.arguments.len() > 1 {
                    return Err(Error::UnsupportedArgumentCount {
                        keyword: statement.keyword.to_string(),
                        arg_count: statement.arguments.len(),
                        line_no,
                    });
                }

                let texture_file_path = statement.get_text_arg(0, line_no)?;

                let texture = Texture::open(Path::new(texture_file_path))?;

                #[cfg(feature = "debug")]
                tracing::debug!("Adding ambient map");

                let texture_id = texture.id();

                curr_material.material_builder = curr_material
                    .material_builder
                    .texture(texture)
                    .ambient_map(texture_id);
            }
            Keyword::MapKd => {
                let texture = get_map_from_texture(&statement, line_no)?;

                #[cfg(feature = "debug")]
                tracing::debug!("Adding diffuse map");

                let texture_id = texture.id();

                curr_material.material_builder = curr_material
                    .material_builder
                    .texture(texture)
                    .diffuse_map(texture_id);
            }
            Keyword::MapKs => {
                let texture = get_map_from_texture(&statement, line_no)?;

                #[cfg(feature = "debug")]
                tracing::debug!("Adding specular map");

                let texture_id = texture.id();

                curr_material.material_builder = curr_material
                    .material_builder
                    .texture(texture)
                    .specular_map(texture_id);
            }
            Keyword::Newmtl => {}
        }
    }

    if curr_material.ready {
        #[cfg(feature = "debug")]
        tracing::debug!("Building last material");

        let material = curr_material.material_builder.build();

        materials.push(NamedMaterial { name: curr_material.name, material });
    }

    Ok(materials)
}

fn get_color_from_statement(
    statement: &Statement<Keyword>,
    line_no: usize,
) -> Result<Color<f32>, Error>
{
    if statement.arguments.len() > 3 {
        return Err(Error::InvalidArgumentCount {
            keyword: statement.keyword.to_string(),
            arg_count: statement.arguments.len(),
            line_no,
        });
    }

    let red = statement.get_float_arg(0, line_no)?;
    let green = statement.get_float_arg(1, line_no)?;
    let blue = statement.get_float_arg(2, line_no)?;

    Ok(Color { red, green, blue })
}

fn get_map_from_texture(
    statement: &Statement<Keyword>,
    line_no: usize,
) -> Result<Texture, Error>
{
    if statement.arguments.len() > 1 {
        return Err(Error::UnsupportedArgumentCount {
            keyword: statement.keyword.to_string(),
            arg_count: statement.arguments.len(),
            line_no,
        });
    }

    let texture_file_path = statement.get_text_arg(0, line_no)?;

    Ok(Texture::open(Path::new(texture_file_path))?)
}

keyword! {
    #[derive(Debug, PartialEq, Eq, Clone, Copy)]
    enum Keyword {
        #[keyword(rename = "newmtl")]
        Newmtl,

        Ka,
        Kd,
        Ks,

        #[keyword(rename = "map_Ka")]
        MapKa,

        #[keyword(rename = "map_Kd")]
        MapKd,

        #[keyword(rename = "map_Ks")]
        MapKs,
    }
}