#pragma once

#include <Tactility/MessageQueue.h>
#include <Tactility/PubSub.h>
#include <Tactility/RecursiveMutex.h>
#include <Tactility/service/Service.h>
#include <Tactility/service/loader/Loader.h>

#include <Tactility/Semaphore.h>

#include <tactility/concurrent/dispatcher.h>

#include <lvgl/devices/keyboard.h>

namespace tt::service::gui {

/**
 * Output a log warning if the current task is the GUI task.
 * This is meant for code that should either create their own task or use a different task to execute on.
 * @param[in] context a descriptive name or label that refers to the caller of this function
 */
void warnIfRunningOnGuiTask(const char* context);

class GuiService final : public Service {

    // Thread and lock
    Thread* thread = nullptr;
    DispatcherHandle_t dispatcher = nullptr;
    bool exitRequested = false;
    RecursiveMutex mutex;
    PubSub<loader::LoaderService::Event>::SubscriptionHandle loader_pubsub_subscription = nullptr;

    // Signaled by hideApp() once App::onHide() has actually finished running on the GUI
    // task. onLoaderEvent() blocks on this (still on the Loader thread, inside the
    // synchronous pubsub publish() call) before returning from the ApplicationHiding
    // branch, so LoaderService::transitionAppToState(Hiding) can't return - and therefore
    // the immediately-following Destroyed transition (which unloads an ELF app's code via
    // esp_elf_deinit) can't run - until onHide() has fully completed. Without this, the
    // ELF's code/data can be unmapped while onHide() (and anything it spawned, like a
    // camera capture task) is still executing it.
    Semaphore hideDoneSem { 1, 0 };

    // Layers and Canvas
    lv_obj_t* appRootWidget = nullptr;
    lv_obj_t* statusbarWidget = nullptr;

    // App-specific
    std::shared_ptr<app::AppInstance> appToRender = nullptr;

    LvglSoftwareKeyboard software_keyboard = {};

    bool isStarted = false;

    static int32_t guiMain();

    static void onGuiDispatch(void* context);

    void onLoaderEvent(loader::LoaderService::Event event);

    lv_obj_t* createAppViews(lv_obj_t* parent);

    void redraw();

    void lock() const {
        check(mutex.lock(pdMS_TO_TICKS(1000)));
    }

    void unlock() const {
        mutex.unlock();
    }

    void showApp(std::shared_ptr<app::AppInstance> app);

    void hideApp();

public:

    bool onStart(ServiceContext& service) override;

    void onStop(ServiceContext& service) override;

    /**
     * Show the on-screen keyboard.
     * @param[in] textarea the textarea to focus the input for
     */
    void softwareKeyboardShow(lv_obj_t* textarea);

    /**
     * Hide the on-screen keyboard.
     * Has no effect when the keyboard is not visible.
     */
    void softwareKeyboardHide();

    void keyboardAddTextArea(lv_obj_t* textarea);

    /**
     * The on-screen keyboard is only shown when both of these conditions are true:
     *  - there is no hardware keyboard
     *  - TT_CONFIG_FORCE_ONSCREEN_KEYBOARD is set to true in tactility_config.h
     * @return if we should show a on-screen keyboard for text input inside our apps
     */
    bool softwareKeyboardIsEnabled();
};

std::shared_ptr<GuiService> findService();

} // namespace
