#include "hid_ptt_menu.h"
#include "hid_ptt.h"
#include <gui/elements.h>
#include <m-array.h>
#include "../hid.h"
#include "../views.h"

#include "hid_icons.h"

#define TAG                           "HidPushToTalkMenu"
#define PTT_MENU_HELP_HINT_DELAY_MS   5000U
#define PTT_MENU_HINT_TIMER_PERIOD_MS 150U

struct HidPushToTalkMenu {
    View* view;
    Hid* hid;
    PushToTalkMenuLongOkCallback long_ok_callback;
    void* long_ok_callback_context;
    FuriTimer* hint_timer;
};

typedef struct {
    FuriString* label;
    uint32_t index;
    PushToTalkMenuItemCallback callback;
    void* callback_context;
} PushToTalkMenuItem;

// Menu item
static void PushToTalkMenuItem_init(PushToTalkMenuItem* item) {
    item->label = furi_string_alloc();
    item->index = 0;
}

static void PushToTalkMenuItem_init_set(PushToTalkMenuItem* item, const PushToTalkMenuItem* src) {
    item->label = furi_string_alloc_set(src->label);
    item->index = src->index;
}

static void PushToTalkMenuItem_set(PushToTalkMenuItem* item, const PushToTalkMenuItem* src) {
    furi_string_set(item->label, src->label);
    item->index = src->index;
}

static void PushToTalkMenuItem_clear(PushToTalkMenuItem* item) {
    furi_string_free(item->label);
}

ARRAY_DEF(
    PushToTalkMenuItemArray,
    PushToTalkMenuItem,
    (INIT(API_2(PushToTalkMenuItem_init)),
     SET(API_6(PushToTalkMenuItem_set)),
     INIT_SET(API_6(PushToTalkMenuItem_init_set)),
     CLEAR(API_2(PushToTalkMenuItem_clear))))

// Menu list (horisontal, 2d array)
typedef struct {
    FuriString* label;
    uint32_t index;
    PushToTalkMenuItemArray_t items;
} PushToTalkMenuList;

typedef struct {
    size_t list_position;
    size_t position;
    size_t window_position;
    PushToTalkMenuList* lists;
    int lists_count;
    uint32_t last_interaction_tick;
    size_t hint_list_position;
    size_t hint_item_position;
    bool hint_visible;
    uint16_t hint_scroll_tick;
} HidPushToTalkMenuModel;

static void hid_ptt_menu_mark_interaction(HidPushToTalkMenu* hid_ptt_menu) {
    with_view_model(
        hid_ptt_menu->view,
        HidPushToTalkMenuModel * model,
        {
            model->last_interaction_tick = furi_get_tick();
            model->hint_list_position = model->list_position;
            model->hint_item_position = model->position;
            model->hint_visible = false;
            model->hint_scroll_tick = 0;
        },
        true);
}

static void hid_ptt_menu_hint_timer_callback(void* context) {
    furi_assert(context);
    HidPushToTalkMenu* hid_ptt_menu = context;
    with_view_model(
        hid_ptt_menu->view,
        HidPushToTalkMenuModel * model,
        {
            const uint32_t now = furi_get_tick();
            const bool selection_changed = (model->list_position != model->hint_list_position) ||
                                           (model->position != model->hint_item_position);

            if(selection_changed) {
                model->hint_list_position = model->list_position;
                model->hint_item_position = model->position;
                model->last_interaction_tick = now;
                model->hint_visible = false;
                model->hint_scroll_tick = 0;
            } else if(!model->hint_visible) {
                if((now - model->last_interaction_tick) >= PTT_MENU_HELP_HINT_DELAY_MS) {
                    model->hint_visible = true;
                    model->hint_scroll_tick = 0;
                }
            } else {
                model->hint_scroll_tick++;
            }
        },
        true);
}

static void hid_ptt_menu_enter_callback(void* context) {
    furi_assert(context);
    HidPushToTalkMenu* hid_ptt_menu = context;
    hid_ptt_menu_mark_interaction(hid_ptt_menu);
    furi_timer_start(hid_ptt_menu->hint_timer, furi_ms_to_ticks(PTT_MENU_HINT_TIMER_PERIOD_MS));
}

static void hid_ptt_menu_exit_callback(void* context) {
    furi_assert(context);
    HidPushToTalkMenu* hid_ptt_menu = context;
    furi_timer_stop(hid_ptt_menu->hint_timer);
}

static void
    hid_ptt_menu_draw_list(Canvas* canvas, void* context, const PushToTalkMenuItemArray_t items) {
    furi_assert(context);
    HidPushToTalkMenuModel* model = context;
    const uint8_t item_height = 16;
    uint8_t item_width = canvas_width(canvas) - 5;

    canvas_set_font(canvas, FontSecondary);
    size_t position = 0;
    PushToTalkMenuItemArray_it_t it;
    for(PushToTalkMenuItemArray_it(it, items); !PushToTalkMenuItemArray_end_p(it);
        PushToTalkMenuItemArray_next(it)) {
        const size_t item_position = position - model->window_position;
        const size_t items_on_screen = 3;
        uint8_t y_offset = 16;

        if(item_position < items_on_screen) {
            if(position == model->position) {
                canvas_set_color(canvas, ColorBlack);
                elements_slightly_rounded_box(
                    canvas,
                    0,
                    y_offset + (item_position * item_height) + 1,
                    item_width,
                    item_height - 2);
                canvas_set_color(canvas, ColorWhite);
            } else {
                canvas_set_color(canvas, ColorBlack);
            }

            FuriString* disp_str;
            disp_str = furi_string_alloc_set(PushToTalkMenuItemArray_cref(it)->label);
            const int32_t text_y = y_offset + (item_position * item_height) + item_height - 4;
            const int32_t row_left_x = 6;
            const int32_t row_right_x = item_width - 2;
            const size_t row_width = row_right_x - row_left_x;

            if((position == model->position) && model->hint_visible) {
                const char* hint_prefix = " | Long press";
                const char* hint_suffix = "for help";
                const int32_t icon_y = text_y - 8;

                const size_t label_w = canvas_string_width(canvas, furi_string_get_cstr(disp_str));
                const size_t prefix_w = canvas_string_width(canvas, hint_prefix);
                const size_t icon_w = 9;
                const size_t suffix_w = canvas_string_width(canvas, hint_suffix);

                const size_t group_w = label_w + 2 + prefix_w + 2 + icon_w + 2 + suffix_w;

                int32_t scroll_offset = 0;
                if(group_w > row_width) {
                    const size_t overflow = group_w - row_width;
                    const size_t cycle = overflow * 2;
                    const size_t step = cycle ? (model->hint_scroll_tick % cycle) : 0;
                    scroll_offset = (step <= overflow) ? step : (cycle - step);
                }

                const int32_t draw_x = row_left_x - scroll_offset;
                canvas_draw_str(canvas, draw_x, text_y, furi_string_get_cstr(disp_str));

                int32_t hint_x = draw_x + label_w + 2;
                canvas_draw_str(canvas, hint_x, text_y, hint_prefix);
                hint_x += prefix_w + 2;
                canvas_draw_icon(canvas, hint_x, icon_y, &I_Ok_btn_9x9);
                hint_x += icon_w + 2;
                canvas_draw_str(canvas, hint_x, text_y, hint_suffix);
            } else {
                elements_string_fit_width(canvas, disp_str, item_width - (6 * 2));
                canvas_draw_str(canvas, row_left_x, text_y, furi_string_get_cstr(disp_str));
            }

            furi_string_free(disp_str);
        }

        position++;
    }
    elements_scrollbar_pos(
        canvas, 128, 17, 46, model->position, PushToTalkMenuItemArray_size(items));
}

PushToTalkMenuList* hid_ptt_menu_get_list_at_index(void* context, uint32_t index) {
    furi_assert(context);
    HidPushToTalkMenuModel* model = context;
    for(int i = 0; i < model->lists_count; i++) {
        PushToTalkMenuList* list = &model->lists[i];
        if(index == list->index) {
            return list;
        }
    }
    return NULL;
}

static void hid_ptt_menu_draw_callback(Canvas* canvas, void* context) {
    furi_assert(context);
    HidPushToTalkMenuModel* model = context;
    if(model->lists_count == 0) {
        return;
    }
    uint8_t item_width = canvas_width(canvas) - 5;

    canvas_clear(canvas);
    canvas_set_font(canvas, FontPrimary);
    canvas_draw_str(canvas, 4, 11, "<");
    canvas_draw_str(canvas, 121, 11, ">");

    PushToTalkMenuList* list = &model->lists[model->list_position];
    FuriString* disp_str;
    disp_str = furi_string_alloc_set(list->label);
    elements_string_fit_width(canvas, disp_str, item_width - (6 * 2));
    uint8_t x_pos =
        (canvas_width(canvas) - canvas_string_width(canvas, furi_string_get_cstr(disp_str))) / 2;
    canvas_draw_str(canvas, x_pos, 11, furi_string_get_cstr(disp_str));
    furi_string_free(disp_str);
    canvas_set_font(canvas, FontSecondary);
    hid_ptt_menu_draw_list(canvas, context, list->items);
}

void ptt_menu_add_list(HidPushToTalkMenu* hid_ptt_menu, const char* label, uint32_t index) {
    furi_assert(label);
    furi_assert(hid_ptt_menu);
    with_view_model(
        hid_ptt_menu->view,
        HidPushToTalkMenuModel * model,
        {
            if(model->lists_count == 0) {
                model->lists = (PushToTalkMenuList*)malloc(sizeof(PushToTalkMenuList));
            } else {
                model->lists = (PushToTalkMenuList*)realloc(
                    model->lists, (model->lists_count + 1) * sizeof(PushToTalkMenuList));
            }
            if(model->lists == NULL) {
                FURI_LOG_E(TAG, "Memory reallocation failed (%i)", model->lists_count);
                return;
            }
            PushToTalkMenuList* list = &model->lists[model->lists_count];
            PushToTalkMenuItemArray_init(list->items);
            list->label = furi_string_alloc_set(label);
            list->index = index;
            model->lists_count += 1;
        },
        true);
}

void ptt_menu_add_item_to_list(
    HidPushToTalkMenu* hid_ptt_menu,
    uint32_t list_index,
    const char* label,
    uint32_t index,
    PushToTalkMenuItemCallback callback,
    void* callback_context) {
    PushToTalkMenuItem* item = NULL;
    furi_assert(label);
    furi_assert(hid_ptt_menu);
    UNUSED(list_index);
    with_view_model(
        hid_ptt_menu->view,
        HidPushToTalkMenuModel * model,
        {
            PushToTalkMenuList* list = hid_ptt_menu_get_list_at_index(model, list_index);
            if(list == NULL) {
                FURI_LOG_E(TAG, "Adding item %s to unknown index %li", label, list_index);
                return;
            }
            item = PushToTalkMenuItemArray_push_new(list->items);
            furi_string_set_str(item->label, label);
            item->index = index;
            item->callback = callback;
            item->callback_context = callback_context;
        },
        true);
}

void ptt_menu_shift_list(HidPushToTalkMenu* hid_ptt_menu, int shift) {
    size_t new_position = 0;
    uint32_t index = 0;
    with_view_model(
        hid_ptt_menu->view,
        HidPushToTalkMenuModel * model,
        {
            int new_list_position = (short)model->list_position + shift;
            if(new_list_position >= model->lists_count) {
                new_list_position = 0;
            } else if(new_list_position < 0) {
                new_list_position = model->lists_count - 1;
            }
            PushToTalkMenuList* list = &model->lists[model->list_position];
            PushToTalkMenuList* new_list = &model->lists[new_list_position];
            size_t new_window_position = model->window_position;
            const size_t items_size = PushToTalkMenuItemArray_size(new_list->items);
            size_t position = 0;
            // Find item index from current list
            PushToTalkMenuItemArray_it_t it;
            for(PushToTalkMenuItemArray_it(it, list->items); !PushToTalkMenuItemArray_end_p(it);
                PushToTalkMenuItemArray_next(it)) {
                if(position == model->position) {
                    index = PushToTalkMenuItemArray_cref(it)->index;
                    break;
                }
                position++;
            }
            // Try to find item with the same index in a new list
            position = 0;
            bool item_exists_in_new_list = false;
            for(PushToTalkMenuItemArray_it(it, new_list->items);
                !PushToTalkMenuItemArray_end_p(it);
                PushToTalkMenuItemArray_next(it)) {
                if(PushToTalkMenuItemArray_cref(it)->index == index) {
                    item_exists_in_new_list = true;
                    new_position = position;
                    break;
                }
                position++;
            }

            // This list item is not presented in a new list, let's try to keep position as is.
            // If it's out of range for the new list set it to the end
            if(!item_exists_in_new_list) {
                new_position = items_size - 1 < model->position ? items_size - 1 : model->position;
            }

            // Tune window position. As we have 3 items on screen, keep focus centered
            const size_t items_on_screen = 3;

            if(new_position >= items_size - 1) {
                if(items_size < items_on_screen + 1) {
                    new_window_position = 0;
                } else {
                    new_window_position = items_size - items_on_screen;
                }
            } else if(new_position < items_on_screen - 1) {
                new_window_position = 0;
            } else {
                new_window_position = new_position - 1;
            }
            model->list_position = new_list_position;
            model->position = new_position;
            model->window_position = new_window_position;
        },
        true);
}

void ptt_menu_process_up(HidPushToTalkMenu* hid_ptt_menu) {
    with_view_model(
        hid_ptt_menu->view,
        HidPushToTalkMenuModel * model,
        {
            PushToTalkMenuList* list = &model->lists[model->list_position];
            const size_t items_on_screen = 3;
            const size_t items_size = PushToTalkMenuItemArray_size(list->items);

            if(model->position > 0) {
                model->position--;
                if((model->position == model->window_position) && (model->window_position > 0)) {
                    model->window_position--;
                }
            } else {
                model->position = items_size - 1;
                if(model->position > items_on_screen - 1) {
                    model->window_position = model->position - (items_on_screen - 1);
                }
            }
        },
        true);
}

void ptt_menu_process_down(HidPushToTalkMenu* hid_ptt_menu) {
    with_view_model(
        hid_ptt_menu->view,
        HidPushToTalkMenuModel * model,
        {
            PushToTalkMenuList* list = &model->lists[model->list_position];
            const size_t items_on_screen = 3;
            const size_t items_size = PushToTalkMenuItemArray_size(list->items);

            if(model->position < items_size - 1) {
                model->position++;
                if((model->position - model->window_position > items_on_screen - 2) &&
                   (model->window_position < items_size - items_on_screen)) {
                    model->window_position++;
                }
            } else {
                model->position = 0;
                model->window_position = 0;
            }
        },
        true);
}

void ptt_menu_process_ok(HidPushToTalkMenu* hid_ptt_menu) {
    PushToTalkMenuList* list = NULL;
    PushToTalkMenuItem* item = NULL;
    with_view_model(
        hid_ptt_menu->view,
        HidPushToTalkMenuModel * model,
        {
            list = &model->lists[model->list_position];
            const size_t items_size = PushToTalkMenuItemArray_size(list->items);
            if(model->position < items_size) {
                item = PushToTalkMenuItemArray_get(list->items, model->position);
            }
        },
        true);
    if(item && list && item->callback) {
        item->callback(item->callback_context, list->index, list->label, item->index, item->label);
    }
}

void ptt_menu_process_long_ok(HidPushToTalkMenu* hid_ptt_menu) {
    PushToTalkMenuList* list = NULL;
    PushToTalkMenuItem* item = NULL;
    with_view_model(
        hid_ptt_menu->view,
        HidPushToTalkMenuModel * model,
        {
            list = &model->lists[model->list_position];
            const size_t items_size = PushToTalkMenuItemArray_size(list->items);
            if(model->position < items_size) {
                item = PushToTalkMenuItemArray_get(list->items, model->position);
            }
        },
        false);
    if(item && list && hid_ptt_menu->long_ok_callback) {
        hid_ptt_menu->long_ok_callback(
            hid_ptt_menu->long_ok_callback_context,
            list->index,
            list->label,
            item->index,
            item->label);
    }
}

void ptt_menu_set_long_ok_callback(
    HidPushToTalkMenu* hid_ptt_menu,
    PushToTalkMenuLongOkCallback callback,
    void* callback_context) {
    furi_assert(hid_ptt_menu);
    hid_ptt_menu->long_ok_callback = callback;
    hid_ptt_menu->long_ok_callback_context = callback_context;
}

static bool hid_ptt_menu_input_callback(InputEvent* event, void* context) {
    furi_assert(context);
    HidPushToTalkMenu* hid_ptt_menu = context;
    bool consumed = false;
    if(event->type == InputTypeShort) {
        switch(event->key) {
        case InputKeyUp:
            consumed = true;
            ptt_menu_process_up(hid_ptt_menu);
            break;
        case InputKeyDown:
            consumed = true;
            ptt_menu_process_down(hid_ptt_menu);
            break;
        case InputKeyLeft:
            consumed = true;
            ptt_menu_shift_list(hid_ptt_menu, -1);
            break;
        case InputKeyRight:
            consumed = true;
            ptt_menu_shift_list(hid_ptt_menu, +1);
            break;
        case InputKeyOk:
            consumed = true;
            ptt_menu_process_ok(hid_ptt_menu);
            break;
        default:
            break;
        }
    } else if(event->type == InputTypeRepeat) {
        if(event->key == InputKeyUp) {
            consumed = true;
            ptt_menu_process_up(hid_ptt_menu);
        } else if(event->key == InputKeyDown) {
            consumed = true;
            ptt_menu_process_down(hid_ptt_menu);
        }
    } else if(event->type == InputTypeLong && event->key == InputKeyOk) {
        consumed = true;
        ptt_menu_process_long_ok(hid_ptt_menu);
    }

    if(event->type != InputTypeRelease) {
        hid_ptt_menu_mark_interaction(hid_ptt_menu);
    }

    return consumed;
}

View* hid_ptt_menu_get_view(HidPushToTalkMenu* hid_ptt_menu) {
    furi_assert(hid_ptt_menu);
    return hid_ptt_menu->view;
}

HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* hid) {
    HidPushToTalkMenu* hid_ptt_menu = malloc(sizeof(HidPushToTalkMenu));
    hid_ptt_menu->hid = hid;
    hid_ptt_menu->view = view_alloc();
    view_set_context(hid_ptt_menu->view, hid_ptt_menu);
    view_allocate_model(hid_ptt_menu->view, ViewModelTypeLocking, sizeof(HidPushToTalkMenuModel));
    view_set_draw_callback(hid_ptt_menu->view, hid_ptt_menu_draw_callback);
    view_set_input_callback(hid_ptt_menu->view, hid_ptt_menu_input_callback);
    view_set_enter_callback(hid_ptt_menu->view, hid_ptt_menu_enter_callback);
    view_set_exit_callback(hid_ptt_menu->view, hid_ptt_menu_exit_callback);

    hid_ptt_menu->hint_timer =
        furi_timer_alloc(hid_ptt_menu_hint_timer_callback, FuriTimerTypePeriodic, hid_ptt_menu);

    with_view_model(
        hid_ptt_menu->view,
        HidPushToTalkMenuModel * model,
        {
            model->lists_count = 0;
            model->position = 0;
            model->window_position = 0;
            model->last_interaction_tick = furi_get_tick();
            model->hint_list_position = 0;
            model->hint_item_position = 0;
            model->hint_visible = false;
            model->hint_scroll_tick = 0;
        },
        true);
    return hid_ptt_menu;
}

void hid_ptt_menu_free(HidPushToTalkMenu* hid_ptt_menu) {
    furi_assert(hid_ptt_menu);
    furi_timer_stop(hid_ptt_menu->hint_timer);
    furi_timer_free(hid_ptt_menu->hint_timer);
    with_view_model(
        hid_ptt_menu->view,
        HidPushToTalkMenuModel * model,
        {
            for(int i = 0; i < model->lists_count; i++) {
                PushToTalkMenuItemArray_clear(model->lists[i].items);
                furi_string_free(model->lists[i].label);
            }
            free(model->lists);
        },
        true);
    view_free(hid_ptt_menu->view);
    free(hid_ptt_menu);
}
