// Copyright (c) Wojciech Figat. All rights reserved.

#include "ParticleEmitterFunction.h"
#include "Particles.h"
#include "Engine/Core/Log.h"
#include "Engine/Serialization/MemoryReadStream.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Profiler/ProfilerMemory.h"
#if USE_EDITOR
#include "Engine/Core/Types/DataContainer.h"
#include "Engine/Serialization/MemoryWriteStream.h"
#endif
#include "Engine/Content/Factories/BinaryAssetFactory.h"

void InitParticleEmitterFunctionCall(const Guid& assetId, AssetReference<Asset>& asset, bool& usesParticleData, ParticleLayout& layout)
{
    const auto function = Content::Load<ParticleEmitterFunction>(assetId);
    asset = function;
    if (function)
    {
        // Insert any used particle data into the calling graph
        for (const ParticleAttribute& e : function->Graph.Layout.Attributes)
        {
            if (layout.FindAttribute(e.Name, e.ValueType) == -1)
                layout.AddAttribute(e.Name, e.ValueType);
        }

        // Detect if function needs to be evaluated per-particle
        for (int32 i = 0; i < function->Outputs.Count() && !usesParticleData; i++)
        {
            usesParticleData = function->Graph.Nodes[function->Outputs.Get()[i]].UsesParticleData;
        }
    }
}

REGISTER_BINARY_ASSET(ParticleEmitterFunction, "FlaxEngine.ParticleEmitterFunction", false);

ParticleEmitterFunction::ParticleEmitterFunction(const SpawnParams& params, const AssetInfo* info)
    : BinaryAsset(params, info)
{
}

Asset::LoadResult ParticleEmitterFunction::load()
{
    PROFILE_MEM(Particles);

    // Load graph
    const auto surfaceChunk = GetChunk(0);
    if (!surfaceChunk || !surfaceChunk->IsLoaded())
        return LoadResult::MissingDataChunk;
    MemoryReadStream stream(surfaceChunk->Get(), surfaceChunk->Size());
    if (Graph.Load(&stream, USE_EDITOR))
        return LoadResult::Failed;
    for (int32 i = 0; i < Graph.Nodes.Count(); i++)
    {
        // Initialize all used nodes (starting from function output as roots)
        if (Graph.Nodes[i].Type == GRAPH_NODE_MAKE_TYPE(16, 2))
        {
            Graph.InitializeNode(&Graph.Nodes[i]);
        }
    }
#if COMPILE_WITH_PARTICLE_GPU_GRAPH
    stream.SetPosition(0);
    if (GraphGPU.Load(&stream, false))
        return LoadResult::Failed;
#endif

    // Cache input and output nodes
    bool tooManyInputsOutputs = false;
    for (int32 i = 0; i < Graph.Nodes.Count(); i++)
    {
        auto& node = Graph.Nodes[i];
        if (node.Type == GRAPH_NODE_MAKE_TYPE(16, 1))
        {
            if (Inputs.Count() < 16)
                Inputs.Add(i);
            else
                tooManyInputsOutputs = true;
        }
        else if (node.Type == GRAPH_NODE_MAKE_TYPE(16, 2))
        {
            if (Outputs.Count() < 16)
                Outputs.Add(i);
            else
                tooManyInputsOutputs = true;
        }
    }
    if (tooManyInputsOutputs)
    {
        LOG(Error, "Too many function inputs/outputs in '{0}'. The limit is max 16 inputs and max 16 outputs.", ToString());
    }

    return LoadResult::Ok;
}

void ParticleEmitterFunction::unload(bool isReloading)
{
    ScopeWriteLock systemScope(Particles::SystemLocker);
    Graph.Clear();
#if COMPILE_WITH_PARTICLE_GPU_GRAPH
    GraphGPU.Clear();
#endif
    Inputs.Clear();
    Outputs.Clear();
}

AssetChunksFlag ParticleEmitterFunction::getChunksToPreload() const
{
    return GET_CHUNK_FLAG(0);
}

bool ParticleEmitterFunction::LoadSurface(ParticleEmitterGraphCPU& graph, bool loadMeta)
{
    if (WaitForLoaded())
        return true;
    ScopeLock lock(Locker);
    if (HasChunk(0))
    {
        if (!LoadChunks(GET_CHUNK_FLAG(0)))
        {
            const auto surfaceChunk = GetChunk(0);
            MemoryReadStream stream(surfaceChunk->Get(), surfaceChunk->Size());
            return graph.Load(&stream, loadMeta);
        }
    }
    return true;
}

#if USE_EDITOR

BytesContainer ParticleEmitterFunction::LoadSurface()
{
    BytesContainer result;
    if (WaitForLoaded())
        return result;
    ScopeLock lock(Locker);
    if (HasChunk(0))
    {
        if (!LoadChunks(GET_CHUNK_FLAG(0)))
        {
            const auto surfaceChunk = GetChunk(0);
            result.Copy(surfaceChunk->Data);
        }
    }
    return result;
}

#if COMPILE_WITH_PARTICLE_GPU_GRAPH

bool ParticleEmitterFunction::LoadSurface(ParticleEmitterGraphGPU& graph) const
{
    if (WaitForLoaded())
        return true;
    ScopeLock lock(Locker);
    if (HasChunk(0))
    {
        if (!LoadChunks(GET_CHUNK_FLAG(0)))
        {
            const auto surfaceChunk = GetChunk(0);
            MemoryReadStream stream(surfaceChunk->Get(), surfaceChunk->Size());
            return graph.Load(&stream, false);
        }
    }
    return true;
}

#endif

void ParticleEmitterFunction::GetSignature(Array<StringView, FixedAllocation<32>>& types, Array<StringView, FixedAllocation<32>>& names)
{
    types.Resize(32);
    names.Resize(32);
    for (int32 i = 0; i < Inputs.Count(); i++)
    {
        auto& node = Graph.Nodes[Inputs[i]];
        types[i] = GetGraphFunctionTypeName_Deprecated(node.Values[0]);
        names[i] = (StringView)node.Values[1];
    }
    for (int32 i = 0; i < Outputs.Count(); i++)
    {
        auto& node = Graph.Nodes[Outputs[i]];
        types[i + 16] = GetGraphFunctionTypeName_Deprecated(node.Values[0]);
        names[i + 16] = (StringView)node.Values[1];
    }
}

bool ParticleEmitterFunction::SaveSurface(const BytesContainer& data) const
{
    if (OnCheckSave())
        return true;
    ScopeLock lock(Locker);

    // Set Visject Surface data
    auto surfaceChunk = GetOrCreateChunk(0);
    ASSERT(surfaceChunk != nullptr);
    surfaceChunk->Data.Copy(data);

    // Save asset
    AssetInitData initData;
    initData.SerializedVersion = 1;
    if (SaveAsset(initData))
    {
        LOG(Error, "Cannot save \'{0}\'", ToString());
        return true;
    }

    return false;
}

bool ParticleEmitterFunction::Save(const StringView& path)
{
    if (OnCheckSave(path))
        return true;
    ScopeLock lock(Locker);
    MemoryWriteStream writeStream;
    if (Graph.Save(&writeStream, true))
        return true;
    BytesContainer data;
    data.Link(ToSpan(writeStream));
    return SaveSurface(data);
}

#endif
