/**
 * @file ImGuiNotify.hpp
 * @brief A header-only library for creating toast notifications with ImGui.
 * 
 * Based on imgui-notify by patrickcjk
 * https://github.com/patrickcjk/imgui-notify
 * 
 * @version 0.0.3 by TyomaVader
 * @date 07.07.2024
 */

#ifndef IMGUI_NOTIFY
#define IMGUI_NOTIFY

#pragma once

#include <vector>			// Vector for storing notifications list
#include <string>
#include <chrono>			// For the notifications timed dissmiss
#include <functional>		// For storing the code, which executest on the button click in the notification

#include "imgui.h"
#include "imgui_internal.h"








/**
 * CONFIGURATION SECTION Start
*/

#define NOTIFY_MAX_MSG_LENGTH				4096		// Max message content length
#define NOTIFY_PADDING_X					20.f		// Bottom-left X padding
#define NOTIFY_PADDING_Y					20.f		// Bottom-left Y padding
#define NOTIFY_PADDING_MESSAGE_Y			10.f		// Padding Y between each message
#define NOTIFY_FADE_IN_OUT_TIME				150			// Fade in and out duration
#define NOTIFY_DEFAULT_DISMISS				3000		// Auto dismiss after X ms (default, applied only of no data provided in constructors)
#define NOTIFY_OPACITY						0.8f		// 0-1 Toast opacity
#define NOTIFY_USE_SEPARATOR 				false 		// If true, a separator will be rendered between the title and the content
#define NOTIFY_USE_DISMISS_BUTTON			false		// If true, a dismiss button will be rendered in the top right corner of the toast
#define NOTIFY_RENDER_LIMIT					5			// Max number of toasts rendered at the same time. Set to 0 for unlimited

// Warning: Requires ImGui docking with multi-viewport enabled
#ifndef NOTIFY_RENDER_OUTSIDE_MAIN_WINDOW
#define NOTIFY_RENDER_OUTSIDE_MAIN_WINDOW	false		// If true, the notifications will be rendered in the corner of the monitor, otherwise in the corner of the main window
#endif

/**
 * CONFIGURATION SECTION End
*/








static const ImGuiWindowFlags NOTIFY_DEFAULT_TOAST_FLAGS = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoFocusOnAppearing;

#define NOTIFY_NULL_OR_EMPTY(str)		(!str || !strlen(str))
#define NOTIFY_FORMAT(fn, format, ...)	if (format) {va_list args; va_start(args, format); fn(format, args, ##__VA_ARGS__); va_end(args);}

enum class ImGuiToastType : uint8_t
{
    None,
    Success,
    Warning,
    Error,
    Info,
    COUNT
};

enum class ImGuiToastPhase : uint8_t
{
    FadeIn,
    Wait,
    FadeOut,
    Expired,
    COUNT
};

enum class ImGuiToastPos : uint8_t
{
    TopLeft,
    TopCenter,
    TopRight,
    BottomLeft,
    BottomCenter,
    BottomRight,
    Center,
    COUNT
};

/**
 * @brief A class for creating toast notifications with ImGui.
 */
class ImGuiToast
{
private:
    ImGuiWindowFlags							flags = NOTIFY_DEFAULT_TOAST_FLAGS;

    ImGuiToastType								type = ImGuiToastType::None;
    char										title[NOTIFY_MAX_MSG_LENGTH];
    char										content[NOTIFY_MAX_MSG_LENGTH];

    int											dismissTime = NOTIFY_DEFAULT_DISMISS;
    std::chrono::system_clock::time_point		creationTime = std::chrono::system_clock::now();

    std::function<void()>						onButtonPress = nullptr; // A lambda variable, which will be executed when button in notification is pressed
    char 										buttonLabel[NOTIFY_MAX_MSG_LENGTH];
    int id = 0; // Unique ID to prevent ImGui cache collisions

private:
    // Setters

    inline void setTitle(const char* format, va_list args)
    {
        vsnprintf(this->title, sizeof(this->title), format, args);
    }

    inline void setContent(const char* format, va_list args)
    {
        vsnprintf(this->content, sizeof(this->content), format, args);
    }

    inline void setButtonLabel(const char* format, va_list args)
    {
        vsnprintf(this->buttonLabel, sizeof(this->buttonLabel), format, args);
    }

    inline ImVec4 toneMapColor(const ImVec4& color)
    {
        // Apply tone mapping (e.g., Reinhard tone mapping)
        float luminance = 0.2126f * color.x + 0.7152f * color.y + 0.0722f * color.z;
        float mappedLuminance = luminance / (1.0f + luminance);
        float scale = mappedLuminance / luminance;

        return ImVec4(color.x * scale, color.y * scale, color.z * scale, color.w);
    }

public:
    public:
    inline int getId() const { return this->id; }

    /**
     * @brief Set the title of the toast notification.
     * 
     * @param format The format string for the title.
     * @param ... The arguments for the format string.
     */
    inline void setTitle(const char* format, ...)
    {
        NOTIFY_FORMAT(this->setTitle, format);
    }

    /**
     * @brief Set the content of the toast notification.
     * 
     * @param format The format string for the content.
     * @param ... The arguments for the format string.
     */
    inline void setContent(const char* format, ...)
    {
        NOTIFY_FORMAT(this->setContent, format);
    }

    /**
     * @brief Set the type of the toast notification.
     * 
     * @param type The type of the toast notification.
     */
    inline void setType(const ImGuiToastType& type)
    {
        IM_ASSERT(type < ImGuiToastType::COUNT);
        this->type = type;
    };

    /**
     * @brief Set the ImGui window flags for the notification.
     * 
     * @param flags ImGui window flags to set.
    */
    inline void setWindowFlags(const ImGuiWindowFlags& flags)
    {
        this->flags = flags;
    }

    /**
     * @brief Set the function to run on the button click in the notification.
     * 
     * @param onButtonPress std::fuction or lambda expression, which contains the code for execution.
    */
    inline void setOnButtonPress(const std::function<void()>& onButtonPress)
    {
        this->onButtonPress = onButtonPress;
    }

    /**
     * @brief Set the label for the button in the notification.
     * 
     * @param format The format string for the label.
     * @param ... The arguments for the format string.
    */
    inline void setButtonLabel(const char* format, ...)
    {
        NOTIFY_FORMAT(this->setButtonLabel, format);
    }

public:
    // Getters

    /**
     * @brief Get the title of the toast notification.
     * 
     * @return const char* The title of the toast notification.
     */
    inline const char* getTitle()
    {
        return this->title;
    };

    /**
     * @brief Get the default title of the toast notification based on its type.
     * 
     * @return const char* The default title of the toast notification.
     */
    inline const char* getDefaultTitle()
    {
        if (!strlen(this->title))
        {
            switch (this->type)
            {
            case ImGuiToastType::None:
                return nullptr;
            case ImGuiToastType::Success:
                return "Success";
            case ImGuiToastType::Warning:
                return "Warning";
            case ImGuiToastType::Error:
                return "Error";
            case ImGuiToastType::Info:
                return "Info";
            default:
                return nullptr;
            }
        }

        return this->title;
    };

    /**
     * @brief Get the type of the toast notification.
     * 
     * @return ImGuiToastType The type of the toast notification.
     */
    inline ImGuiToastType getType()
    {
        return this->type;
    };

    /**
     * @brief Get the color of the toast notification based on its type.
     * 
     * @return ImVec4 The color of the toast notification.
     */
    inline ImVec4 getColor(bool toneMap = false)
    {
        ImVec4 color;

        switch (this->type)
        {
        case ImGuiToastType::None:
            color = { 255, 255, 255, 255 }; // White
            break;
        case ImGuiToastType::Success:
            color = {0, 255, 0, 255}; // Green
            break;
        case ImGuiToastType::Warning:
            color = {255, 255, 0, 255}; // Yellow
            break;
        case ImGuiToastType::Error:
            color = {255, 0, 0, 255}; // Error
            break;
        case ImGuiToastType::Info:
            color = {0, 157, 255, 255}; // Blue
            break;
        default:
            color = {255, 255, 255, 255}; // White
        }

        if (toneMap)
            color = toneMapColor(color);

        return color;
    }

    /**
     * @brief Get the icon of the toast notification based on its type.
     * 
     * @return const char* The icon of the toast notification.
     */
    inline const char* getIcon() 
    { 
        return nullptr;
    }

    /**
     * @brief Get the content of the toast notification.
     * 
     * @return char* The content of the toast notification.
     */
    inline char* getContent() 
    {
        return this->content;
    };

    /**
     * @brief Get the elapsed time in milliseconds since the creation of the object.
     * 
     * @return int64_t The elapsed time in milliseconds.
     * @throws An exception with the message "Unsupported platform" if the platform is not supported.
     */
    inline std::chrono::nanoseconds getElapsedTime() 
    {
        return std::chrono::system_clock::now() - this->creationTime;
    }

    /**
     * @brief Get the current phase of the toast notification based on the elapsed time since its creation.
     * 
     * @return ImGuiToastPhase The current phase of the toast notification.
     *         - ImGuiToastPhase::FadeIn: The notification is fading in.
     *         - ImGuiToastPhase::Wait: The notification is waiting to be dismissed.
     *         - ImGuiToastPhase::FadeOut: The notification is fading out.
     *         - ImGuiToastPhase::Expired: The notification has expired and should be removed.
     */
    inline ImGuiToastPhase getPhase()
    {
        const int64_t elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(getElapsedTime()).count();

        if (elapsed > NOTIFY_FADE_IN_OUT_TIME + this->dismissTime + NOTIFY_FADE_IN_OUT_TIME)
        {
            return ImGuiToastPhase::Expired;
        } else 
        if (elapsed > NOTIFY_FADE_IN_OUT_TIME + this->dismissTime)
        {
            return ImGuiToastPhase::FadeOut;
        } else 
        if (elapsed > NOTIFY_FADE_IN_OUT_TIME)
        {
            return ImGuiToastPhase::Wait;
        } else
        {
            return ImGuiToastPhase::FadeIn;
        }
    }

    /**
     * Returns the percentage of fade for the notification.
     * @return The percentage of fade for the notification.
     */
    inline float getFadePercent()
    {
         const ImGuiToastPhase phase = getPhase();
        const int64_t elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(getElapsedTime()).count();

        if (phase == ImGuiToastPhase::FadeIn)
        {
            return ((float)elapsed / (float)NOTIFY_FADE_IN_OUT_TIME) * NOTIFY_OPACITY;
        } else 
        if (phase == ImGuiToastPhase::FadeOut)
        {
            return (1.f - (((float)elapsed - (float)NOTIFY_FADE_IN_OUT_TIME - (float)this->dismissTime) / (float)NOTIFY_FADE_IN_OUT_TIME)) * NOTIFY_OPACITY;
        }

        return 1.f * NOTIFY_OPACITY;
    }

    /**
     * @return ImGui window flags for the notification.
    */
    inline ImGuiWindowFlags getWindowFlags()
    {
        return this->flags;
    }

    /**
     * @return The function, which is executed on the button click in the notification.
    */
    inline std::function<void()> getOnButtonPress()
    {
        return this->onButtonPress;
    }

    /**
     * @return The label on the button in notification.
    */
    inline const char* getButtonLabel()
    {
        return this->buttonLabel;
    }

public:
    // Constructors

    /**
     * @brief Creates a new ImGuiToast object with the specified type and dismiss time.
     * 
     * @param type The type of the toast.
     * @param dismissTime The time in milliseconds after which the toast should be dismissed. Default is NOTIFY_DEFAULT_DISMISS.
     */
    ImGuiToast(ImGuiToastType type, int dismissTime = NOTIFY_DEFAULT_DISMISS)
    {
        IM_ASSERT(type < ImGuiToastType::COUNT);

        this->type = type;
        this->dismissTime = dismissTime;

        this->creationTime = std::chrono::system_clock::now();

        static int toastIdCounter = 0;
        this->id = toastIdCounter++;

        memset(this->title, 0, sizeof(this->title));
        memset(this->content, 0, sizeof(this->content));
    }

    /**
     * @brief Constructor for creating an ImGuiToast object with a specified type and message format.
     * 
     * @param type The type of the toast message.
     * @param format The format string for the message.
     * @param ... The variable arguments to be formatted according to the format string.
     */
    ImGuiToast(ImGuiToastType type, const char* format, ...) : ImGuiToast(type)
    {
        NOTIFY_FORMAT(this->setContent, format);
    }

    /**
     * @brief Constructor for creating a new ImGuiToast object with a specified type, dismiss time, and content format.
     * 
     * @param type The type of the toast message.
     * @param dismissTime The time in milliseconds before the toast message is dismissed.
     * @param format The format string for the content of the toast message.
     * @param ... The variable arguments to be formatted according to the format string.
     */
    ImGuiToast(ImGuiToastType type, int dismissTime, const char* format, ...) : ImGuiToast(type, dismissTime)
    {
        NOTIFY_FORMAT(this->setContent, format);
    }

    /**
     * @brief Constructor for creating a new ImGuiToast object with a specified type, dismiss time, title format, content format and a button.
     * 
     * @param type The type of the toast message.
     * @param dismissTime The time in milliseconds before the toast message is dismissed.
     * @param buttonLabel The label for the button.
     * @param onButtonPress The lambda function to be executed when the button is pressed.
     * @param format The format string for the content of the toast message.
     * @param ... The variable arguments to be formatted according to the format string.
     */
    ImGuiToast(ImGuiToastType type, int dismissTime, const char* buttonLabel, const std::function<void()>& onButtonPress, const char* format, ...) : ImGuiToast(type, dismissTime)
    {
        NOTIFY_FORMAT(this->setContent, format);

        this->onButtonPress = onButtonPress;
        this->setButtonLabel(buttonLabel);
    }
};

namespace ImGui
{
    inline std::vector<ImGuiToast> notifications;

    /**
     * Inserts a new notification into the notification queue.
     * @param toast The notification to be inserted.
     */
    inline void InsertNotification(const ImGuiToast& toast)
    {
        notifications.push_back(toast);
    }

    /**
     * @brief Removes a notification from the list of notifications.
     * 
     * @param index The index of the notification to remove.
     */
    inline void RemoveNotification(size_t index)
    {
        notifications.erase(notifications.begin() + index);
    }

    /**
     * Renders all notifications in the notifications vector.
     * Each notification is rendered as a toast window with a title, content and an optional icon.
     * If a notification is expired, it is removed from the vector.
     */
    inline void RenderNotifications(ImGuiToastPos toastPos, float scale, bool toneMap = false)
    {
        const ImVec2 mainWindowPos = GetMainViewport()->Pos;
        const ImVec2 mainWindowSize = GetMainViewport()->Size;

        float stackHeight = 0.f;

        bool stylePushed = false;

        const static auto defaultStyle = ImGuiStyle();

        if (!notifications.empty())
        {
            ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, defaultStyle.WindowPadding * scale);
            ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, defaultStyle.FramePadding * scale);
            ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, defaultStyle.CellPadding * scale);
            ImGui::PushStyleVar(ImGuiStyleVar_SeparatorTextPadding, defaultStyle.SeparatorTextPadding * scale);

            ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, defaultStyle.ItemSpacing * scale);
            ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, defaultStyle.ItemInnerSpacing * scale);
            ImGui::PushStyleVar(ImGuiStyleVar_IndentSpacing, defaultStyle.IndentSpacing * scale);

            stylePushed = true;
        }

        for (size_t i = 0; i < notifications.size(); ++i)
        {
            ImGuiToast* currentToast = &notifications[i];

            // Remove toast if expired
            if (currentToast->getPhase() == ImGuiToastPhase::Expired)
            {
                RemoveNotification(i);
                continue;
            }

            #if NOTIFY_RENDER_LIMIT > 0
                if (i > NOTIFY_RENDER_LIMIT)
                {
                    continue;
                }
            #endif

            // Get icon, title and other data
            const char* icon = currentToast->getIcon();
            const char* title = currentToast->getTitle();
            const char* content = currentToast->getContent();
            const char* defaultTitle = currentToast->getDefaultTitle();
            const float opacity = currentToast->getFadePercent(); // Get opacity based of the current phase

            // Window rendering
            ImVec4 textColor = currentToast->getColor();
            textColor.w = opacity;

            // Generate new unique name for this toast
            char windowName[50];
            #ifdef _WIN32
            sprintf_s(windowName, "##TOAST%d", currentToast->getId());
            #elif defined(__linux__) || defined(__EMSCRIPTEN__)
            std::sprintf(windowName, "##TOAST%d", currentToast->getId());
            #elif defined(__APPLE__)
            std::snprintf(windowName, 50, "##TOAST%d", currentToast->getId());
            #else
                throw "Unsupported platform";
            #endif

            //PushStyleColor(ImGuiCol_Text, textColor);
            SetNextWindowBgAlpha(opacity);

            ImVec2 windowPos;
            ImVec2 pivot;

            switch (toastPos)
            {
            case ImGuiToastPos::TopLeft:
                windowPos =
                    ImVec2(mainWindowPos.x + NOTIFY_PADDING_X, mainWindowPos.y + NOTIFY_PADDING_Y + stackHeight);
                pivot = ImVec2(0.0f, 0.0f);
                break;
            case ImGuiToastPos::TopCenter:
                windowPos =
                    ImVec2(mainWindowPos.x + mainWindowSize.x * 0.5f, mainWindowPos.y + NOTIFY_PADDING_Y + stackHeight);
                pivot = ImVec2(0.5f, 0.0f);
                break;
            case ImGuiToastPos::TopRight:
                windowPos = ImVec2(mainWindowPos.x + mainWindowSize.x - NOTIFY_PADDING_X,
                                   mainWindowPos.y + NOTIFY_PADDING_Y + stackHeight);
                pivot = ImVec2(1.0f, 0.0f);
                break;
            case ImGuiToastPos::BottomLeft:
                windowPos = ImVec2(mainWindowPos.x + NOTIFY_PADDING_X,
                                   mainWindowPos.y + mainWindowSize.y - NOTIFY_PADDING_Y - stackHeight);
                pivot = ImVec2(0.0f, 1.0f);
                break;
            case ImGuiToastPos::BottomCenter:
                windowPos = ImVec2(mainWindowPos.x + mainWindowSize.x * 0.5f,
                                   mainWindowPos.y + mainWindowSize.y - NOTIFY_PADDING_Y - stackHeight);
                pivot = ImVec2(0.5f, 1.0f);
                break;
            case ImGuiToastPos::Center:
                windowPos = ImVec2(mainWindowPos.x + mainWindowSize.x * 0.5f,
                                   mainWindowPos.y + mainWindowSize.y * 0.5f + stackHeight);
                pivot = ImVec2(0.5f, 0.5f);
                break;
            case ImGuiToastPos::BottomRight:
            default:
                windowPos = ImVec2(mainWindowPos.x + mainWindowSize.x - NOTIFY_PADDING_X,
                                   mainWindowPos.y + mainWindowSize.y - NOTIFY_PADDING_Y - stackHeight);
                pivot = ImVec2(1.0f, 1.0f);
                break;
            }

            SetNextWindowPos(windowPos, ImGuiCond_Always, pivot);

            // Set notification window flags
            if (!NOTIFY_USE_DISMISS_BUTTON && currentToast->getOnButtonPress() == nullptr)
            {
                currentToast->setWindowFlags(NOTIFY_DEFAULT_TOAST_FLAGS | ImGuiWindowFlags_NoInputs);
            }

            Begin(windowName, nullptr, currentToast->getWindowFlags());

            // Render over all other windows
            BringWindowToDisplayFront(GetCurrentWindow());

            // Here we render the toast content
            {
                PushTextWrapPos(mainWindowSize.x / 3.f); // We want to support multi-line text, this will wrap the text after 1/3 of the screen width

                bool wasTitleRendered = false;

                // If an icon is set
                if (!NOTIFY_NULL_OR_EMPTY(icon))
                {
                    //Text(icon); // Render icon text
                    TextColored(textColor, "%s", icon);
                    wasTitleRendered = true;
                }

                // If a title is set
                if (!NOTIFY_NULL_OR_EMPTY(title))
                {
                    // If a title and an icon is set, we want to render on same line
                    if (!NOTIFY_NULL_OR_EMPTY(icon))
                        SameLine();

                    //Text("%s", title); // Render title text
                    TextColored(textColor, "%s", title);
                    wasTitleRendered = true;
                } else 
                if (!NOTIFY_NULL_OR_EMPTY(defaultTitle))
                {
                    if (!NOTIFY_NULL_OR_EMPTY(icon))
                        SameLine();

                    TextColored(
                        textColor, "%s",
                        defaultTitle); // Render default title text (ImGuiToastType_Success -> "Success", etc...)
                    wasTitleRendered = true;
                }

                // If a dismiss button is enabled
                if (NOTIFY_USE_DISMISS_BUTTON)
                {
                    // If a title or content is set, we want to render the button on the same line
                    if (wasTitleRendered || !NOTIFY_NULL_OR_EMPTY(content))
                    {
                        SameLine();
                    }

                    // Render the dismiss button on the top right corner
                    // NEEDS TO BE REWORKED
                    float scale = 0.8f;

                    if (CalcTextSize(content).x > GetContentRegionAvail().x)
                    {
                        scale = 0.8f;
                    }

                    SetCursorPosX(GetCursorPosX() + (GetWindowSize().x - GetCursorPosX()) * scale);

                    // If the button is pressed, we want to remove the notification
                    if (Button("X"))
                    {
                        RemoveNotification(i);
                    }
                }

                // In case ANYTHING was rendered in the top, we want to add a small padding so the text (or icon) looks centered vertically
                if (wasTitleRendered && !NOTIFY_NULL_OR_EMPTY(content))
                {
                    SetCursorPosY(GetCursorPosY() + 5.f); // Must be a better way to do this!!!!
                }

                // If a content is set
                if (!NOTIFY_NULL_OR_EMPTY(content))
                {
                    if (wasTitleRendered)
                    {
                        #if NOTIFY_USE_SEPARATOR
                            Separator();
                        #endif
                    }

                    Text("%s", content); // Render content text
                }

                // If a button is set
                if (currentToast->getOnButtonPress() != nullptr)
                {
                    // If the button is pressed, we want to execute the lambda function
                    if (Button(currentToast->getButtonLabel()))
                    {
                        currentToast->getOnButtonPress()();
                    }
                }

                PopTextWrapPos();
            }

            // Save height for next toasts
            auto windowSize = ImGui::GetWindowSize();

            // End
            End();

            stackHeight += windowSize.y + NOTIFY_PADDING_MESSAGE_Y;
        }

        if (stylePushed)
            ImGui::PopStyleVar(7);
    }
}

#endif
