#include "ShaderLibrary.h"

#include <utility>
#include "iimage.h"
#include "itextstream.h"
#include "ShaderTemplate.h"

namespace shaders 
{

std::shared_ptr<ShaderTemplate> ShaderLibrary::getTemplate(const std::string& name)
{
    auto decl = std::static_pointer_cast<ShaderTemplate>(
        GlobalDeclarationManager().findDeclaration(decl::Type::Material, name));

    if (decl)
    {
        return decl;
    }

    // The shader definition hasn't been found, let's check if the name
    // refers to a file in the VFS
	if (auto img = GlobalImageLoader().imageFromVFS(name); img)
	{
        // Insert a new declaration, creating a diffusemap with that image
        decl = std::static_pointer_cast<ShaderTemplate>(
            GlobalDeclarationManager().findOrCreateDeclaration(decl::Type::Material, name));

        auto imgExpr = std::make_shared<ImageExpression>(name);
        decl->addLayer(IShaderLayer::DIFFUSE, imgExpr);

        decl->setFileInfo(vfs::FileInfo("materials/", "_autogenerated_by_darkradiant_.mtr", vfs::Visibility::HIDDEN));

        return decl;
    }

    rWarning() << "ShaderLibrary: definition not found: " << name << std::endl;

    // Insert a new declaration, creating a diffusemap with that image
    decl = std::static_pointer_cast<ShaderTemplate>(
        GlobalDeclarationManager().findOrCreateDeclaration(decl::Type::Material, name));

    decl->setDescription("This material is missing and has been auto-generated by DarkRadiant");

    // Make the definition VFS-visible to let them show in MediaBrowser (#5475)
    decl->setFileInfo(vfs::FileInfo("materials/", "_autogenerated_by_darkradiant_.mtr", vfs::Visibility::NORMAL));

    return decl;
}

bool ShaderLibrary::definitionExists(const std::string& name) const
{
    return GlobalDeclarationManager().findDeclaration(decl::Type::Material, name) != nullptr;
}

void ShaderLibrary::copyDefinition(const std::string& nameOfOriginal, const std::string& nameOfCopy)
{
    // These need to be checked by the caller
    assert(definitionExists(nameOfOriginal));
    assert(!definitionExists(nameOfCopy));

    auto originalDecl = GlobalDeclarationManager().findDeclaration(decl::Type::Material, nameOfOriginal);
    auto decl = GlobalDeclarationManager().findOrCreateDeclaration(decl::Type::Material, nameOfCopy);

    // Replace the syntax block of the target with the one of the original
    auto syntax = originalDecl->getBlockSyntax();
    syntax.name = nameOfCopy;
    syntax.fileInfo = vfs::FileInfo{ "", "", vfs::Visibility::HIDDEN };

    decl->setBlockSyntax(syntax);
}

bool ShaderLibrary::renameDefinition(const std::string& oldName, const std::string& newName)
{
    auto result = GlobalDeclarationManager().renameDeclaration(decl::Type::Material, oldName, newName);

    // Rename in shaders table (if existing)
    if (result && _shaders.count(oldName) > 0)
    {
        auto extractedShader = _shaders.extract(oldName);
        extractedShader.key() = newName;

        // Insert it under the new name before setting the CShader instance's name
        // the observing OpenGLShader instance will request the material to reconstruct itself
        // If the new name is not present at that point, the library will create a default material.
        auto insertedShader = _shaders.insert(std::move(extractedShader));

        // Rename the CShader instance
        insertedShader.position->second->setName(newName);
    }

    return result;
}

void ShaderLibrary::removeDefinition(const std::string& name)
{
    assert(definitionExists(name));

    GlobalDeclarationManager().removeDeclaration(decl::Type::Material, name);
    _shaders.erase(name);
}

CShaderPtr ShaderLibrary::findShader(const std::string& name)
{
    // Try to lookup the shader in the active shaders list
    auto existing = _shaders.find(name);

    if (existing != _shaders.end())
    {
        // A shader has been found, return its pointer
        return existing->second;
    }

    // No shader has been found, retrieve its declaration (may also be a dummy one)
    auto decl = getTemplate(name);

    // Construct a new shader object with this def and insert it into the map
    return _shaders.emplace(name, std::make_shared<CShader>(name, decl)).first->second;
}

void ShaderLibrary::clear()
{
	_shaders.clear();
}

void ShaderLibrary::foreachShaderName(const ShaderNameCallback& callback)
{
    GlobalDeclarationManager().foreachDeclaration(decl::Type::Material, [&](const decl::IDeclaration::Ptr& decl)
    {
        if (decl->getBlockSyntax().fileInfo.visibility == vfs::Visibility::NORMAL)
        {
            callback(decl->getDeclName());
        }
    });
}

void ShaderLibrary::foreachShader(const std::function<void(const CShaderPtr&)>& func)
{
	for (const ShaderMap::value_type& pair : _shaders)
	{
        func(pair.second);
	}
}

} // namespace shaders
