#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <SDL_video.h>
#else
#include <SDL2/SDL_video.h>
#endif

#include "rt64_render_hooks.h"

#include "concurrentqueue.h"

#include "RmlUi/Core.h"
#include "RmlUi/Debugger.h"
#include "RmlUi/Core/RenderInterfaceCompatibility.h"
#include "RmlUi/../../Source/Core/Elements/ElementLabel.h"
#include "RmlUi_Platform_SDL.h"

#include "recomp_ui.h"
#include "recomp_input.h"
#include "librecomp/game.hpp"
#include "zelda_config.h"
#include "ui_rml_hacks.hpp"
#include "ui_elements.h"
#include "ui_mod_menu.h"
#include "ui_renderer.h"

bool can_focus(Rml::Element* element) {
    return element->GetOwnerDocument() != nullptr && element->GetProperty(Rml::PropertyId::TabIndex)->Get<Rml::Style::TabIndex>() != Rml::Style::TabIndex::None;
}

//! Copied from lib\RmlUi\Source\Core\Elements\ElementLabel.cpp
// Get the first descending element whose tag name matches one of tags.
static Rml::Element* TagMatchRecursive(const Rml::StringList& tags, Rml::Element* element)
{
	const int num_children = element->GetNumChildren();

	for (int i = 0; i < num_children; i++)
	{
		Rml::Element* child = element->GetChild(i);

		for (const Rml::String& tag : tags)
		{
			if (child->GetTagName() == tag)
				return child;
		}

		Rml::Element* matching_element = TagMatchRecursive(tags, child);
		if (matching_element)
			return matching_element;
	}

	return nullptr;
}

Rml::Element* get_target(Rml::ElementDocument* document, Rml::Element* element) {
    // Labels can have targets, so check if this element is a label.
    if (element->GetTagName() == "label") {
        Rml::ElementLabel* labelElement = (Rml::ElementLabel*)element;
        const Rml::String target_id = labelElement->GetAttribute<Rml::String>("for", "");

        if (target_id.empty())
        {
            const Rml::StringList matching_tags = {"button", "input", "textarea", "progress", "progressbar", "select"};

            return TagMatchRecursive(matching_tags, element);
        }
        else
        {
            Rml::Element* target = labelElement->GetElementById(target_id);
            if (target != element)
                return target;
        }

        return nullptr;
    }
    // Return the element directly if no target exists.
    return element;
}

namespace recompui {
    class UiEventListener : public Rml::EventListener {
        event_handler_t* handler_;
        Rml::String param_;
    public:
        UiEventListener(event_handler_t* handler, Rml::String&& param) : handler_(handler), param_(std::move(param)) {}
        void ProcessEvent(Rml::Event& event) override {
            handler_(param_, event);
        }
    };

    class UiEventListenerInstancer : public Rml::EventListenerInstancer {
        std::unordered_map<Rml::String, event_handler_t*> handler_map_;
        std::unordered_map<Rml::String, UiEventListener> listener_map_;
    public:
        Rml::EventListener* InstanceEventListener(const Rml::String& value, Rml::Element* element) override {
            // Check if a listener has already been made for the full event string and return it if so.
            auto find_listener_it = listener_map_.find(value);
            if (find_listener_it != listener_map_.end()) {
                return &find_listener_it->second;
            }

            // No existing listener, so check if a handler has been registered for this event type and create a listener for it if so.
            size_t delimiter_pos = value.find(':');
            Rml::String event_type = value.substr(0, delimiter_pos);
            auto find_handler_it = handler_map_.find(event_type);
            if (find_handler_it != handler_map_.end()) {
                // A handler was found, create a listener and return it.
                Rml::String event_param = value.substr(std::min(delimiter_pos, value.size()));
                return &listener_map_.emplace(value, UiEventListener{ find_handler_it->second, std::move(event_param) }).first->second;
            }

            return nullptr;
        }

        void register_event(const Rml::String& value, event_handler_t* handler) {
            handler_map_.emplace(value, handler);
        }
    };
}

void recompui::register_event(UiEventListenerInstancer& listener, const std::string& name, event_handler_t* handler) {
    listener.register_event(name, handler);
}

Rml::Element* find_autofocus_element(Rml::Element* start) {
    Rml::Element* cur_element = start;
    Rml::Element* first_found = nullptr;

    while (cur_element) {
        if (cur_element->HasAttribute("autofocus")) {
            break;
        }
        cur_element = RecompRml::FindNextTabElement(cur_element, true);
        // Track the first element that was found to know when we've wrapped around.
        if (!first_found) {
            first_found = cur_element;
        }
        // Stop searching if we found the first element again.
        else {
            if (cur_element == first_found) {
                // Return the first tab element as there was nothing marked with autofocus.
                return first_found;
            }
        }
    }

    return cur_element;
}

struct ContextDetails {
    recompui::ContextId context;
    Rml::ElementDocument* document;
    bool takes_input;
};

class UIState {
    Rml::Element* prev_focused = nullptr;
    bool mouse_is_active_changed = false;
    std::unique_ptr<recompui::MenuController> launcher_menu_controller{};
    std::unique_ptr<recompui::MenuController> config_menu_controller{};
    std::vector<ContextDetails> opened_contexts{};
public:
    bool mouse_is_active_initialized = false;
    bool mouse_is_active = false;
    bool cont_is_active = false;
    bool await_stick_return_x = false;
    bool await_stick_return_y = false;
    int last_active_mouse_position[2] = {0, 0};
    std::unique_ptr<recompui::MenuController> config_controller;
    std::unique_ptr<recompui::MenuController> launcher_controller;
    std::unique_ptr<SystemInterface_SDL> system_interface;
    recompui::RmlRenderInterface_RT64 render_interface;
    Rml::Context* context;
    recompui::UiEventListenerInstancer event_listener_instancer;

    UIState(const UIState& rhs) = delete;
    UIState& operator=(const UIState& rhs) = delete;
    UIState(UIState&& rhs) = delete;
    UIState& operator=(UIState&& rhs) = delete;

    UIState(SDL_Window* window, RT64::RenderInterface* interface, RT64::RenderDevice* device) {
        launcher_menu_controller = recompui::create_launcher_menu();
        config_menu_controller = recompui::create_config_menu();

        system_interface = std::make_unique<SystemInterface_SDL>();
        system_interface->SetWindow(window);
        render_interface.init(interface, device);

        launcher_menu_controller->register_events(event_listener_instancer);
        config_menu_controller->register_events(event_listener_instancer);

        Rml::SetSystemInterface(system_interface.get());
        Rml::SetRenderInterface(render_interface.get_rml_interface());
        Rml::Factory::RegisterEventListenerInstancer(&event_listener_instancer);

        recompui::register_custom_elements();

        Rml::Initialise();
        
        // Apply the hack to replace RmlUi's default color parser with one that conforms to HTML5 alpha parsing for SASS compatibility
        recompui::apply_color_hack();

        int width, height;
        SDL_GetWindowSizeInPixels(window, &width, &height);
        
        context = Rml::CreateContext("main", Rml::Vector2i(width, height));
        launcher_menu_controller->make_bindings(context);
        config_menu_controller->make_bindings(context);

        Rml::Debugger::Initialise(context);
        {
            const Rml::String directory = "assets/";

            struct FontFace {
                const char* filename;
                bool fallback_face;
            };
            FontFace font_faces[] = {
                {"LatoLatin-Regular.ttf", false},
                {"ChiaroNormal.otf", false},
                {"ChiaroBold.otf", false},
                {"LatoLatin-Italic.ttf", false},
                {"LatoLatin-Bold.ttf", false},
                {"LatoLatin-BoldItalic.ttf", false},
                {"NotoEmoji-Regular.ttf", true},
                {"promptfont/promptfont.ttf", false},
            };

            for (const FontFace& face : font_faces) {
                Rml::LoadFontFace(directory + face.filename, face.fallback_face);
            }
        }
    }

    void load_documents() {
        launcher_menu_controller->load_document(context);
        config_menu_controller->load_document(context);
    }

    void unload() {
        render_interface.reset();
    }

    void update_primary_input(bool mouse_moved, bool non_mouse_interacted) {
        mouse_is_active_changed = false;
        if (non_mouse_interacted) {
            // controller newly interacted with
            if (mouse_is_active) {
                mouse_is_active = false;
                mouse_is_active_changed = true;
            }
        }
        else if (mouse_moved) {
            // mouse newly interacted with
            if (!mouse_is_active) {
                mouse_is_active = true;
                mouse_is_active_changed = true;
            }
        }

        if (mouse_moved || non_mouse_interacted) {
            mouse_is_active_initialized = true;
        }

        if (mouse_is_active_initialized) {
            recompui::set_cursor_visible(mouse_is_active);
        }

        Rml::ElementDocument* current_document = top_input_document();
        if (current_document == nullptr) {
            return;
        }

        // TODO is this needed?
        Rml::Element* window_el = current_document->GetElementById("window");
        if (window_el != nullptr) {
            if (mouse_is_active) {
                if (!window_el->HasAttribute("mouse-active")) {
                    window_el->SetAttribute("mouse-active", true);
                }
            }
            else if (window_el->HasAttribute("mouse-active")) {
                window_el->RemoveAttribute("mouse-active");
            }
        }
    }

    void update_focus(bool mouse_moved, bool non_mouse_interacted) {
        Rml::ElementDocument* current_document = top_input_document();

        if (current_document == nullptr) {
            return;
        }

        if (cont_is_active || non_mouse_interacted) {
            if (non_mouse_interacted) {
                auto focusedEl = current_document->GetFocusLeafNode();
                if (focusedEl == nullptr || RecompRml::CanFocusElement(focusedEl) != RecompRml::CanFocus::Yes) {
                    Rml::Element* element = find_autofocus_element(current_document);
                    if (element != nullptr) {
                        element->Focus();
                    }
                }
            }
            return;
        }

        // If there was mouse motion, get the current hovered element (or its target if it points to one) and focus that if applicable.
        if (mouse_is_active) {
            if (mouse_is_active_changed) {
                Rml::Element* focused = current_document->GetFocusLeafNode();
                if (focused) focused->Blur();
            } else if (mouse_moved) {
                Rml::Element* hovered = context->GetHoverElement();
                if (hovered) {
                    Rml::Element* hover_target = get_target(current_document, hovered);
                    if (hover_target && can_focus(hover_target)) {
                        prev_focused = hover_target;
                    }
                }
            }
        }

        if (!mouse_is_active) {
            if (!prev_focused || !can_focus(prev_focused)) {
                // Find the autofocus element in the tab chain
                Rml::Element* element = find_autofocus_element(current_document);
                if (element && can_focus(element)) {
                    prev_focused = element;
                }
            }

            if (mouse_is_active_changed && prev_focused && can_focus(prev_focused)) {
                prev_focused->Focus();
            }
        }
    }

    void show_context(recompui::ContextId context) {
        if (std::find_if(opened_contexts.begin(), opened_contexts.end(), [context](auto& c){ return c.context == context; }) != opened_contexts.end()) {
            recompui::message_box("Attemped to show the same context twice");
            assert(false);
        }
        bool takes_input = context.takes_input();
        Rml::ElementDocument* document = context.get_document();
        opened_contexts.push_back(ContextDetails{
            .context = context,
            .document = document,
            .takes_input = takes_input
        });

        // auto& on_show = context.on_show;
        // if (on_show) {
        //     context.open();
        //     on_show();
        //     context.close();
        // }

        document->PullToFront();
        document->Show();
    }

    void hide_context(recompui::ContextId context) {
        auto remove_it = std::remove_if(opened_contexts.begin(), opened_contexts.end(), [context](auto& c) { return c.context == context; });
        if (remove_it == opened_contexts.end()) {
            recompui::message_box("Attemped to hide a context that isn't shown");
            assert(false);
        }
        opened_contexts.erase(remove_it, opened_contexts.end());

        context.get_document()->Hide();
    }
    
    void hide_all_contexts() {
        for (auto& context : opened_contexts) {
            context.document->Hide();
        }

        opened_contexts.clear();
    }

    bool is_context_open(recompui::ContextId context) {
        return std::find_if(opened_contexts.begin(), opened_contexts.end(), [context](auto& c){ return c.context == context; }) != opened_contexts.end();
    }

    bool is_context_taking_input() {
        return std::find_if(opened_contexts.begin(), opened_contexts.end(), [](auto& c){ return c.takes_input; }) != opened_contexts.end();
    }

    bool is_any_context_open() {
        return !opened_contexts.empty();
    }

    Rml::ElementDocument* top_input_document() {
        // Iterate backwards and stop at the first context that takes input.
        for (auto it = opened_contexts.rbegin(); it != opened_contexts.rend(); it++) {
            if (it->takes_input) {
                return it->document;
            }
        }
        return nullptr;
    }
};

std::unique_ptr<UIState> ui_state;
std::recursive_mutex ui_state_mutex{};

// TODO make this not be global
extern SDL_Window* window;

void recompui::get_window_size(int& width, int& height) {
    SDL_GetWindowSizeInPixels(window, &width, &height);
}

inline const std::string read_file_to_string(std::filesystem::path path) {
    std::ifstream stream = std::ifstream{path};
    std::ostringstream ss;
    ss << stream.rdbuf();
    return ss.str(); 
}

void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
#if defined(__linux__)
    std::locale::global(std::locale::classic());
#endif
    ui_state = std::make_unique<UIState>(window, interface, device);
    ui_state->load_documents();
}

moodycamel::ConcurrentQueue<SDL_Event> ui_event_queue{};

void recompui::queue_event(const SDL_Event& event) {
    ui_event_queue.enqueue(event);
}

bool recompui::try_deque_event(SDL_Event& out) {
    return ui_event_queue.try_dequeue(out);
}

int cont_button_to_key(SDL_ControllerButtonEvent& button) {
    // Configurable accept button in menu
    auto menuAcceptBinding0 = recomp::get_input_binding(recomp::GameInput::ACCEPT_MENU, 0, recomp::InputDevice::Controller);
    auto menuAcceptBinding1 = recomp::get_input_binding(recomp::GameInput::ACCEPT_MENU, 1, recomp::InputDevice::Controller);
    // note - magic number: 0 is InputType::None
    if ((menuAcceptBinding0.input_type != 0 && button.button == menuAcceptBinding0.input_id) ||
        (menuAcceptBinding1.input_type != 0 && button.button == menuAcceptBinding1.input_id)) {
        return SDLK_RETURN;
    }

    // Configurable apply button in menu
    auto menuApplyBinding0 = recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 0, recomp::InputDevice::Controller);
    auto menuApplyBinding1 = recomp::get_input_binding(recomp::GameInput::APPLY_MENU, 1, recomp::InputDevice::Controller);
    // note - magic number: 0 is InputType::None
    if ((menuApplyBinding0.input_type != 0 && button.button == menuApplyBinding0.input_id) ||
        (menuApplyBinding1.input_type != 0 && button.button == menuApplyBinding1.input_id)) {
        return SDLK_f;
    } 

    // Allows closing the menu
    auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller);
    auto menuToggleBinding1 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller);
    // note - magic number: 0 is InputType::None
    if ((menuToggleBinding0.input_type != 0 && button.button == menuToggleBinding0.input_id) ||
        (menuToggleBinding1.input_type != 0 && button.button == menuToggleBinding1.input_id)) {
        return SDLK_ESCAPE;
    }

    switch (button.button) {
        case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_UP:
            return SDLK_UP;
        case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_DOWN:
            return SDLK_DOWN;
        case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_LEFT:
            return SDLK_LEFT;
        case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
            return SDLK_RIGHT;
    }

    return 0;
}


int cont_axis_to_key(SDL_ControllerAxisEvent& axis, float value) {
    switch (axis.axis) {
    case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY:
        if (value < 0) return SDLK_UP;
        return SDLK_DOWN;
    case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTX:
        if (value >= 0) return SDLK_RIGHT;
        return SDLK_LEFT;
    }
    return 0;
}

void apply_background_input_mode() {
    static recomp::BackgroundInputMode last_input_mode = recomp::BackgroundInputMode::OptionCount;

    recomp::BackgroundInputMode cur_input_mode = recomp::get_background_input_mode();

    if (last_input_mode != cur_input_mode) {
        SDL_SetHint(
            SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
            cur_input_mode == recomp::BackgroundInputMode::On
                ? "1"
                : "0"
        );
    }
    last_input_mode = cur_input_mode;
}

bool recompui::get_cont_active() {
    return ui_state->cont_is_active;
}

void recompui::set_cont_active(bool active) {
    ui_state->cont_is_active = active;
}

void recompui::activate_mouse() {
    ui_state->update_primary_input(true, false);
    ui_state->update_focus(true, false);
}

void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* swap_chain_framebuffer) {

    apply_background_input_mode();

    // Return early if the ui context has been destroyed already.
    if (!ui_state) {
        return;
    }

    // Return to the launcher if no menu is open and the game isn't started.
    if (!recompui::is_any_context_open() && !ultramodern::is_game_started()) {
        recompui::show_context(recompui::get_launcher_context_id(), "");
    }

    std::lock_guard lock{ ui_state_mutex };

    SDL_Event cur_event{};

    bool mouse_moved = false;
    bool mouse_clicked = false;
    bool non_mouse_interacted = false;
    bool cont_interacted = false;
    bool kb_interacted = false;

    bool config_was_open = recompui::is_context_open(recompui::get_config_context_id()) || recompui::is_context_open(recompui::get_config_sub_menu_context_id());

    while (recompui::try_deque_event(cur_event)) {
        bool context_taking_input = recompui::is_context_taking_input();
        if (!recomp::all_input_disabled()) {
            // Implement some additional behavior for specific events on top of what RmlUi normally does with them.
            switch (cur_event.type) {
            case SDL_EventType::SDL_MOUSEMOTION: {
                int *last_mouse_pos = ui_state->last_active_mouse_position;

                if (!ui_state->mouse_is_active) {
                    float xD = cur_event.motion.x - last_mouse_pos[0];
                    float yD = cur_event.motion.y - last_mouse_pos[1];
                    if (sqrt(xD * xD + yD * yD) < 100) {
                        break;
                    }
                }
                last_mouse_pos[0] = cur_event.motion.x;
                last_mouse_pos[1] = cur_event.motion.y;

                // if controller is the primary input, don't use mouse movement to allow cursor to reactivate
                if (recompui::get_cont_active()) {
                    break;
                }
            }
            // fallthrough
            case SDL_EventType::SDL_MOUSEBUTTONDOWN:
                mouse_moved = true;
                mouse_clicked = true;
                break;
                
            case SDL_EventType::SDL_CONTROLLERBUTTONDOWN: {
                int rml_key = cont_button_to_key(cur_event.cbutton);
                if (context_taking_input && rml_key) {
                    ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(rml_key), 0);
                }
                non_mouse_interacted = true;
                cont_interacted = true;
                break;
            }
            case SDL_EventType::SDL_KEYDOWN:
                non_mouse_interacted = true;
                kb_interacted = true;
                break;
            case SDL_EventType::SDL_USEREVENT:
                if (cur_event.user.code == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY) {
                    ui_state->await_stick_return_y = true;
                } else if (cur_event.user.code == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTX) {
                    ui_state->await_stick_return_x = true;
                }
                break;
            case SDL_EventType::SDL_CONTROLLERAXISMOTION:
                SDL_ControllerAxisEvent* axis_event = &cur_event.caxis;
                if (axis_event->axis != SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY && axis_event->axis != SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTX) {
                    break;
                }

                float axis_value = axis_event->value * (1 / 32768.0f);
                bool* await_stick_return = axis_event->axis == SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_LEFTY
                        ? &ui_state->await_stick_return_y
                        : &ui_state->await_stick_return_x;
                if (fabsf(axis_value) > 0.5f) {
                    if (!*await_stick_return) {
                        *await_stick_return = true;
                        non_mouse_interacted = true;
                        int rml_key = cont_axis_to_key(cur_event.caxis, axis_value);
                        if (context_taking_input && rml_key) {
                            ui_state->context->ProcessKeyDown(RmlSDL::ConvertKey(rml_key), 0);
                        }
                    }
                    non_mouse_interacted = true;
                    cont_interacted = true;
                }
                else if (*await_stick_return && fabsf(axis_value) < 0.15f) {
                    *await_stick_return = false;
                }
                break;
            }

            if (context_taking_input) {
                RmlSDL::InputEventHandler(ui_state->context, cur_event);
            }
        }

        // If the config menu isn't open and the game has been started and either the escape key or select button are pressed, open the config menu.
        if (!config_was_open && ultramodern::is_game_started()) {
            bool open_config = false;

            switch (cur_event.type) {
            case SDL_EventType::SDL_KEYDOWN:
                if (cur_event.key.keysym.scancode == SDL_Scancode::SDL_SCANCODE_ESCAPE) {
                    open_config = true;
                }
                break;
            case SDL_EventType::SDL_CONTROLLERBUTTONDOWN:
                auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller);
                auto menuToggleBinding1 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 1, recomp::InputDevice::Controller);
                // note - magic number: 0 is InputType::None
                if ((menuToggleBinding0.input_type != 0 && cur_event.cbutton.button == menuToggleBinding0.input_id) ||
                    (menuToggleBinding1.input_type != 0 && cur_event.cbutton.button == menuToggleBinding1.input_id)) {
                    open_config = true;
                }
                break;
            }

            if (open_config) {
                recompui::show_context(recompui::get_config_context_id(), "");
            }
        }
    } // end dequeue event loop

    if (cont_interacted || kb_interacted || mouse_clicked) {
        recompui::set_cont_active(cont_interacted);
    }
    recomp::config_menu_set_cont_or_kb(ui_state->cont_is_active);

    recomp::InputField scanned_field = recomp::get_scanned_input();
    if (scanned_field != recomp::InputField{}) {
        recomp::finish_scanning_input(scanned_field);
    }

    ui_state->update_primary_input(mouse_moved, non_mouse_interacted);
    ui_state->update_focus(mouse_moved, non_mouse_interacted);

    if (recompui::is_any_context_open()) {
        int width = swap_chain_framebuffer->getWidth();
        int height = swap_chain_framebuffer->getHeight();

        // Scale the UI based on the window size with 1080 vertical resolution as the reference point.
        ui_state->context->SetDensityIndependentPixelRatio((height) / 1080.0f);

        ui_state->render_interface.start(command_list, width, height);

        static int prev_width = 0;
        static int prev_height = 0;

        if (prev_width != width || prev_height != height) {
            ui_state->context->SetDimensions({ width, height });
        }
        prev_width = width;
        prev_height = height;

        ui_state->context->Update();
        ui_state->context->Render();
        ui_state->render_interface.end(command_list, swap_chain_framebuffer);
    }
}

void deinit_hook() {
    recompui::destroy_all_contexts();

    std::lock_guard lock {ui_state_mutex};
    Rml::Debugger::Shutdown();
    Rml::Shutdown();
    ui_state->unload();
    ui_state.reset();
}

void recompui::set_render_hooks() {
    RT64::SetRenderHooks(init_hook, draw_hook, deinit_hook);
}

void recompui::message_box(const char* msg) {
    SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, zelda64::program_name.data(), msg, nullptr);
    printf("[ERROR] %s\n", msg);
}

void recompui::show_context(ContextId context, std::string_view param) {
    std::lock_guard lock{ui_state_mutex};

    // TODO call the context's on_show callback with the param.
    ui_state->show_context(context);
}

void recompui::hide_context(ContextId context) {
    std::lock_guard lock{ui_state_mutex};

    ui_state->hide_context(context);
}

void recompui::hide_all_contexts() {
    std::lock_guard lock{ui_state_mutex};

    if (ui_state) {
        ui_state->hide_all_contexts();
    }
}

bool recompui::is_context_open(ContextId context) {
    std::lock_guard lock{ui_state_mutex};

    if (!ui_state) {
        return false;
    }

    return ui_state->is_context_open(context);
}

bool recompui::is_context_taking_input() {
    std::lock_guard lock{ui_state_mutex};

    if (!ui_state) {
        return false;
    }

    return ui_state->is_context_taking_input();
}

bool recompui::is_any_context_open() {
    std::lock_guard lock{ui_state_mutex};

    if (!ui_state) {
        return false;
    }

    return ui_state->is_any_context_open();
}

Rml::ElementDocument* recompui::load_document(const std::filesystem::path& path) {
    std::lock_guard lock{ui_state_mutex};

    return ui_state->context->LoadDocument(path.string());
}

Rml::ElementDocument* recompui::create_empty_document() {
    std::lock_guard lock{ui_state_mutex};

    return ui_state->context->CreateDocument();
}

