// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.

#if COMPILE_WITH_MATERIAL_GRAPH

#include "MaterialGenerator.h"
#include "Engine/Core/Math/Vector4.h"
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Content/Assets/MaterialInstance.h"
#include "Engine/Content/Assets/Material.h"
#include "Engine/Serialization/MemoryReadStream.h"
#include "Engine/Engine/Engine.h"

MaterialGenerator::MaterialGraphBoxesMapping MaterialGenerator::MaterialGraphBoxesMappings[] =
{
    { 0, nullptr, MaterialTreeType::PixelShader, MaterialValue::Zero },
    { 1, TEXT("Color"), MaterialTreeType::PixelShader, MaterialValue::InitForZero(VariantType::Float3) },
    { 2, TEXT("Mask"), MaterialTreeType::PixelShader, MaterialValue::InitForOne(VariantType::Float) },
    { 3, TEXT("Emissive"), MaterialTreeType::PixelShader, MaterialValue::InitForZero(VariantType::Float3) },
    { 4, TEXT("Metalness"), MaterialTreeType::PixelShader, MaterialValue::InitForZero(VariantType::Float) },
    { 5, TEXT("Specular"), MaterialTreeType::PixelShader, MaterialValue::InitForHalf(VariantType::Float) },
    { 6, TEXT("Roughness"), MaterialTreeType::PixelShader, MaterialValue::InitForHalf(VariantType::Float) },
    { 7, TEXT("AO"), MaterialTreeType::PixelShader, MaterialValue::InitForOne(VariantType::Float) },
    { 8, TEXT("TangentNormal"), MaterialTreeType::PixelShader, MaterialValue(VariantType::Float3, TEXT("float3(0, 0, 1.0)")) },
    { 9, TEXT("Opacity"), MaterialTreeType::PixelShader, MaterialValue::InitForOne(VariantType::Float) },
    { 10, TEXT("Refraction"), MaterialTreeType::PixelShader, MaterialValue::InitForOne(VariantType::Float) },
    { 11, TEXT("PositionOffset"), MaterialTreeType::VertexShader, MaterialValue::InitForZero(VariantType::Float3) },
    { 12, TEXT("TessellationMultiplier"), MaterialTreeType::VertexShader, MaterialValue(VariantType::Float, TEXT("4.0f")) },
    { 13, TEXT("WorldDisplacement"), MaterialTreeType::DomainShader, MaterialValue::InitForZero(VariantType::Float3) },
    { 14, TEXT("SubsurfaceColor"), MaterialTreeType::PixelShader, MaterialValue::InitForZero(VariantType::Float3) },
};

const MaterialGenerator::MaterialGraphBoxesMapping& MaterialGenerator::GetMaterialRootNodeBox(MaterialGraphBoxes box)
{
    static_assert(ARRAY_COUNT(MaterialGenerator::MaterialGraphBoxesMappings) == 15, "Invalid amount of boxes added for root node. Please update the code above");
    return MaterialGraphBoxesMappings[static_cast<int32>(box)];
}

void MaterialGenerator::AddLayer(MaterialLayer* layer)
{
    _layers.Add(layer);
}

MaterialLayer* MaterialGenerator::GetLayer(const Guid& id, Node* caller)
{
    // Find layer first
    for (int32 i = 0; i < _layers.Count(); i++)
    {
        if (_layers[i]->ID == id)
        {
            // Found
            return _layers[i];
        }
    }

    // Load asset
    Asset* asset = Assets.LoadAsync<MaterialBase>(id);
    if (asset == nullptr || asset->WaitForLoaded(30000))
    {
        OnError(caller, nullptr, TEXT("Failed to load material asset."));
        return nullptr;
    }

    // Special case for engine exit event
    if (Engine::ShouldExit())
    {
        // End
        return nullptr;
    }

    // Check if load failed
    if (!asset->IsLoaded())
    {
        OnError(caller, nullptr, TEXT("Failed to load material asset."));
        return nullptr;
    }

    // Check if it's a material instance
    Material* material = nullptr;
    Asset* iterator = asset;
    while (material == nullptr)
    {
        // Wait for material to be loaded
        if (iterator->WaitForLoaded())
        {
            OnError(caller, nullptr, TEXT("Material asset load failed."));
            return nullptr;
        }

        if (iterator->GetTypeName() == MaterialInstance::TypeName)
        {
            auto instance = ((MaterialInstance*)iterator);

            // Check if base material has been assigned
            if (instance->GetBaseMaterial() == nullptr)
            {
                OnError(caller, nullptr, TEXT("Material instance has missing base material."));
                return nullptr;
            }

            iterator = instance->GetBaseMaterial();
        }
        else
        {
            material = ((Material*)iterator);

            // Check if instanced material is not instancing current material
            ASSERT(GetRootLayer());
            if (material->GetID() == GetRootLayer()->ID)
            {
                OnError(caller, nullptr, TEXT("Cannot use instance of the current material as layer."));
                return nullptr;
            }
        }
    }
    ASSERT(material);

    // Get surface data
    BytesContainer surfaceData = material->LoadSurface(true);
    if (surfaceData.IsInvalid())
    {
        OnError(caller, nullptr, TEXT("Cannot load surface data."));
        return nullptr;
    }
    MemoryReadStream surfaceDataStream(surfaceData.Get(), surfaceData.Length());

    // Load layer
    auto layer = MaterialLayer::Load(id, &surfaceDataStream, material->GetInfo(), material->ToString());

    // Validate layer info
    if (layer == nullptr)
    {
        OnError(caller, nullptr, TEXT("Cannot load layer."));
        return nullptr;
    }
#if 0 // allow mixing material domains because material generator uses the base layer for features usage checks and sub layers produce only Material structure for blending or extracting
	if (layer->Domain != GetRootLayer()->Domain) // TODO: maybe allow solid on transparent?
	{
		OnError(caller, nullptr, TEXT("Cannot mix materials with different Domains."));
		return nullptr;
	}
#endif

    // Override parameters values if using material instance
    if (asset->GetTypeName() == MaterialInstance::TypeName)
    {
        auto instance = ((MaterialInstance*)asset);
        auto instanceParams = &instance->Params;

        // Clone overriden parameters values
        for (auto& param : layer->Graph.Parameters)
        {
            const auto instanceParam = instanceParams->Get(param.Identifier);
            if (instanceParam && instanceParam->IsOverride())
            {
                param.Value = instanceParam->GetValue();

                // Fold object references to their ids
                if (param.Value.Type == VariantType::Object)
                    param.Value = param.Value.AsObject ? param.Value.AsObject->GetID() : Guid::Empty;
                else if (param.Value.Type == VariantType::Asset)
                    param.Value = param.Value.AsObject ? param.Value.AsObject->GetID() : Guid::Empty;
            }
        }
    }

    // Prepare layer
    layer->Prepare();
    const bool allowPublicParameters = true;
    prepareLayer(layer, allowPublicParameters);

    AddLayer(layer);
    return layer;
}

MaterialLayer* MaterialGenerator::GetRootLayer() const
{
    return _layers.Count() > 0 ? _layers[0] : nullptr;
}

void MaterialGenerator::prepareLayer(MaterialLayer* layer, bool allowVisibleParams)
{
    LayerParamMapping m;

    // Ensure that layer hasn't been used
    ASSERT(layer->HasAnyVariableName() == false);

    // Add all parameters to be saved in the result material parameters collection (perform merge)
    bool isRooLayer = GetRootLayer() == layer;
    for (int32 j = 0; j < layer->Graph.Parameters.Count(); j++)
    {
        const auto param = &layer->Graph.Parameters[j];

        // For all not root layers (sub-layers) we won't to change theirs ID in order to prevent duplicated ID)
        m.SrcId = param->Identifier;
        m.DstId = param->Identifier;
        if (!isRooLayer)
        {
            // Generate new ID (stable permutation based on the original ID)
            m.DstId.A += _parameters.Count() * 17 + 13;
        }
        layer->ParamIdsMappings.Add(m);

        SerializedMaterialParam& mp = _parameters.AddOne();
        mp.ID = m.DstId;
        mp.IsPublic = param->IsPublic && allowVisibleParams;
        mp.Override = true;
        mp.Name = param->Name;
        mp.ShaderName = getParamName(_parameters.Count());
        mp.Type = MaterialParameterType::Bool;
        mp.AsBool = false;
        switch (param->Type.Type)
        {
        case VariantType::Bool:
            mp.Type = MaterialParameterType::Bool;
            mp.AsBool = param->Value.AsBool;
            break;
        case VariantType::Int:
            mp.Type = MaterialParameterType::Integer;
            mp.AsInteger = param->Value.AsInt;
            break;
        case VariantType::Int64:
            mp.Type = MaterialParameterType::Integer;
            mp.AsInteger = (int32)param->Value.AsInt64;
            break;
        case VariantType::Uint:
            mp.Type = MaterialParameterType::Integer;
            mp.AsInteger = (int32)param->Value.AsUint;
            break;
        case VariantType::Uint64:
            mp.Type = MaterialParameterType::Integer;
            mp.AsInteger = (int32)param->Value.AsUint64;
            break;
        case VariantType::Float:
            mp.Type = MaterialParameterType::Float;
            mp.AsFloat = param->Value.AsFloat;
            break;
        case VariantType::Double:
            mp.Type = MaterialParameterType::Float;
            mp.AsFloat = (float)param->Value.AsDouble;
            break;
        case VariantType::Float2:
            mp.Type = MaterialParameterType::Vector2;
            mp.AsFloat2 = param->Value.AsFloat2();
            break;
        case VariantType::Float3:
            mp.Type = MaterialParameterType::Vector3;
            mp.AsFloat3 = param->Value.AsFloat3();
            break;
        case VariantType::Float4:
            mp.Type = MaterialParameterType::Vector4;
            *(Float4*)&mp.AsData = param->Value.AsFloat4();
            break;
        case VariantType::Double2:
            mp.Type = MaterialParameterType::Vector2;
            mp.AsFloat2 = (Float2)param->Value.AsDouble2();
            break;
        case VariantType::Double3:
            mp.Type = MaterialParameterType::Vector3;
            mp.AsFloat3 = (Float3)param->Value.AsDouble3();
            break;
        case VariantType::Double4:
            mp.Type = MaterialParameterType::Vector4;
            *(Float4*)&mp.AsData = (Float4)param->Value.AsDouble4();
            break;
        case VariantType::Color:
            mp.Type = MaterialParameterType::Color;
            mp.AsColor = param->Value.AsColor();
            break;
        case VariantType::Matrix:
            mp.Type = MaterialParameterType::Matrix;
            *(Matrix*)&mp.AsData = *(Matrix*)param->Value.AsBlob.Data;
            break;
        case VariantType::Asset:
            if (!param->Type.TypeName)
            {
                OnError(nullptr, nullptr, String::Format(TEXT("Invalid or unsupported material parameter type {0}."), param->Type));
                break;
            }
            if (StringUtils::Compare(param->Type.TypeName, "FlaxEngine.Texture") == 0)
            {
                mp.Type = MaterialParameterType::Texture;

                // Special case for Normal Maps
                auto asset = (Texture*)::LoadAsset((Guid)param->Value, Texture::TypeInitializer);
                if (asset && !asset->WaitForLoaded() && asset->IsNormalMap())
                    mp.Type = MaterialParameterType::NormalMap;
            }
            else if (StringUtils::Compare(param->Type.TypeName, "FlaxEngine.CubeTexture") == 0)
                mp.Type = MaterialParameterType::CubeTexture;
            else
                OnError(nullptr, nullptr, String::Format(TEXT("Invalid or unsupported material parameter type {0}."), param->Type));
            mp.AsGuid = (Guid)param->Value;
            break;
        case VariantType::Object:
            if (!param->Type.TypeName)
            {
                OnError(nullptr, nullptr, String::Format(TEXT("Invalid or unsupported material parameter type {0}."), param->Type));
                break;
            }
            if (StringUtils::Compare(param->Type.TypeName, "FlaxEngine.GPUTexture") == 0)
                mp.Type = MaterialParameterType::GPUTexture;
            else
                OnError(nullptr, nullptr, String::Format(TEXT("Invalid or unsupported material parameter type {0}."), param->Type));
            mp.AsGuid = (Guid)param->Value;
            break;
        case VariantType::Enum:
            if (!param->Type.TypeName)
            {
                OnError(nullptr, nullptr, String::Format(TEXT("Invalid or unsupported material parameter type {0}."), param->Type));
                break;
            }
            if (StringUtils::Compare(param->Type.TypeName, "FlaxEngine.MaterialSceneTextures") == 0)
                mp.Type = MaterialParameterType::SceneTexture;
            else if (StringUtils::Compare(param->Type.TypeName, "FlaxEngine.ChannelMask") == 0)
                mp.Type = MaterialParameterType::ChannelMask;
            else
                OnError(nullptr, nullptr, String::Format(TEXT("Invalid or unsupported material parameter type {0}."), param->Type));
            mp.AsInteger = (int32)param->Value.AsUint64;
            break;
        default:
            OnError(nullptr, nullptr, String::Format(TEXT("Invalid or unsupported material parameter type {0}."), param->Type));
            break;
        }
    }
}

#endif
