//
// Created by sebastian on 30.07.26.
//

#include "ModelUploader.h"

#include <array>
#include <filesystem>
#include <iostream>

#include "Material.h"
#include "glm/vec3.hpp"
#include "loader/textures/TextureUploader.h"
#include "spdlog/spdlog.h"
#include "AABB.h"

std::vector<engine::LoadedAsset> engine::ModelUploader::upload(const engine::RawModelBundle &bundle, engine::ModelSplitPolicy policy) {
    std::vector<engine::LoadedAsset> results;

    auto uploadedTextures = uploadTextures(bundle.textures, results);
    auto materials = buildMaterials(bundle.materials, uploadedTextures);

    switch (policy) {
        case ModelSplitPolicy::ByObject:
            for (const auto& object : bundle.model.objects) {
                auto model = std::make_shared<Model>(uploadSections(object.subModels, materials));
                results.push_back(LoadedModel{bundle.model.name + "." + object.name, model});
            }
            break;
        case ModelSplitPolicy::Combined:
            results.push_back(LoadedModel{
                bundle.model.name,
                std::make_shared<Model>(buildCombinedByObject(bundle.model.objects, materials))
            });
            break;
        case ModelSplitPolicy::ByMaterial:
            results.push_back(LoadedModel{
            bundle.model.name,
                std::make_shared<Model>(buildCombinedByMaterial(bundle.model.objects, materials))
            });
            break;
        default:
            throw std::runtime_error("Invalid ModelSplitPolicy");
    }
    return results;
}

std::unordered_map<std::string, std::shared_ptr<engine::Texture2D>> engine::ModelUploader::uploadTextures(
    const std::vector<RawTextureData> &textures, std::vector<LoadedAsset> &results) {

    std::unordered_map<std::string, std::shared_ptr<engine::Texture2D>> uploadedTextures;
    for (const auto& rawTexture : textures) {
        auto texture = std::make_shared<engine::Texture2D>(TextureUploader::upload(rawTexture));
        uploadedTextures[rawTexture.name] = texture;
        results.emplace_back(LoadedTexture{rawTexture.name, texture});
    }
    return uploadedTextures;
}

engine::ModelUploader::MaterialMap engine::ModelUploader::buildMaterials(const std::vector<RawMaterialData> &materials, const std::unordered_map<std::string, std::shared_ptr<Texture2D> > &textures) {
    MaterialMap result;

    for (const auto& raw : materials) {
        std::array<std::shared_ptr<Texture2D>, static_cast<size_t>(TextureType::Count)> resolved;
        for (const auto& [type, texFileName] : raw.texturePaths) {
           auto texPath = std::filesystem::path(texFileName);
           const auto& texName = texPath.replace_extension();
            if (auto it = textures.find(texName); it != textures.end()) {
                resolved[static_cast<size_t>(type)] = it->second;
            } else {
                std::cerr << texName.string() << texName.string().size() << std::endl;
                spdlog::warn("Textur '{}' für Material '{}' nicht gefunden", texFileName, raw.name);
            }
        }
        result[raw.name] = std::make_shared<Material>(resolved, raw.shininess, raw.opacity);
    }

    result[""] = std::make_shared<Material>(); // Fallback für Faces ohne usemtl

    return result;
}

engine::Model engine::ModelUploader::uploadSections(const std::vector<RawMeshSection> &sections, const MaterialMap &materials) {
    std::vector<float> vertices, normals, texCoords;
    std::vector<unsigned int> indices;
    std::vector<MeshSection> meshSections;

    unsigned int vertexOffset = 0;
    for (const auto& section : sections) {
        MeshSection meshSection;
        meshSection.name        = section.name.empty() ? section.materialName : section.name;
        meshSection.indexOffset = static_cast<unsigned int>(indices.size());
        meshSection.indexCount  = static_cast<unsigned int>(section.indices.size());

        auto it = materials.find(section.materialName);
        meshSection.material = (it != materials.end()) ? it->second : materials.at("");

        vertices.insert(vertices.end(), section.vertices.begin(), section.vertices.end());
        normals.insert(normals.end(), section.normals.begin(), section.normals.end());
        texCoords.insert(texCoords.end(), section.textureCoords.begin(), section.textureCoords.end());
        for (int localIndex : section.indices) indices.push_back(vertexOffset + static_cast<unsigned int>(localIndex));

        glm::vec3 centroidSum(0.0f);
        for (size_t i = 0; i < section.vertices.size(); i += 3) {
            centroidSum += glm::vec3(section.vertices[i], section.vertices[i+1], section.vertices[i+2]);
        }
        meshSection.centroid = centroidSum / static_cast<float>(section.vertices.size() / 3);

        //Calculate AABB
        AABB aabb(
        glm::vec3(std::numeric_limits<float>::max(), std::numeric_limits<float>::max(), std::numeric_limits<float>::max()),
        glm::vec3(std::numeric_limits<float>::lowest(), std::numeric_limits<float>::lowest(), std::numeric_limits<float>::lowest())
        );
        for (size_t i = 0; i < section.vertices.size(); i += 3) {
            aabb.expandToInclude(glm::vec3(section.vertices[i], section.vertices[i+1], section.vertices[i+2]));
        }

        vertexOffset += static_cast<unsigned int>(section.vertices.size() / 3);
        meshSections.push_back(std::move(meshSection));
    }

    auto vao = Vao::create();
    vao->bind();


    vao->initDataFeed(vertices.data(),  vertices.size()  * sizeof(float), GL_STATIC_DRAW, /* Attribute für Location 0, 3 Komponenten */ makeAttributeList(std::make_unique<Vec3Attribute>(0)));
    vao->initDataFeed(normals.data(),   normals.size()   * sizeof(float), GL_STATIC_DRAW, /* Attribute für Location 1, 3 Komponenten */ makeAttributeList(std::make_unique<Vec3Attribute>(1)));
    vao->initDataFeed(texCoords.data(), texCoords.size() * sizeof(float), GL_STATIC_DRAW, /* Attribute für Location 2, 2 Komponenten */ makeAttributeList(std::make_unique<Vec2Attribute>(2)));
    vao->createIndexBuffer(indices.data(), indices.size());

    vao->unbind();

    AABB modelAABB = AABB(
        glm::vec3(std::numeric_limits<float>::max(), std::numeric_limits<float>::max(), std::numeric_limits<float>::max()),
        glm::vec3(std::numeric_limits<float>::lowest(), std::numeric_limits<float>::lowest(), std::numeric_limits<float>::lowest())
    );
    for (int i = 0; i < static_cast<int>(vertices.size()); i+=3) {
        modelAABB.expandToInclude(glm::vec3(vertices[i], vertices[i+1], vertices[i+2]));
    }

    return Model(std::move(vao), static_cast<int>(indices.size()), std::move(meshSections), modelAABB);
}

engine::Model engine::ModelUploader::buildCombinedByObject(const std::vector<RawObjectData> &objects, const MaterialMap &materials) {
    std::vector<RawMeshSection> all;
    for (const auto& object : objects) {
        for (auto section : object.subModels) {
            all.push_back(std::move(section));
        }
    }
    return uploadSections(all, materials);
}

engine::Model engine::ModelUploader::buildCombinedByMaterial(const std::vector<RawObjectData> &objects, const MaterialMap &materials) {
    std::unordered_map<std::string, RawMeshSection> merged;

    for (const auto& object : objects) {
        for (auto section : object.subModels) {
            auto& target = merged[section.materialName];

            target.materialName = target.name = section.materialName;
            unsigned int offset = static_cast<unsigned int>(target.vertices.size() / 3);
            target.vertices.insert(target.vertices.end(), section.vertices.begin(), section.vertices.end());
            target.normals.insert(target.normals.end(), section.normals.begin(), section.normals.end());
            target.textureCoords.insert(target.textureCoords.end(), section.textureCoords.begin(), section.textureCoords.end());
            for (int idx : section.indices) target.indices.push_back(static_cast<int>(offset) + idx);
        }
    }

    std::vector<RawMeshSection> all;
    for (auto& [name, section] : merged) all.push_back(std::move(section));
    return uploadSections(all, materials);
}

std::vector<std::unique_ptr<engine::Attribute>> engine::ModelUploader::makeAttributeList(std::unique_ptr<Attribute> attr) {
    std::vector<std::unique_ptr<engine::Attribute>> result;
    result.push_back(std::move(attr));
    return result;
};
