/******************************************************************************
*	File		-	content_manager.cpp
*	Author		-	Joey Pollack
*	Date		-	2022/02/22 (y/m/d)
*	Mod Date	-	2022/02/22 (y/m/d)
*	Description	-	Keeps track of all resource files in the project.
*                   Reads/Writes meta-data to the contents_meta.xml file.
*                   Also manages the physical location of each asset file.
******************************************************************************/

#include "content_manager.h"
#include <core/common_defs.h>
#include "editor_asset.h"
#include "../project.h"
#include <editor/editor.h>
#include <fstream>

// Asset types
#include "tile_set.h"

namespace lunarium { namespace editor 
{
    ContentManager* ContentManager::mpInstance = nullptr;

    ContentManager::ContentManager()
        : mpProject(nullptr), mNextID(0)
    {

    }

    ContentManager& ContentManager::GetInstance()
    {
        if (!mpInstance)
        {
            mpInstance = new ContentManager();
        }

        return *mpInstance;
    }

    void ContentManager::FreeInstance()
    {
        if (mpInstance)
        {
            delete mpInstance;
            mpInstance = nullptr;
        }
    }

    OpRes ContentManager::Load(Project* project)
    {
        Unload();

        mpProject = project;
        std::filesystem::path root = mpProject->GetRootDirectory();
        mContentFile = root / "contents/content_meta.json";

        // If this is a new project being generated just create a base contents file
        if (!std::filesystem::exists(mContentFile))
        {
            return Save();
        }

        std::ifstream ifs = std::ifstream(mContentFile.string().c_str());
        if (!ifs.is_open())
        {
            return OpRes::Fail("Could not open contents file: %s", mContentFile.string().c_str());
        }

        nlohmann::json j;
        ifs >> j;
        ifs.close();

        auto p = j["Project"];

        if (p.is_null())
        {
            return OpRes::Fail("content_meta.json missing Project element");
        }

        if (p["NextID"].is_null())
        {
            return OpRes::Fail("content_meta.xml missing NextID node");
        }

        mNextID = p["NextID"].get<u64>();

        auto c = p["Contents"];

        if (c.is_null())
        {
            return OpRes::OK();

            // Not actually an error if a project does not have any content yet
            //return OpRes::Fail("content_meta.xml missing Contents node");
        }

        for (auto it = c.begin(); it != c.end(); ++it)
        {
            auto& asset = (*it);
            if (!IsValidAsset(asset))
            {
                return OpRes::Fail("Invalid asset node in content_meta.json");
            }

            // Load Type
            AssetType type = (AssetType)asset["Type"].get<int>();
            EditorAsset* pAsset = CreateAsset(type);
            
            if (!pAsset)
            {
                return OpRes::Fail("Could not create Editor Asset. Unknown type: %n", (int)type);
            }
            pAsset->mAssetDir = mpProject->GetAssetDirectory();

            // Load ID
            pAsset->mID = asset["ID"].get<u64>();

            // Load Location
            pAsset->mLocation = std::filesystem::path(asset["Location"].get<std::string>());

            // Load type specific data
            if (Failed(pAsset->LoadFromJSON(asset).LogIfFailed(Editor::LogCat).LogIfFailed(Editor::LogCat)))
            {
                return OpRes::Fail("Could not load asset type specific data for asset with ID: %llu, File: %s", 
                                    pAsset->GetID(), pAsset->GetFileLocation().filename().string().c_str());
            }

            // Store asset
            auto iter = mAssets.find(pAsset->mID);
            if (iter != mAssets.end())
            {
                return OpRes::Fail("Asset ID collision, First asset: ID: %llu, File: %s, Second asset: ID: %llu, File: %s", 
                                    iter->second->GetID(), iter->second->GetFileLocation().filename().string().c_str(),
                                    pAsset->GetID(), pAsset->GetFileLocation().filename().string().c_str());
            }

            mAssets[pAsset->mID] = pAsset;
        }

        return OpRes::OK();
    }

    OpRes ContentManager::Save()
    {
        if (!mpProject)
        {
            return OpRes::Fail("ConentManager::Save failed: no project set");
        }

        nlohmann::ordered_json oj;
        auto& p = oj["Project"];

        // Save header info
        p["Name"] = mpProject->GetName();
        p["NextID"] = mNextID;

        // Save all assets
        auto& c = p["Contents"];
        for (auto iter = mAssets.begin(); iter != mAssets.end(); iter++)
        {
            EditorAsset* pAsset = iter->second;
            nlohmann::json asset;

            asset["Type"] = (i32)pAsset->GetType();
            asset["ID"] = pAsset->GetID();

            // TODO: This needs to be a relative path! (Relative to the project root)
            asset["Location"] = pAsset->GetFileLocation().string().c_str();

            if (Failed(pAsset->SaveToJSON(asset).LogIfFailed(Editor::LogCat).LogIfFailed(Editor::LogCat)))
            {
                return OpRes::Fail("Could not save asset meta data for file: %s", pAsset->GetFileLocation().string().c_str());
            }

            c.push_back(asset);
        }

        std::ofstream ofs = std::ofstream(mContentFile.string().c_str(), std::ios_base::trunc);
        if (!ofs.is_open())
        {
            return OpRes::Fail("ContentManager could not save file: %s - could not open file", mContentFile.string().c_str());
        }

        ofs << std::setw(4) << oj;
        ofs.close();

        return OpRes::OK();
    }

    void ContentManager::Unload()
    {
        mpProject = nullptr;
        FreeAssets();
        mContentFile = "";
        mNextID = 0;
    }
    
    void ContentManager::GetAllAssetIDs(std::vector<uint64_t>& container) const
    {
        container.clear();
        for (auto iter = mAssets.begin(); iter != mAssets.end(); iter++)
        {
            container.push_back(iter->first);
        }
    }

    void ContentManager::GetAllAssetsByType(std::vector<EditorAsset*>& container, AssetType type) const
    {
        container.clear();
        for (auto iter = mAssets.begin(); iter != mAssets.end(); iter++)
        {
            if (iter->second->GetType() == type)
            {
                container.push_back(iter->second);      
            }
        }
    }

    void ContentManager::GetAllAssetsInDirectory(std::vector<EditorAsset*>& container, std::filesystem::path dir)
    {
        container.clear();
        for (auto iter = mAssets.begin(); iter != mAssets.end(); iter++)
        {
            auto temp = iter->second->GetFileLocation().remove_filename();
            if (temp == dir)
            {
                container.push_back(iter->second);      
            }
        }
    }

    EditorAsset* ContentManager::GetAsset(uint64_t id)
    {   
        auto iter = mAssets.find(id);
        if (iter == mAssets.end())
        {
            return nullptr;
        }

        return iter->second;
    }    

    
    OpRes ContentManager::AddGeneratedAsset(EditorAsset* asset, uint64_t& id)
    {
        if (mAssets.find(mNextID) != mAssets.end())
        {
            return OpRes::Fail("Could not import asset, ID collision. ID: %llu", mNextID);
        }

        asset->mID = mNextID;
        mNextID++; 
        mAssets[asset->mID] = asset;
        id = asset->mID;

        return OpRes::OK();        
    }

    OpRes ContentManager::ImportFile(std::filesystem::path file, std::filesystem::path to_location, AssetType type, uint64_t& id)
    {
        if (mAssets.find(mNextID) != mAssets.end())
        {
            return OpRes::Fail("Could not import asset, ID collision. ID: %llu", mNextID);
        }

        if (std::filesystem::exists(to_location))
        {
            std::filesystem::remove(to_location);
        }

        if (!std::filesystem::copy_file(file, to_location))
        {
            return OpRes::Fail("Could not copy asset file from: %s to: %s", file.string().c_str(), to_location.string().c_str());
        }

        

        EditorAsset* pAsset = CreateAsset(type);
        pAsset->mAssetDir = mpProject->GetAssetDirectory();
        pAsset->mID = mNextID;
        mNextID++;
        pAsset->mLocation = mpProject->MakeRelativeToAssets(to_location);
        if (Failed(pAsset->LoadRawFile().LogIfFailed(Editor::LogCat)))
        {
            delete pAsset;
            return OpRes::Fail("Could not load asset data from raw file: %s", file.string().c_str());
        }

        mAssets[pAsset->mID] = pAsset;
        id = pAsset->mID;

        return OpRes::OK();        
    }

    void ContentManager::RemoveAsset(uint64_t asset_id)
    {
        auto iter = mAssets.find(asset_id);
        if (iter == mAssets.end())
        {
            return;
        }

        delete iter->second;
        mAssets.erase(iter);
    }

    bool ContentManager::IsValidAsset(nlohmann::json& node)
    {
        if (node.is_null()) { return false; }
        if (node["ID"].is_null()) { return false; }
        if (!node["ID"].is_number_unsigned()) { return false; }
        if (node["Type"].is_null()) { return false; }
        if (!node["Type"].is_number()) { return false; }
        if (node["Location"].is_null()) { return false; }

        return true;
    }

    EditorAsset* ContentManager::CreateAsset(AssetType type)
    {
        switch (type)
        {
            case AssetType::EATYPE_TILE_SET: return new TileSet();

            default: return nullptr;
        }
    }
    
    void ContentManager::FreeAssets()
    {
        for (auto iter = mAssets.begin(); iter != mAssets.end(); iter++)
        {
            delete iter->second;
        }

        mAssets.clear();
    }
}}