//! Standard materials from [`MaterialDesc`] authoring data.

use bevy::{
    image::{
        ImageAddressMode, ImageFilterMode, ImageLoaderSettings, ImageSampler,
        ImageSamplerDescriptor,
    },
    prelude::*,
};

use crate::{
    asset_server_path, MaterialDesc, MaterialShaderKind, RuntimeTextureLoadSpec, TextureFilter,
    TextureWrap,
};

pub fn material_from_desc(asset_server: &AssetServer, desc: &MaterialDesc) -> StandardMaterial {
    material_from_desc_with_texture_loader(asset_server, desc, |path| {
        asset_server.load(asset_server_path(path))
    })
}

pub fn material_from_desc_with_texture_loader(
    _asset_server: &AssetServer,
    desc: &MaterialDesc,
    mut load_texture: impl FnMut(&str) -> Handle<Image>,
) -> StandardMaterial {
    StandardMaterial {
        base_color: desc.base_color.to_color(),
        metallic: desc.metallic,
        perceptual_roughness: desc.roughness,
        unlit: matches!(desc.shader.kind, MaterialShaderKind::Unlit),
        base_color_texture: desc
            .base_color_texture
            .as_ref()
            .map(|path| load_texture(path)),
        emissive: LinearRgba::rgb(
            desc.emissive_color.r * desc.emissive_intensity,
            desc.emissive_color.g * desc.emissive_intensity,
            desc.emissive_color.b * desc.emissive_intensity,
        ),
        emissive_texture: desc
            .emissive_texture
            .as_ref()
            .map(|path| load_texture(path)),
        normal_map_texture: desc
            .normal_map_texture
            .as_ref()
            .map(|path| load_texture(path)),
        metallic_roughness_texture: desc
            .metallic_roughness_texture
            .as_ref()
            .map(|path| load_texture(path)),
        occlusion_texture: desc
            .occlusion_texture
            .as_ref()
            .map(|path| load_texture(path)),
        uv_transform: bevy::math::Affine2::from_scale_angle_translation(
            desc.uv_tiling,
            0.0,
            desc.uv_offset,
        ),
        ..default()
    }
}

pub fn load_runtime_texture(
    asset_server: &AssetServer,
    spec: &RuntimeTextureLoadSpec,
) -> Handle<Image> {
    let sampler = ImageSampler::Descriptor(runtime_image_sampler_descriptor(spec));
    let is_srgb = spec.is_srgb;
    asset_server
        .load_builder()
        .with_settings(move |settings: &mut ImageLoaderSettings| {
            settings.is_srgb = is_srgb;
            settings.sampler = sampler.clone();
        })
        .load(asset_server_path(&spec.path))
}

pub fn runtime_image_sampler_descriptor(spec: &RuntimeTextureLoadSpec) -> ImageSamplerDescriptor {
    let address_mode = match spec.wrap {
        TextureWrap::Repeat => ImageAddressMode::Repeat,
        TextureWrap::Clamp => ImageAddressMode::ClampToEdge,
        TextureWrap::Mirror => ImageAddressMode::MirrorRepeat,
    };
    let filter = match spec.filter {
        TextureFilter::Nearest => ImageFilterMode::Nearest,
        TextureFilter::Linear => ImageFilterMode::Linear,
    };
    let anisotropy = if filter == ImageFilterMode::Linear {
        spec.anisotropy.clamp(1, 16)
    } else {
        1
    };
    ImageSamplerDescriptor {
        address_mode_u: address_mode,
        address_mode_v: address_mode,
        address_mode_w: address_mode,
        mag_filter: filter,
        min_filter: filter,
        mipmap_filter: filter,
        anisotropy_clamp: anisotropy,
        ..Default::default()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ColorDesc;

    #[test]
    fn emissive_desc_maps_to_standard_material() {
        let mut app = App::new();
        app.add_plugins((MinimalPlugins, AssetPlugin::default()));
        let asset_server = app.world().resource::<AssetServer>();
        let desc = MaterialDesc {
            emissive_color: ColorDesc::srgb(0.5, 0.25, 1.0),
            emissive_intensity: 1000.0,
            ..Default::default()
        };

        let material = material_from_desc(asset_server, &desc);

        assert_eq!(material.emissive.red, 500.0);
        assert_eq!(material.emissive.green, 250.0);
        assert_eq!(material.emissive.blue, 1000.0);
    }

    #[test]
    fn shared_uv_transform_maps_to_standard_material() {
        let mut app = App::new();
        app.add_plugins((MinimalPlugins, AssetPlugin::default()));
        let asset_server = app.world().resource::<AssetServer>();
        let desc = MaterialDesc {
            uv_offset: Vec2::new(0.25, -0.5),
            uv_tiling: Vec2::new(4.0, 2.0),
            ..Default::default()
        };

        let material = material_from_desc(asset_server, &desc);

        assert_eq!(material.uv_transform.translation, desc.uv_offset);
        assert_eq!(material.uv_transform.matrix2.x_axis.x, desc.uv_tiling.x);
        assert_eq!(material.uv_transform.matrix2.y_axis.y, desc.uv_tiling.y);
    }

    #[test]
    fn runtime_sampler_maps_authored_wrap_filter_and_anisotropy() {
        let spec = RuntimeTextureLoadSpec {
            path: "texture.basis".into(),
            is_srgb: false,
            filter: TextureFilter::Linear,
            wrap: TextureWrap::Repeat,
            anisotropy: 64,
        };
        let sampler = runtime_image_sampler_descriptor(&spec);
        assert_eq!(sampler.address_mode_u, ImageAddressMode::Repeat);
        assert_eq!(sampler.address_mode_v, ImageAddressMode::Repeat);
        assert_eq!(sampler.address_mode_w, ImageAddressMode::Repeat);
        assert_eq!(sampler.min_filter, ImageFilterMode::Linear);
        assert_eq!(sampler.anisotropy_clamp, 16);

        let mut nearest = spec;
        nearest.filter = TextureFilter::Nearest;
        nearest.wrap = TextureWrap::Mirror;
        let sampler = runtime_image_sampler_descriptor(&nearest);
        assert_eq!(sampler.address_mode_u, ImageAddressMode::MirrorRepeat);
        assert_eq!(sampler.anisotropy_clamp, 1);
    }
}
