
const int U_GrowthRule_Down = 1;
const int U_GrowthRule_Up = 2;
const int U_SizeRule_Exact = 4;
const int U_SizeRule_MatchText = 8;
const int U_AlignRule_Left = 16;
const int U_AlignRule_Center = 32;
const int U_AnimationRule_Off = 64;
const int U_LayoutRule_Absolute = 128;

struct U_Layout {
    int rule_flags;
    Vec2 pos;
    Vec2 size;
    Vec2 pos_iter;
    uint64_t di;
};

const int U_Action_Button = 1;
const int U_Action_Slider = 2;

struct U_Action {
    int flags;
    bool pressed;
};

struct U_Widget {
    S8_String string;
    Vec2 pos;
    Vec2 size;
    Vec2 string_pos;
    U_Action action;
    float t;

    float running_t; // @retained
    float max_t;     // @retained
    U_Layout layout; // @absolute

    uint64_t di;
    uint64_t hash;
    uint64_t last_touched_frame_index;
};

enum U_EventKind {
    U_EventKind_None,
    U_EventKind_Widget,
    U_EventKind_PushLayout,
    U_EventKind_PopLayout,
};

struct U_Event {
    U_EventKind kind;
    union {
        U_Widget *widget;
        U_Layout *layout;
    };
};

MA_Arena U_Arena;
Array<U_Layout *> U_FreeLayouts;
Array<U_Layout *> U_LayoutStack;

Array<U_Widget> U_RetainedWidgets;
Array<U_Event> U_Events;
Array<U_Widget *> U_WidgetCache;
Array<U_Widget *> U_FreeWidgets;
Array<U_Widget *> U_WidgetsToDraw;

U_Widget *U_Hot;
U_Widget *U_InteractingWith;
uint64_t U_DebugID;

// One frame delayed utility variables
bool U_InteractionEnded;
bool U_ClickedUnpress;
bool U_ClickedOutsideUI;
bool U_DrawDebug;

U_Widget U_NullWidget;

U_Widget *U_CreateWidget(S8_String string, int action_flags) {
    // Hash the string up to '::' so that varying numbers are ok
    int64_t string_to_hash_len = string.len;
    for (int64_t i = 0; i < string.len; i += 1) {
        if (string.str[i] == ':' && string.str[i + 1] == ':') {
            string_to_hash_len = i;
            break;
        }
    }
    uint64_t hash = HashBytes(string.str, string_to_hash_len);
    // string = S8_ReplaceAll(G_Frame, string, S8_Lit("##"), S8_Lit(""), 0);

    U_Widget *widget = 0;
    bool found_in_cache = false;
    For(U_WidgetCache) {
        if (it->hash == hash) {
            widget = it;
            found_in_cache = true;
            break;
        }
    }

    if (widget && widget->last_touched_frame_index == Mu->frame) {
        return &U_NullWidget;
    }

    if (widget == 0 && U_FreeWidgets.len) widget = U_FreeWidgets.pop();
    if (widget == 0) widget = MA_PushStruct(&U_Arena, U_Widget);

    if (!found_in_cache) {
        U_WidgetCache.add(widget);
        *widget = {};
    }

    // @string_lifetime:
    // I think we dont need to worry about the string lifetime because
    // it should get updated on every frame
    //
    // Will see how this plays out when adding notifications
    // maybe that should also be called on every frame etc.
    widget->string = string;
    widget->hash = hash;
    widget->last_touched_frame_index = Mu->frame;
    widget->di = ++U_DebugID;
    widget->action.flags = action_flags;

    U_Events.add({U_EventKind_Widget, widget});
    return widget;
}

void U_PushLayout(U_Layout layout) {
    layout.di = ++U_DebugID;

    U_Layout *l = 0;
    if (U_FreeLayouts.len) {
        l = U_FreeLayouts.pop();
    }
    else {
        l = MA_PushStruct(&U_Arena, U_Layout);
    }
    *l = layout;

    U_LayoutStack.add(l);
    auto e = U_Events.alloc();
    e->kind = U_EventKind_PushLayout;
    e->layout = l;
}

void U_PushLayoutIndent(float indent) {
    U_Layout **l = U_LayoutStack.last();
    U_Layout layout = **l;
    layout.pos.x += indent;
    U_PushLayout(layout);
}

void U_PopLayout() {
    U_Layout *l = U_LayoutStack.pop();
    auto e = U_Events.alloc();
    e->kind = U_EventKind_PopLayout;
    e->layout = l;
}

bool U_Button(char *str, ...) {
    S8_FORMAT(G_Frame, str, string_result);
    U_Widget *w = U_CreateWidget(string_result, U_Action_Button);
    return w->action.pressed;
}

bool U_Checkbox(bool *result, char *str, ...) {
    S8_FORMAT(G_Frame, str, string_result);
    U_Widget *w = U_CreateWidget(string_result, U_Action_Button);
    if (w->action.pressed) *result = !*result;
    return *result;
}

void U_Popup(Vec2 pos, char *str, ...) {
    S8_FORMAT(G_Frame, str, string_result);
    U_Widget *w = U_CreateWidget(string_result, 0);
    w->layout = {};
    w->layout.pos = pos;
    w->layout.rule_flags = U_LayoutRule_Absolute | U_AlignRule_Center | U_GrowthRule_Down | U_SizeRule_MatchText;
}

void U_Notification(float max_t, char *str, ...) {
    S8_FORMAT(G_Frame, str, string_result);
    U_Widget *w = U_RetainedWidgets.alloc();
    w->max_t = max_t;
    w->string = string_result;
    w->string.str = (char *)malloc(string_result.len + 1);
    w->di = ++U_DebugID;
    MA_MemoryCopy(w->string.str, string_result.str, string_result.len);
}

Vec2 UI_RectPadding = {32, 8.f};
float UI_DrawBox(float t, Rect2 animated_popup_rect) {
    float bounce_t = EaseOutElastic(Clamp01(t));
    animated_popup_rect = ShrinkByHalfSize(animated_popup_rect, (1 - bounce_t) * CalcSize(animated_popup_rect) / 2);
    Rect2 popup_rect = ExpandByHalfSize(animated_popup_rect, UI_RectPadding);
    Rect2 outline_popup_rect = ExpandByHalfSize(popup_rect, {1.f, 1.f});

    R2_DrawRectRounded(popup_rect, COLOR_on_hover_rect, 0.1f);
    return bounce_t;
}

void U_EndFrame() {
    R2.font = &R2.font_medium;
    R2.vertex_list = &R2.ui_vertex_list;
    IO_Assert(U_LayoutStack.len == 0);

    // We move all of the "inline" function logic into the event list
    // and then afterwards we only render those things that got updated
    // in this frame (which got it's index updated meaning that they dont
    // get evicted from cache.

    For(U_Events) {
        if (it.kind == U_EventKind_PushLayout) {
            U_LayoutStack.add(it.layout);
            continue;
        }
        else if (it.kind == U_EventKind_PopLayout) {
            U_LayoutStack.pop();
            continue;
        }
        else {
            IO_Assert(it.kind == U_EventKind_Widget);
        }
        U_Widget *widget = it.widget;

        U_Layout *layout = 0;
        if (U_LayoutStack.len) layout = *U_LayoutStack.last();
        if (widget->layout.rule_flags & U_LayoutRule_Absolute) layout = &widget->layout;

        Vec2 string_size = R2_GetStringSize(widget->string);

        if (layout->rule_flags & U_SizeRule_Exact) {
            widget->size = layout->size;
        }
        else if (layout->rule_flags & U_SizeRule_MatchText) {
            widget->size = string_size;
        }
        else {
            IO_InvalidCodepath();
        }

        widget->pos = layout->pos + layout->pos_iter;
        if (layout->rule_flags & U_GrowthRule_Up) {
            layout->pos_iter.y += widget->size.y;
        }
        else if (layout->rule_flags & U_GrowthRule_Down) {
            layout->pos_iter.y -= widget->size.y;
        }
        else {
            IO_InvalidCodepath();
        }

        if (layout->rule_flags & U_AnimationRule_Off) {
            widget->t = 1.0f;
        }

        Rect2 string_rect = R2_GetStringRect(widget->pos, widget->string);
        widget->string_pos = widget->pos;
        // align font (aligned to baseline) to top left of rectangle
        widget->string_pos.y = widget->pos.y + (widget->pos.y - string_rect.min.y);

        if (layout->rule_flags & U_AlignRule_Left) {
            // widget->string_pos = widget->pos + (widget->size - string_size) / 2;
            widget->string_pos.y += (widget->size.y - string_size.y) / 2;
            widget->string_pos.x += 32;
        }
        else if (layout->rule_flags & U_AlignRule_Center) {
            widget->string_pos += (widget->size - string_size) / 2;
        }
        else {
            IO_InvalidCodepath();
        }
    }

    float retained_y_iter = 0;
    ForArrayRemovable(U_RetainedWidgets) {
        ForArrayRemovablePrepare(U_RetainedWidgets);

        it.t = Clamp01(it.running_t);
        float completion_rate = it.running_t / it.max_t;
        if (completion_rate > 1.05) {
            free(it.string.str);
            ForArrayRemovableDeclare();
            continue;
        }
        if (completion_rate > 0.8f) {
            float eigth = 0.8f * it.max_t;
            it.t = it.t - (it.running_t - eigth);
        }

        it.running_t += Mu->time.deltaf;

        Vec2 string_size = R2_GetStringSize(it.string);

        it.size = string_size + UI_RectPadding;
        it.pos = UI_RectPadding;
        it.pos.y += retained_y_iter;
        retained_y_iter += string_size.y + UI_RectPadding.y * 2;

        // @copy_paste: from layouting, might need to collapse it in the future
        Rect2 string_rect = R2_GetStringRect(it.pos, it.string);
        it.string_pos = it.pos;
        // align font (aligned to baseline) to top left of rectangle
        it.string_pos.y = it.pos.y + (it.pos.y - string_rect.min.y);
        // align center
        it.string_pos += (it.size - string_size) / 2;
        U_WidgetsToDraw.add(&it);
    }

    ForArrayRemovable(U_WidgetCache) {
        ForArrayRemovablePrepare(U_WidgetCache);
        if (it->last_touched_frame_index != Mu->frame) {
            ForArrayRemovableDeclare();
            U_FreeWidgets.add(it);
        }
        else {
            U_WidgetsToDraw.add(it);
        }
    }

    U_InteractionEnded = false;
    U_ClickedUnpress = false;
    U_ClickedOutsideUI = false;
    U_Hot = 0;
    For(U_WidgetsToDraw) {
        bool activated = false;
        Rect2 rect = Rect2_Size(it->pos, it->size);

        if (AreColliding(rect, Mu->window->mouse.posf)) {
            U_Hot = it;
        }

        if (U_Hot == it) {
            if (Mu->window->mouse.left.press) {
                U_InteractingWith = it;
            }
        }

        if (U_InteractingWith) {
            if (Mu->window->mouse.left.unpress) {
                if (U_Hot == it) {
                    activated = true;
                }
                U_InteractionEnded = true;
            }
        }

        if (it->action.flags & U_Action_Button) {
            it->action.pressed = false;
            if (activated) it->action.pressed = true;
        }

        Vec4 text_color = COLOR_text;
        if (it->action.flags & U_Action_Button) {
            if (U_Hot == it) text_color = COLOR_highlighted_text;
            if (U_InteractingWith == it) text_color = {1, 0, 0, 1};
            if (activated) text_color = {0, 1, 0, 1};
        }
        text_color.a = UI_DrawBox(it->t, rect);
        R2_DrawString(it->string_pos, it->string, text_color);
        it->t += Mu->time.deltaf;
    }

    if (Mu->window->mouse.left.unpress) {
        U_ClickedUnpress = true;
        U_InteractingWith = 0;
    }
    if (Mu->window->mouse.left.press && U_InteractingWith == 0) {
        U_ClickedOutsideUI = true;
    }

    if (U_DrawDebug) {
        R2_DebugString("FreeWidgets: %d", U_FreeWidgets.len);
        R2_DebugString("CachedWidgets: %d", U_WidgetCache.len);
        R2_DebugString("Events: %d", U_Events.len);
        R2_DebugString("FreeLayouts: %d", U_FreeLayouts.len);
        R2_DebugString("RetainedElements: %d", U_RetainedWidgets.len);
    }

    For(U_Events) {
        if (it.kind == U_EventKind_PushLayout) {
            U_FreeLayouts.add(it.layout);
        }
    }

    U_Events.reset();
    U_WidgetsToDraw.reset();
    R2.vertex_list = &R2.base_vertex_list;
}
