#ifndef MATERIAL_HPP_
#define MATERIAL_HPP_

#include <Utilities/Extern.hpp>
#include <Vk/Buffer/Buffer.hpp>
#include <Vk/Image/Image.hpp>
#include <Vk/ImageView/ImageView.hpp>
#include <Vk/CommandBuffer/CommandBuffer.hpp>
#include <Vk/DescriptorPool/DescriptorPool.hpp>
#include <Vk/DescriptorSet/DescriptorSet.hpp>

/**
 * @brief Represents a material.
 * 
 * A material is a combination of an image with its normal map etc.
 */
class Material
{
private:
    struct Component
    {
        std::shared_ptr<Vk::Buffer> stagingBuffer;
        std::shared_ptr<Vk::Image> image;
        std::shared_ptr<Vk::ImageView> imageView;
    };

    static std::unordered_map<std::string, Material*> materialTable;

    const char *name;
    int widthValue;
    int heightValue;
    std::vector<Component> components;
    std::shared_ptr<Vk::DescriptorPool> descriptorPool;
    std::shared_ptr<Vk::DescriptorSet> descriptorSetPriv;
    std::shared_ptr<Vk::Buffer> metadataStagingBuffer;
    std::shared_ptr<Vk::Buffer> metadataBuffer;
    unsigned char *pixels = nullptr;
    unsigned char lightPixel[4];

    void addComponent(std::shared_ptr<Vk::Device> device, Vk::CommandBuffer &cmdbuf, const void *pixels);
    void addComponent(std::shared_ptr<Vk::Device> device, Vk::CommandBuffer &cmdbuf, const void *pixels, int width, int height);
    void buildMetadata(std::shared_ptr<Vk::Device> device, Vk::CommandBuffer &cmdbuf);
    void buildDescriptorSet(std::shared_ptr<Vk::Device> device);
    void finalize();
    
public:
    /**
     * @brief Material metadata passed to the GPU.
     */
    struct Metadata
    {
        glm::ivec2 size;
        int flags;        // GLSL_MAT_*
    };

    int flags = 0;

    /**
     * @brief Construct a material.
     * 
     * This simply stores the name internally, it does not actually perform the loading.
     * The `load()` method must later be called by the preloader to actually create the
     * Vulkan objects.
     * 
     * In most cases, rather than directly invoking this constructor, you should add an
     * invocation to the `DECL_MATERIAL()` macro to the list in `MaterialList.hpp`.
     * 
     * @param name The material name.
     */
    Material(const char *name = nullptr, unsigned char lightLevel = 0x00);

    /**
     * @brief Perform the loading of this material.
     */
    void load(std::atomic<unsigned int> &tasksDone);

    /**
     * @brief Load this material from a specific path.
     */
    void load(const std::string &imagePath, std::atomic<unsigned int> &tasksDone);

    /**
     * @brief Get the width of this material.
     * 
     * @return The width.
     */
    int width();

    /**
     * @brief Get the height of this material.
     * 
     * @return The height.
     */
    int height();

    /**
     * @brief Returns the pixels of this material, if available.
     */
    unsigned char* getPixels();

    /**
     * @brief Returns a reference to the descriptor set.
     */
    Vk::DescriptorSet& descriptorSet();

    /**
     * @brief Return a material given its name, or `nullptr` if not recognised.
     */
    static Material* byName(const std::string &name);

    /**
     * @brief The internal game icon image.
     * 
     * This is loaded in the constructor of `GraphicsContext` from `assets/logo.png`
     * and is available even to the preloader. It is a 64x64 Voxelverse logo.
     */
    static Material internalGameIcon;

    /**
     * @brief The white material.
     * 
     * This is loaded in the constructor of `GraphicsContext`, so that is it available even
     * to the preloader.
     */
    static Material white;

    #define DECL_MATERIAL(name) static Material name
    #include <Material/MaterialList.hpp>

    friend class GraphicsContext;
    friend class BlockAtlas;
    friend class Model;
    friend class VoxelSpriteRenderer;
    friend class BlockRegistry;
    friend class FluidRegistry;
};

#endif