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

#include "BinaryAsset.h"
#include "Cache/AssetsCache.h"
#include "Storage/ContentStorageManager.h"
#include "Loading/Tasks/LoadAssetDataTask.h"
#include "Factories/BinaryAssetFactory.h"
#include "Engine/ContentImporters/AssetsImportingManager.h"
#include "Engine/Content/Content.h"
#include "Engine/Serialization/JsonTools.h"
#include "Engine/Debug/Exceptions/JsonParseException.h"
#include "Engine/Threading/ThreadPoolTask.h"
#if USE_EDITOR
#include "Engine/Platform/FileSystem.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Engine/Globals.h"
#endif

REGISTER_BINARY_ASSET_ABSTRACT(BinaryAsset, "FlaxEngine.BinaryAsset");

BinaryAsset::BinaryAsset(const SpawnParams& params, const AssetInfo* info)
    : Asset(params, info)
    , _storageRef(nullptr) // We link storage container later
    , _isSaving(false)
    , Storage(nullptr)
{
}

BinaryAsset::~BinaryAsset()
{
#if USE_EDITOR
    if (Storage)
        Storage->OnReloaded.Unbind<BinaryAsset, &BinaryAsset::OnStorageReloaded>(this);
#endif
}

bool BinaryAsset::Init(const FlaxStorageReference& storage, AssetHeader& header)
{
    // We allow to init asset only once like that
    ASSERT(Storage == nullptr && _header.ID.IsValid() == false);

    // Block initialization with a different storage
    bool isChanged = _storageRef != storage;
    if (Storage != nullptr && isChanged)
    {
        LOG(Error, "Asset \'{0}\' has been already initialized.", GetPath());
        return true;
    }

    // Get data
    _storageRef = storage;
    Storage = storage.Get();
    _header = header;

#if USE_EDITOR
    // Link for storage reload event
    if (Storage && isChanged)
        Storage->OnReloaded.Bind<BinaryAsset, &BinaryAsset::OnStorageReloaded>(this);
#endif

    return false;
}

bool BinaryAsset::Init(AssetInitData& initData)
{
    // Validate serialized version
    if (initData.SerializedVersion != GetSerializedVersion())
    {
        LOG(Error, "Asset \'{0}\' is using different serialized version. Loaded: {1}, Runtime: {2}.", GetPath(), initData.SerializedVersion, GetSerializedVersion());
        return true;
    }

    // Get asset data
    _header = initData.Header;
#if USE_EDITOR
    Metadata.Copy(initData.Metadata);
    ClearDependencies();
    Dependencies = initData.Dependencies;
    for (auto& e : Dependencies)
    {
        auto asset = Cast<BinaryAsset>(Content::GetAsset(e.First));
        if (asset)
        {
            asset->_dependantAssets.Add(this);
        }
    }
#endif

    return init(initData);
}

bool BinaryAsset::InitVirtual(AssetInitData& initData)
{
    // Be virtual
    _isVirtual = true;

    return Init(initData);
}

#if USE_EDITOR

#if COMPILE_WITH_ASSETS_IMPORTER

void BinaryAsset::Reimport() const
{
    const String importPath = GetImportPath();
    if (importPath.HasChars())
    {
        AssetsImportingManager::Import(importPath, GetPath());
    }
}

#endif

void BinaryAsset::GetImportMetadata(String& path, String& username) const
{
    if (Metadata.IsInvalid())
        return;

    // Parse metadata and try to get import info
    rapidjson_flax::Document document;
    document.Parse((const char*)Metadata.Get(), Metadata.Length());
    if (document.HasParseError() == false)
    {
        path = JsonTools::GetString(document, "ImportPath");
        username = JsonTools::GetString(document, "ImportUsername");
        if (path.HasChars() && FileSystem::IsRelative(path))
        {
            // Convert path back to thr absolute (eg. if stored in relative format)
            path = Globals::ProjectFolder / path;
            StringUtils::PathRemoveRelativeParts(path);
        }
    }
    else
    {
        Log::JsonParseException(document.GetParseError(), document.GetErrorOffset(), GetPath());
    }
}

String BinaryAsset::GetImportPath() const
{
    String path, username;
    GetImportMetadata(path, username);
    return path;
}

void BinaryAsset::ClearDependencies()
{
    for (auto& e : Dependencies)
    {
        auto asset = Cast<BinaryAsset>(Content::GetAsset(e.First));
        if (asset)
            asset->_dependantAssets.Remove(this);
    }
    Dependencies.Clear();
}

void BinaryAsset::AddDependency(BinaryAsset* asset)
{
    ASSERT_LOW_LAYER(asset);
    const Guid id = asset->GetID();
    for (auto& e : Dependencies)
    {
        if (e.First == id)
            return;
    }
    ASSERT(!asset->_dependantAssets.Contains(asset));
    Dependencies.Add(ToPair(id, FileSystem::GetFileLastEditTime(asset->GetPath())));
    asset->_dependantAssets.Add(this);
}

bool BinaryAsset::HasDependenciesModified() const
{
    AssetInfo info;
    for (const auto& e : Dependencies)
    {
        if (Content::GetAssetInfo(e.First, info))
        {
            const auto editTime = FileSystem::GetFileLastEditTime(info.Path);
            if (editTime > e.Second)
            {
                LOG(Info, "Asset {0} was modified - dependency of {1}", info.Path, GetPath());
                return true;
            }
        }
    }
    return false;
}

#endif

FlaxChunk* BinaryAsset::GetOrCreateChunk(int32 index)
{
    ASSERT(Math::IsInRange(index, 0, ASSET_FILE_DATA_CHUNKS - 1));

    // Try get
    auto chunk = _header.Chunks[index];
    if (chunk)
    {
        chunk->RegisterUsage();
        return chunk;
    }

    // Allocate
    ASSERT(Storage);
    _header.Chunks[index] = chunk = Storage->AllocateChunk();
    if (chunk)
        chunk->RegisterUsage();

    return chunk;
}

void BinaryAsset::SetChunk(int32 index, const Span<byte>& data)
{
    auto chunk = GetOrCreateChunk(index);
    if (chunk)
        chunk->Data.Copy(data.Get(), data.Length());
}

void BinaryAsset::ReleaseChunks() const
{
    for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
        ReleaseChunk(i);
}

void BinaryAsset::ReleaseChunk(int32 index) const
{
    auto chunk = GetChunk(index);
    if (chunk)
        chunk->Data.Release();
}

ContentLoadTask* BinaryAsset::RequestChunkDataAsync(int32 index)
{
    auto chunk = GetChunk(index);
    if (chunk != nullptr && chunk->IsLoaded())
    {
        // Data already here
        chunk->RegisterUsage();
        return nullptr;
    }

    // Spawn loading task
    return New<LoadAssetDataTask>(this, GET_CHUNK_FLAG(index));
}

void BinaryAsset::GetChunkData(int32 index, BytesContainer& data) const
{
    //ScopeLock lock(Locker);

    // Check if has data missing
    if (!HasChunkLoaded(index))
    {
        // Missing data
        data.Release();
        return;
    }

    // Get data
    auto chunk = GetChunk(index);
    data.Link(chunk->Data);
}

bool BinaryAsset::LoadChunk(int32 chunkIndex)
{
    ASSERT(Storage);

    const auto chunk = _header.Chunks[chunkIndex];
    if (chunk != nullptr
        && chunk->IsMissing()
        && chunk->ExistsInFile())
    {
        if (Storage->LoadAssetChunk(chunk))
            return true;
    }

    return false;
}

bool BinaryAsset::LoadChunks(AssetChunksFlag chunks)
{
    ASSERT(Storage);

    // Check if skip loading
    if (chunks == 0)
        return false;

    // Load all missing marked chunks
    for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
    {
        auto chunk = _header.Chunks[i];
        if (chunk != nullptr
            && chunks & GET_CHUNK_FLAG(i)
            && chunk->IsMissing()
            && chunk->ExistsInFile())
        {
            if (Storage->LoadAssetChunk(chunk))
                return true;
        }
    }

    return false;
}

#if USE_EDITOR

bool BinaryAsset::SaveAsset(AssetInitData& data, bool silentMode) const
{
    return SaveAsset(GetPath(), data, silentMode);
}

bool BinaryAsset::SaveAsset(const StringView& path, AssetInitData& data, bool silentMode) const
{
    data.Header = _header;
    data.Metadata.Link(Metadata);
    data.Dependencies = Dependencies;
    return SaveToAsset(path, data, silentMode);
}

bool BinaryAsset::SaveToAsset(const StringView& path, AssetInitData& data, bool silentMode)
{
    // Ensure path is in a valid format
    String pathNorm(path);
    ContentStorageManager::FormatPath(pathNorm);
    const StringView filePath = pathNorm;

    // Find target storage container and the asset
    auto storage = ContentStorageManager::TryGetStorage(filePath);
    auto asset = Content::GetAsset(filePath);
    auto binaryAsset = dynamic_cast<BinaryAsset*>(asset);
    if (asset && !binaryAsset)
    {
        LOG(Warning, "Cannot write to the non-binary asset location.");
        return true;
    }
    if (!binaryAsset && !storage && FileSystem::FileExists(filePath))
    {
        // Force-resolve storage (asset at that path could be not yet loaded into registry)
        storage = ContentStorageManager::GetStorage(filePath);
    }

    // Check if can perform write operation to the asset container
    if (storage && !storage->AllowDataModifications())
    {
        LOG(Warning, "Cannot write to the asset storage container.");
        return true;
    }

    // Initialize data container
    ASSERT(data.SerializedVersion > 0);
    if (binaryAsset)
    {
        // Use the same asset ID
        data.Header.ID = binaryAsset->GetID();
    }
    else if (storage && storage->GetEntriesCount())
    {
        // Use the same file ID
        data.Header.ID = storage->GetEntry(0).ID;
    }
    else
    {
        // Randomize ID
        data.Header.ID = Guid::New();
    }

    // Save (set flag to lock reloads on storage modified)
    if (binaryAsset)
        binaryAsset->_isSaving = true;
    bool result;
    if (storage)
    {
        // HACK: file is locked by some tasks (e.g material asset loaded some data and is updating the asset)
        // Let's hide these locks just for the saving
        const auto locks = storage->_chunksLock;
        storage->_chunksLock = 0;
        result = storage->Save(data, silentMode);
        storage->_chunksLock = locks;
    }
    else
    {
        ASSERT(filePath.HasChars());
        result = FlaxStorage::Create(filePath, data, silentMode);
    }
    if (binaryAsset)
        binaryAsset->_isSaving = false;

    if (binaryAsset)
    {
        // Inform dependant asset (use cloned version because it might be modified by assets when they got reloaded)
        auto dependantAssets = binaryAsset->_dependantAssets;
        for (auto& e : dependantAssets)
        {
            e->OnDependencyModified(binaryAsset);
        }
    }

    return result;
}

void BinaryAsset::OnStorageReloaded(FlaxStorage* storage, bool failed)
{
    ASSERT(Storage != nullptr && Storage == storage);

    // Clear header (prevent from using old chunks)
    auto oldHeader = _header;
    Platform::MemoryClear(_header.Chunks, sizeof(_header.Chunks));

    // Check if reload failed
    if (failed)
    {
        LOG(Error, "Asset storage reloading failed. Asset: \'{0}\'.", ToString());
        return;
    }

    // Gather updated asset init data
    AssetInitData initData;
    if (Storage->LoadAssetHeader(GetID(), initData))
    {
        LOG(Error, "Asset header loading failed. Asset: \'{0}\'.", ToString());
        return;
    }
    if (oldHeader.ID != initData.Header.ID || oldHeader.TypeName != initData.Header.TypeName)
    {
        LOG(Warning, "Asset reloading data mismatch. Old ID:{0},TypeName:{1}, New ID:{2},TypeName:{3}. Asset: \'{4}\'.", oldHeader.ID, oldHeader.TypeName, initData.Header.ID, initData.Header.TypeName, GetPath());

        // Unload asset (file contains different asset data)
        // For eg. texture has been changed into sprite atlas on reimport
        Content::UnloadAsset(this);

        // Delete managed object now because it way fail when we recreate the asset object and want to register the new managed object (IDs will overlap)
        DeleteManaged();

        return;
    }

    // Reinitialize (file may modify some data so it needs to be flushed)
    if (Init(initData))
    {
        LOG(Error, "Asset reloading failed. Asset: \'{0}\'.", ToString());
    }

    // Don't reload on save
    if (_isSaving == false)
    {
        Reload();
    }

    // Inform dependant asset (use cloned version because it might be modified by assets when they got reloaded)
    auto dependantAssets = _dependantAssets;
    for (auto& e : dependantAssets)
    {
        e->OnDependencyModified(this);
    }
}

void BinaryAsset::OnDeleteObject()
{
    // Clear dependencies stuff
    ClearDependencies();
    _dependantAssets.Clear();

    Asset::OnDeleteObject();
}

#endif

const String& BinaryAsset::GetPath() const
{
#if USE_EDITOR
    return Storage ? Storage->GetPath() : String::Empty;
#else
    // In build all assets are packed into packages so use ID for original path lookup
    return Content::GetRegistry()->GetEditorAssetPath(_id);
#endif
}

uint64 BinaryAsset::GetMemoryUsage() const
{
    Locker.Lock();
    uint64 result = Asset::GetMemoryUsage();
    result += sizeof(BinaryAsset) - sizeof(Asset);
    result += _dependantAssets.Capacity() * sizeof(BinaryAsset*);
    for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
    {
        auto chunk = _header.Chunks[i];
        if (chunk != nullptr && chunk->IsLoaded())
            result += chunk->Size();
    }
    Locker.Unlock();
    return result;
}

/// <summary>
/// Helper task used to initialize binary asset and upgrade it if need to in background.
/// </summary>
/// <seealso cref="ContentLoadTask" />
class InitAssetTask : public ContentLoadTask
{
private:
    WeakAssetReference<BinaryAsset> _asset;
    FlaxStorage::LockData _dataLock;

public:
    /// <summary>
    /// Initializes a new instance of the <see cref="InitAssetTask"/> class.
    /// </summary>
    /// <param name="asset">The asset.</param>
    InitAssetTask(BinaryAsset* asset)
        : _asset(asset)
        , _dataLock(asset->Storage->Lock())
    {
    }

public:
    // [ContentLoadTask]
    bool HasReference(Object* obj) const override
    {
        return obj == _asset;
    }

protected:
    // [ContentLoadTask]
    Result run() override
    {
        AssetReference<BinaryAsset> ref = _asset.Get();
        if (ref == nullptr)
            return Result::MissingReferences;
        auto storage = ref->Storage;
        auto factory = (BinaryAssetFactoryBase*)Content::GetAssetFactory(ref->GetTypeName());
        ASSERT(factory);

        // Here we should open storage and extract AssetInitData
        // This would also allow to convert/upgrade data
        if (!storage->IsLoaded() && storage->Load())
            return Result::AssetLoadError;
        if (factory->Init(ref.Get()))
            return Result::AssetLoadError;

        return Result::Ok;
    }

    void OnEnd() override
    {
        _dataLock.Release();
        _asset = nullptr;

        ContentLoadTask::OnEnd();
    }
};

ContentLoadTask* BinaryAsset::createLoadingTask()
{
    ContentLoadTask* loadTask = Asset::createLoadingTask();

    // Check if asset need any just to be preloaded
    auto chunksToPreload = getChunksToPreload();
    if (chunksToPreload != 0)
    {
        // Inject loading chunks task
        auto preLoadChunksTask = New<LoadAssetDataTask>(this, chunksToPreload);
        preLoadChunksTask->ContinueWith(loadTask);
        loadTask = preLoadChunksTask;
    }

    // Before asset loading we have to initialize storage
    // TODO: maybe in build game we could do it in place?
    // This step is only for opening asset files in background and upgrading them
    // In build game we have only a few packages which are ready to use
    auto initTask = New<InitAssetTask>(this);
    initTask->ContinueWith(loadTask);
    loadTask = initTask;

    return loadTask;
}

Asset::LoadResult BinaryAsset::loadAsset()
{
    // Ensure that asset has been initialized
    ASSERT(Storage && _header.ID.IsValid() && _header.TypeName.HasChars());

    auto lock = Storage->Lock();
    auto chunksToPreload = getChunksToPreload();
    if (chunksToPreload != 0)
    {
        // Ensure that any chunks that were requested before are loaded in memory (in case streaming flushed them out after timeout)
        for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
        {
            const auto chunk = _header.Chunks[i];
            if (GET_CHUNK_FLAG(i) & chunksToPreload && chunk && chunk->IsMissing())
                Storage->LoadAssetChunk(chunk);
        }
    }
    const LoadResult result = load();
#if !BUILD_RELEASE
    if (result == LoadResult::MissingDataChunk)
    {
        // Provide more insights on potentially missing asset data chunk
        Char chunksBitMask[ASSET_FILE_DATA_CHUNKS + 1];
        Char chunksExistBitMask[ASSET_FILE_DATA_CHUNKS + 1];
        Char chunksLoadBitMask[ASSET_FILE_DATA_CHUNKS + 1];
        for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++)
        {
            if (const FlaxChunk* chunk = _header.Chunks[i])
            {
                chunksBitMask[i] = '1';
                chunksExistBitMask[i] = chunk->ExistsInFile() ? '1' : '0';
                chunksLoadBitMask[i] = chunk->IsLoaded() ? '1' : '0';
            }
            else
            {
                chunksBitMask[i] = chunksExistBitMask[i] = chunksLoadBitMask[i] = '0';
            }
        }
        chunksBitMask[ASSET_FILE_DATA_CHUNKS] = chunksExistBitMask[ASSET_FILE_DATA_CHUNKS] = chunksLoadBitMask[ASSET_FILE_DATA_CHUNKS] = 0;
        LOG(Warning, "Asset reports missing data chunk. Chunks bitmask: {}, existing chunks: {} loaded chunks: {}. '{}'", chunksBitMask, chunksExistBitMask, chunksLoadBitMask, ToString());
    }
#endif
    return result;
}

void BinaryAsset::releaseStorage()
{
#if USE_EDITOR
    // Close file
    if (Storage)
        Storage->CloseFileHandles();
#endif
}

#if USE_EDITOR

void BinaryAsset::onRename(const StringView& newPath)
{
    ScopeLock lock(Locker);

    // We don't support packages now
    ASSERT(!Storage->IsPackage() && Storage->AllowDataModifications() && Storage->GetEntriesCount() == 1);

    // Rename storage
    Storage->OnRename(newPath);
}

#endif
