#pragma once

#include "engine/framework.h"
#include "rapidjson/document.h"
#include "rapidjson/filereadstream.h"
#include "entities/song.h"

/**
 * For when you want multiple of the same manager.
 */
template <typename T>
class MultiManager : public Manager
{
public:
    std::unordered_map<std::string, std::unique_ptr<T>> managers;

    MultiManager() = default;

    /**
     * Initialize all managers.
     */
    void init() override
    {
        for (auto& manager : managers)
        {
            manager.second->init_manager();
        }
        Manager::init();
    }

    /**
     * Add a manager to the MultiManager.
     *
     * @param name The name to give the manager.
     * @param manager The manager to add.
     */
    void add_manager(std::string name, std::unique_ptr<T> manager)
    {
        static_assert(std::is_base_of<Manager, T>::value, "T must derive from Manager");
        managers[name] = std::move(manager);
    }

    /**
     * Create a manager and add it to the MultiManager.
     *
     * @param name The name to give the manager.
     * @param args The arguments to forward to the manager constructor.
     * @return A pointer to the added manager.
     */
    template <typename... TArgs>
    T* add_manager(std::string name, TArgs&&... args)
    {
        static_assert(std::is_base_of<Manager, T>::value, "T must derive from Manager");
        auto new_manager = std::make_unique<T>(std::forward<TArgs>(args)...);
        T* manager_ptr = new_manager.get();
        add_manager(name, std::move(new_manager));
        return manager_ptr;
    }

    /**
     * Get a manager by name.
     *
     * @param name The name of the manager.
     * @return A pointer to the manager.
     */
    T* get_manager(std::string name)
    {
        return managers[name].get();
    }
};

/**
 * Manager for handling fonts so they are not loaded multiple times.
 */
class FontManager : public Manager
{
public:
    std::unordered_map<std::string, Font> fonts;

    /**
     * Constructor for FontManager.
     * Loads the default font.
     */
    FontManager()
    {
        fonts["default"] = GetFontDefault();
    }

    ~FontManager()
    {
        for (auto& pair : fonts)
        {
            UnloadFont(pair.second);
        }
    }

    /**
     * Load a font from a file.
     *
     * @param name The name to associate with the font.
     * @param filename The filename of the font to load.
     * @param size The font size to save the font texture as.
     *
     * @return A reference to the loaded font.
     */
    Font& load_font(const std::string& name, const std::string& filename, int size = 32)
    {
        if (fonts.find(name) != fonts.end())
        {
            return fonts[name];
        }

        Font font = LoadFontEx(filename.c_str(), size, nullptr, 0);
        fonts[name] = font;
        return fonts[name];
    }

    /**
     * Get a font by name.
     *
     * @param name The name of the font.
     * @return A reference to the font.
     */
    Font& get_font(const std::string& name)
    {
        return fonts[name];
    }

    /**
     * Set the texture filter for a font.
     *
     * @param name The name of the font.
     * @param filter The filter to set.
     */
    void set_texture_filter(const std::string& name, int filter)
    {
        if (fonts.find(name) != fonts.end())
        {
            SetTextureFilter(fonts[name].texture, filter);
        }
    }
};

/**
 * Manager for handling the application window.
 */
class WindowManager : public Manager
{
public:
    int width = 1280;
    int height = 720;
    std::string title = "My Game";
    int target_fps = 60;

    WindowManager(int w, int h, const std::string& t, int fps = 60) : width(w), height(h), title(t), target_fps(fps) {}

    /**
     * Initialize the window.
     */
    void init() override
    {
        SetConfigFlags(FLAG_WINDOW_RESIZABLE);
        InitWindow(width, height, title.c_str());
        InitAudioDevice();
        SetTargetFPS(target_fps);

        char* mappings = LoadFileText("assets/gamecontrollerdb.txt");
        SetGamepadMappings(mappings);
        Manager::init();
    }

    /**
     * Set the window title.
     *
     * @param t The new title.
     */
    void set_title(const std::string& t)
    {
        title = t;
        SetWindowTitle(title.c_str());
    }

    /**
     * Get the window width.
     *
     * @return The window width.
     */
    float get_width() const
    {
        return static_cast<float>(width);
    }

    /**
     * Get the window height.
     */
    float get_height() const
    {
        return static_cast<float>(height);
    }

    /**
     * Get the window size.
     *
     * @return The window size as a Vector2.
     */
    Vector2 get_size() const
    {
        return Vector2{(float)width, (float)height};
    }

    /**
     * Get the window aspect ratio.
     *
     * @return The window aspect ratio.
     */
    float get_aspect_ratio() const
    {
        return static_cast<float>(width) / static_cast<float>(height);
    }
};

/**
 * Manager for handling JSON files using RapidJSON.
 */
class JsonManager : public Manager
{
public:
    std::unordered_map<std::string, rapidjson::Document> documents;

    JsonManager() = default;

    ~JsonManager()
    {
        // Documents clean up automatically
    }

    /**
     * Load a JSON file and parse it into a RapidJSON document.
     *
     * @param name The name to associate with the document.
     * @param filename The filename of the JSON file to load.
     * @return A reference to the loaded document.
     */
    rapidjson::Document& load_json(const std::string& name, const std::string& filename)
    {
        printf("load_json: %s\n", filename.c_str());
        // If already loaded, return existing document
        if (documents.find(name) != documents.end())
        {
            return documents[name];
        }

        // Open the file
        FILE* fp = fopen(filename.c_str(), "rb");
        if (!fp)
        {
            fprintf(stderr, "Error: Could not open file %s\n", filename.c_str());
            // Return an empty document on error
            documents[name] = rapidjson::Document();
            return documents[name];
        }

        // Create a buffer and stream
        char readBuffer[65536];
        rapidjson::FileReadStream is(fp, readBuffer, sizeof(readBuffer));

        // Parse the stream into a document
        rapidjson::Document doc;
        doc.ParseStream(is);
        fclose(fp);

        // Check for parse errors
        if (doc.HasParseError())
        {
            fprintf(stderr, "JSON parse error in %s: %d\n", filename.c_str(), doc.GetParseError());
        }

        // Move document into map
        documents[name] = std::move(doc);
        return documents[name];
    }

    /**
     * Get a JSON document by name.
     *
     * @param name The name of the document.
     * @return A reference to the document.
     */
    rapidjson::Document& get_document(const std::string& name)
    {
        return documents[name];
    }

    /**
     * Check if a document exists.
     *
     * @param name The name of the document.
     * @return True if the document exists, false otherwise.
     */
    bool has_document(const std::string& name) const
    {
        return documents.find(name) != documents.end();
    }
};

/**
 * Manager for handling Song objects parsed from JSON files.
 */
class SongManager : public Manager
{
public:
    std::unordered_map<std::string, Song> songs;
    JsonManager* json_manager;

    SongManager() = default;

    SongManager(JsonManager* json_mgr) : json_manager(json_mgr) {}

    /**
     * Load a song from a JSON file.
     *
     * @param name The name to associate with the song.
     * @param filename The filename of the JSON file to load.
     * @return A reference to the loaded song.
     */
    Song& load_song(const std::string& name, const std::string& filename)
    {
        printf("load_song %s\n", name.c_str());
        // If already loaded, return existing song
        if (songs.find(name) != songs.end())
        {
            return songs[name];
        }

        // Load and parse the JSON file
        rapidjson::Document& doc = json_manager->load_json(name, filename);
        
        // Parse the document into a Song struct
        Song song = parseSong(doc);
        
        // Store the song
        songs[name] = song;

        return songs[name];
    }

    /**
     * Get a song by name.
     *
     * @param name The name of the song.
     * @return A reference to the song.
     */
    Song& get_song(const std::string& name)
    {
        return songs[name];
    }

    /**
     * Check if a song exists.
     *
     * @param name The name of the song.
     * @return True if the song exists, false otherwise.
     */
    bool has_song(const std::string& name) const
    {
        return songs.find(name) != songs.end();
    }

    /**
     * Get the number of tracks in a song.
     *
     * @param name The name of the song.
     * @return The number of tracks.
     */
    size_t get_track_count(const std::string& name) const
    {
        auto it = songs.find(name);
        if (it != songs.end())
        {
            return it->second.tracks.size();
        }
        return 0;
    }
};