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

#if COMPILE_WITH_ASSETS_IMPORTER

#include "AssetsImportingManager.h"
#include "Engine/Core/Utilities.h"
#include "Engine/Serialization/JsonWriters.h"
#include "Engine/Threading/MainThreadTask.h"
#include "Engine/Content/Storage/ContentStorageManager.h"
#include "Engine/Content/Content.h"
#include "Engine/Content/Cache/AssetsCache.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Core/Log.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Platform/Platform.h"
#include "ImportTexture.h"
#include "ImportModelFile.h"
#include "ImportAudio.h"
#include "ImportShader.h"
#include "ImportFont.h"
#include "CreateMaterial.h"
#include "CreateMaterialInstance.h"
#include "CreateRawData.h"
#include "CreateCollisionData.h"
#include "CreateAnimationGraph.h"
#include "CreateSkeletonMask.h"
#include "CreateParticleEmitter.h"
#include "CreateParticleSystem.h"
#include "CreateSceneAnimation.h"
#include "CreateMaterialFunction.h"
#include "CreateParticleEmitterFunction.h"
#include "CreateAnimationGraphFunction.h"
#include "CreateVisualScript.h"

// Tags used to detect asset creation mode
const String AssetsImportingManager::CreateTextureTag(TEXT("Texture"));
const String AssetsImportingManager::CreateTextureAsTextureDataTag(TEXT("TextureAsTextureData"));
const String AssetsImportingManager::CreateTextureAsInitDataTag(TEXT("TextureAsInitData"));
const String AssetsImportingManager::CreateMaterialTag(TEXT("Material"));
const String AssetsImportingManager::CreateMaterialInstanceTag(TEXT("MaterialInstance"));
const String AssetsImportingManager::CreateCubeTextureTag(TEXT("CubeTexture"));
const String AssetsImportingManager::CreateModelTag(TEXT("Model"));
const String AssetsImportingManager::CreateRawDataTag(TEXT("RawData"));
const String AssetsImportingManager::CreateCollisionDataTag(TEXT("CollisionData"));
const String AssetsImportingManager::CreateAnimationGraphTag(TEXT("AnimationGraph"));
const String AssetsImportingManager::CreateSkeletonMaskTag(TEXT("SkeletonMask"));
const String AssetsImportingManager::CreateParticleEmitterTag(TEXT("ParticleEmitter"));
const String AssetsImportingManager::CreateParticleSystemTag(TEXT("ParticleSystem"));
const String AssetsImportingManager::CreateSceneAnimationTag(TEXT("SceneAnimation"));
const String AssetsImportingManager::CreateMaterialFunctionTag(TEXT("MaterialFunction"));
const String AssetsImportingManager::CreateParticleEmitterFunctionTag(TEXT("ParticleEmitterFunction"));
const String AssetsImportingManager::CreateAnimationGraphFunctionTag(TEXT("AnimationGraphFunction"));
const String AssetsImportingManager::CreateVisualScriptTag(TEXT("VisualScript"));

class AssetsImportingManagerService : public EngineService
{
public:

    AssetsImportingManagerService()
        : EngineService(TEXT("AssetsImportingManager"), -400)
    {
    }

    bool Init() override;
    void Dispose() override;
};

AssetsImportingManagerService AssetsImportingManagerServiceInstance;

Array<AssetImporter> AssetsImportingManager::Importers;
Array<AssetCreator> AssetsImportingManager::Creators;

CreateAssetContext::CreateAssetContext(const StringView& inputPath, const StringView& outputPath, const Guid& id, void* arg)
{
    InputPath = inputPath;
    TargetAssetPath = outputPath;
    CustomArg = arg;
    Data.Header.ID = id;
    SkipMetadata = false;

    // TODO: we should use ASNI only chars path (Assimp can use only that kind)
    OutputPath = Content::CreateTemporaryAssetPath();
}

CreateAssetResult CreateAssetContext::Run(const CreateAssetFunction& callback)
{
    ASSERT(callback.IsBinded());

    // Call action
    auto result = callback(*this);
    if (result != CreateAssetResult::Ok)
        return result;

    // Validate assigned TypeID
    if (Data.Header.TypeName.IsEmpty())
    {
        LOG(Warning, "Assigned asset TypeName is invalid.");
        return CreateAssetResult::InvalidTypeID;
    }

    // Add import metadata to the file (if it's empty)
    if (!SkipMetadata && Data.Metadata.IsInvalid())
    {
        rapidjson_flax::StringBuffer buffer;
        CompactJsonWriter writer(buffer);
        writer.StartObject();
        {
            AddMeta(writer);
        }
        writer.EndObject();
        Data.Metadata.Copy((const byte*)buffer.GetString(), (uint32)buffer.GetSize());
    }

    // Save file
    result = Save();

    if (result == CreateAssetResult::Ok)
    {
        _applyChangesResult = CreateAssetResult::Abort;
        INVOKE_ON_MAIN_THREAD(CreateAssetContext, CreateAssetContext::ApplyChanges, this);
        result = _applyChangesResult;
    }
    FileSystem::DeleteFile(OutputPath);

    return result;
}

bool CreateAssetContext::AllocateChunk(int32 index)
{
    // Check index
    if (index < 0 || index >= ASSET_FILE_DATA_CHUNKS)
    {
        LOG(Warning, "Invalid asset chunk index {0}.", index);
        return true;
    }

    // Check if chunk has been already allocated
    if (Data.Header.Chunks[index] != nullptr)
    {
        LOG(Warning, "Asset chunk {0} has been already allocated.", index);
        return true;
    }

    // Create new chunk
    const auto chunk = New<FlaxChunk>();
    Data.Header.Chunks[index] = chunk;

    if (chunk == nullptr)
    {
        OUT_OF_MEMORY;
    }

    return false;
}

void CreateAssetContext::AddMeta(JsonWriter& writer) const
{
    writer.JKEY("ImportPath");
    writer.String(InputPath);
    writer.JKEY("ImportUsername");
    writer.String(Platform::GetUserName());
}

CreateAssetResult CreateAssetContext::Save()
{
    return FlaxStorage::Create(OutputPath, Data) ? CreateAssetResult::CannotSaveFile : CreateAssetResult::Ok;
}

void CreateAssetContext::ApplyChanges()
{
    // Get access
    auto storage = ContentStorageManager::TryGetStorage(TargetAssetPath);
    if (storage && storage->IsLoaded())
    {
        storage->CloseFileHandles();
    }

    // Move file
    if (FileSystem::MoveFile(TargetAssetPath, OutputPath, true))
    {
        LOG(Warning, "Cannot move imported file to the destination path.");
        _applyChangesResult = CreateAssetResult::CannotSaveFile;
        return;
    }

    // Reload (any asset using it will receive OnStorageReloaded event and handle it)
    if (storage)
    {
        storage->Reload();
    }

    _applyChangesResult = CreateAssetResult::Ok;
}

const AssetImporter* AssetsImportingManager::GetImporter(const String& extension)
{
    for (int32 i = 0; i < Importers.Count(); i++)
    {
        if (Importers[i].FileExtension.Compare(extension, StringSearchCase::IgnoreCase) == 0)
            return &Importers[i];
    }
    return nullptr;
}

const AssetCreator* AssetsImportingManager::GetCreator(const String& tag)
{
    for (int32 i = 0; i < Creators.Count(); i++)
    {
        if (Creators[i].Tag == tag)
            return &Creators[i];
    }
    return nullptr;
}

bool AssetsImportingManager::Create(const CreateAssetFunction& importFunc, const StringView& outputPath, Guid& assetId, void* arg)
{
    return Create(importFunc, StringView::Empty, outputPath, assetId, arg);
}

bool AssetsImportingManager::Create(const String& tag, const StringView& outputPath, Guid& assetId, void* arg)
{
    const auto creator = GetCreator(tag);
    if (creator == nullptr)
    {
        LOG(Warning, "Cannot find asset creator object for tag \'{0}\'.", tag);
        return true;
    }

    return Create(creator->Callback, outputPath, assetId, arg);
}

bool AssetsImportingManager::Import(const StringView& inputPath, const StringView& outputPath, Guid& assetId, void* arg)
{
    ASSERT(outputPath.EndsWith(StringView(ASSET_FILES_EXTENSION)));

    LOG(Info, "Importing file '{0}' to '{1}'...", inputPath, outputPath);

    // Check if input file exists
    if (!FileSystem::FileExists(inputPath))
    {
        LOG(Error, "Missing file '{0}'", inputPath);
        return true;
    }

    // Get file extension and try to find import function for it
    const String extension = FileSystem::GetExtension(inputPath).ToLower();

    // Special case for raw assets
    const String assetExtension = ASSET_FILES_EXTENSION;
    if (assetExtension.Compare(extension, StringSearchCase::IgnoreCase) == 0)
    {
        // Simply copy file (content layer will resolve duplicated IDs, etc.)
        return FileSystem::CopyFile(outputPath, inputPath);
    }

    // Find valid importer for that file
    const auto importer = GetImporter(extension);
    if (importer == nullptr)
    {
        LOG(Error, "Cannot import file \'{0}\'. Unknown file type.", inputPath);
        return true;
    }

    return Create(importer->Callback, inputPath, outputPath, assetId, arg);
}

bool AssetsImportingManager::ImportIfEdited(const StringView& inputPath, const StringView& outputPath, Guid& assetId, void* arg)
{
    ASSERT(outputPath.EndsWith(StringView(ASSET_FILES_EXTENSION)));

    // Check if asset not exists
    if (!FileSystem::FileExists(outputPath))
    {
        return Import(inputPath, outputPath, assetId, arg);
    }

    // Check if need to reimport file (it could be checked via asset header but this way is faster and works in general)
    const DateTime sourceEdited = FileSystem::GetFileLastEditTime(inputPath);
    const DateTime assetEdited = FileSystem::GetFileLastEditTime(outputPath);
    if (sourceEdited > assetEdited)
    {
        return Import(inputPath, outputPath, assetId, arg);
    }

    // No import
    if (!assetId.IsValid())
    {
        AssetInfo assetInfo;
        Content::GetAssetInfo(outputPath, assetInfo);
        assetId = assetInfo.ID;
    }
    return false;
}

bool AssetsImportingManager::Create(const Function<CreateAssetResult(CreateAssetContext&)>& callback, const StringView& inputPath, const StringView& outputPath, Guid& assetId, void* arg)
{
    const auto startTime = Platform::GetTimeSeconds();

    // Pick ID if not specified
    if (!assetId.IsValid())
        assetId = Guid::New();

    // Check if asset at target path is loaded
    AssetReference<Asset> asset = Content::GetAsset(outputPath);
    if (asset)
    {
        // Use the same ID
        assetId = asset->GetID();
    }
    else
    {
        // Check if asset already exists and isn't empty
        if (FileSystem::FileExists(outputPath) && FileSystem::GetFileSize(outputPath) > 0)
        {
            // Load storage container
            const auto storage = ContentStorageManager::GetStorage(outputPath);
            if (storage)
            {
                // Try to load old asset header and then use old ID
                Array<FlaxStorage::Entry> e;
                storage->GetEntries(e);
                if (e.Count() == 1)
                {
                    // Override asset id (use the old value)
                    assetId = e[0].ID;
                    LOG(Info, "Asset already exists. Using old ID: {0}", assetId);
                }
                else
                {
                    LOG(Warning, "File {0} is a package.", outputPath);
                }
            }
            else
            {
                LOG(Warning, "Cannot open storage container at {0}", outputPath);
            }
        }
        else
        {
            // Ensure that path exists
            const String outputDirectory = StringUtils::GetDirectoryName(*outputPath);
            if (FileSystem::CreateDirectory(outputDirectory))
            {
                LOG(Warning, "Cannot create directory '{0}'", outputDirectory);
            }
        }
    }

    // Import file
    CreateAssetContext context(inputPath, outputPath, assetId, arg);
    const auto result = context.Run(callback);

    // Clear reference
    asset = nullptr;

    // Switch result
    if (result == CreateAssetResult::Ok)
    {
        // Register asset
        Content::GetRegistry()->RegisterAsset(context.Data.Header, outputPath);

        // Done
        const auto endTime = Platform::GetTimeSeconds();
        LOG(Info, "Asset '{0}' imported in {2}s! {1}", outputPath, context.Data.Header.ToString(), Utilities::RoundTo2DecimalPlaces(endTime - startTime));
    }
    else if (result == CreateAssetResult::Abort)
    {
        // Do nothing
        return true;
    }
    else
    {
        LOG(Error, "Cannot import file '{0}'! Result: {1}", inputPath, ::ToString(result));
        return true;
    }

    return false;
}

bool AssetsImportingManagerService::Init()
{
    // Initialize with in-build importers
    AssetImporter InBuildImporters[] =
    {
        // Textures and Cube Textures
        { TEXT("tga"), ImportTexture::Import },
        { TEXT("dds"), ImportTexture::Import },
        { TEXT("png"), ImportTexture::Import },
        { TEXT("bmp"), ImportTexture::Import },
        { TEXT("gif"), ImportTexture::Import },
        { TEXT("tiff"), ImportTexture::Import },
        { TEXT("tif"), ImportTexture::Import },
        { TEXT("jpeg"), ImportTexture::Import },
        { TEXT("jpg"), ImportTexture::Import },
        { TEXT("hdr"), ImportTexture::Import },
        { TEXT("raw"), ImportTexture::Import },

        // IES Profiles
        { TEXT("ies"), ImportTexture::ImportIES },

        // Shaders
        { TEXT("shader"), ImportShader::Import },

        // Audio
        { TEXT("wav"), ImportAudio::ImportWav },
        { TEXT("mp3"), ImportAudio::ImportMp3 },
#if COMPILE_WITH_OGG_VORBIS
        { TEXT("ogg"), ImportAudio::ImportOgg },
#endif

        // Fonts
        { TEXT("ttf"), ImportFont::Import },
        { TEXT("otf"), ImportFont::Import },

        // Models
        { TEXT("obj"), ImportModelFile::Import },
        { TEXT("fbx"), ImportModelFile::Import },
        { TEXT("x"), ImportModelFile::Import },
        { TEXT("dae"), ImportModelFile::Import },
        { TEXT("gltf"), ImportModelFile::Import },
        { TEXT("glb"), ImportModelFile::Import },

        // Models (untested formats - may fail :/)
        { TEXT("blend"), ImportModelFile::Import },
        { TEXT("bvh"), ImportModelFile::Import },
        { TEXT("ase"), ImportModelFile::Import },
        { TEXT("ply"), ImportModelFile::Import },
        { TEXT("dxf"), ImportModelFile::Import },
        { TEXT("ifc"), ImportModelFile::Import },
        { TEXT("nff"), ImportModelFile::Import },
        { TEXT("smd"), ImportModelFile::Import },
        { TEXT("vta"), ImportModelFile::Import },
        { TEXT("mdl"), ImportModelFile::Import },
        { TEXT("md2"), ImportModelFile::Import },
        { TEXT("md3"), ImportModelFile::Import },
        { TEXT("md5mesh"), ImportModelFile::Import },
        { TEXT("q3o"), ImportModelFile::Import },
        { TEXT("q3s"), ImportModelFile::Import },
        { TEXT("ac"), ImportModelFile::Import },
        { TEXT("stl"), ImportModelFile::Import },
        { TEXT("lwo"), ImportModelFile::Import },
        { TEXT("lws"), ImportModelFile::Import },
        { TEXT("lxo"), ImportModelFile::Import },
    };
    AssetsImportingManager::Importers.Add(InBuildImporters, ARRAY_COUNT(InBuildImporters));

    // Initialize with in-build creators
    AssetCreator InBuildCreators[] =
    {
        // Textures
        { AssetsImportingManager::CreateTextureTag, ImportTexture::Import },
        { AssetsImportingManager::CreateTextureAsTextureDataTag, ImportTexture::ImportAsTextureData },
        { AssetsImportingManager::CreateTextureAsInitDataTag, ImportTexture::ImportAsInitData },
        { AssetsImportingManager::CreateCubeTextureTag, ImportTexture::ImportCube },

        // Materials
        { AssetsImportingManager::CreateMaterialTag, CreateMaterial::Create },
        { AssetsImportingManager::CreateMaterialInstanceTag, CreateMaterialInstance::Create },

        // Models
        { AssetsImportingManager::CreateModelTag, ImportModelFile::Create },

        // Other
        { AssetsImportingManager::CreateRawDataTag, CreateRawData::Create },
        { AssetsImportingManager::CreateCollisionDataTag, CreateCollisionData::Create },
        { AssetsImportingManager::CreateAnimationGraphTag, CreateAnimationGraph::Create },
        { AssetsImportingManager::CreateSkeletonMaskTag, CreateSkeletonMask::Create },
        { AssetsImportingManager::CreateParticleEmitterTag, CreateParticleEmitter::Create },
        { AssetsImportingManager::CreateParticleSystemTag, CreateParticleSystem::Create },
        { AssetsImportingManager::CreateSceneAnimationTag, CreateSceneAnimation::Create },
        { AssetsImportingManager::CreateMaterialFunctionTag, CreateMaterialFunction::Create },
        { AssetsImportingManager::CreateParticleEmitterFunctionTag, CreateParticleEmitterFunction::Create },
        { AssetsImportingManager::CreateAnimationGraphFunctionTag, CreateAnimationGraphFunction::Create },
        { AssetsImportingManager::CreateVisualScriptTag, CreateVisualScript::Create },
    };
    AssetsImportingManager::Creators.Add(InBuildCreators, ARRAY_COUNT(InBuildCreators));

    return false;
}

void AssetsImportingManagerService::Dispose()
{
    // Cleanup
    AssetsImportingManager::Importers.Clear();
    AssetsImportingManager::Creators.Clear();
}

#endif
