/*

Wish list:

1.  variable_item_set_current_value_text allows for large text, but
it trucates it to X characters.  It would be nice it it scrolled.


*/

#include <furi.h>
#include <furi_hal.h>
#include <gui/gui.h>
#include <gui/view.h>
#include <gui/view_dispatcher.h>
#include <gui/modules/submenu.h>
#include <gui/modules/widget.h>
#include <gui/modules/variable_item_list.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include "rolling_flaws_icons.h"

#include <lib/subghz/receiver.h>
#include <lib/subghz/protocols/protocol_items.h>
#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
#include <lib/subghz/devices/devices.h>

#include "rolling_flaws_subghz_receive.h"
#include "rolling_flaws_settings.h"
#include "rolling_flaws_about.h"
#include "rolling_flaws_keeloq.h"
#include "rolling_flaws_send_keeloq.h"

#ifdef TAG
#undef TAG
#endif
#define TAG "RollingFlawsSubGHzApp"

// Comment this line if you don't want the backlight to be continuously on.
#define BACKLIGHT_ALWAYS_ON yes

typedef enum {
    RollingFlawsSubmenuIndexConfigure,
    RollingFlawsSubmenuIndexResetCountToZero,
    RollingFlawsSubmenuIndexTransmit,
    RollingFlawsSubmenuIndexReceive,
    RollingFlawsSubmenuIndexSyncRemote,
    RollingFlawsSubmenuIndexAbout,
} RollingFlawsSubmenuIndex;

typedef enum {
    RollingFlawsViewSubmenu,
    RollingFlawsViewConfigure,
    RollingFlawsViewReceiveSignals,
    RollingFlawsViewReceiveSync,
    RollingFlawsViewAbout,
} RollingFlawsView;

typedef enum {
    RollingFlawsEventIdReceivedSignal,
    RollingFlawsEventIdCycleSignal,
} RollingFlawsEventId;

static bool decode_packet(FuriString* buffer, void* ctx) {
    RollingFlaws* context = ctx;
    furi_hal_vibro_on(true);
    furi_delay_ms(50);
    furi_hal_vibro_on(false);

    if(furi_string_start_with_str(buffer, "KeeLoq 64bit")) {
        if(!furi_string_start_with_str(
               buffer, rolling_flaws_setting_protocol_base_name_get(context->model))) {
            FURI_LOG_I(TAG, "KeeLoq 64bit protocol is not enabled");
            return true;
        }
        decode_keeloq(context->model, buffer, false);
    } else {
        FURI_LOG_I(TAG, "Unknown protocol");
    }

    return true;
}

static bool sync_packet(FuriString* buffer, void* ctx) {
    RollingFlaws* context = ctx;
    if(furi_string_start_with_str(buffer, "KeeLoq 64bit")) {
        if(!furi_string_start_with_str(
               buffer, rolling_flaws_setting_protocol_base_name_get(context->model))) {
            FURI_LOG_I(TAG, "KeeLoq 64bit protocol is not enabled");
            return true;
        }
        decode_keeloq(context->model, buffer, true);
        view_dispatcher_send_custom_event(
            context->view_dispatcher, RollingFlawsEventIdReceivedSignal);
        return false;
    } else {
        FURI_LOG_I(TAG, "Unknown protocol");
    }

    return true;
}

/**
 * @brief      Callback for navigation events
 * @details    This function is called when user press back button.  We return VIEW_NONE to
 *            indicate that we want to exit the application.
 * @param      context  The context
 * @return     next view id
*/
uint32_t rolling_flaws_navigation_exit_callback(void* context) {
    UNUSED(context);
    return VIEW_NONE;
}

/**
 * @brief      Callback for navigation events
 * @details    This function is called when user press back button.  We return VIEW_NONE to
 *            indicate that we want to exit the application.
 * @param      context  The context
 * @return     next view id
*/
uint32_t rolling_flaws_navigation_submenu_callback(void* context) {
    UNUSED(context);

    return RollingFlawsViewSubmenu;
}

/**
 * @brief      Callback for navigation events
 * @details    This function is called when user press back button.  We return VIEW_NONE to
 *            indicate that we want to exit the application.
 * @param      context  The context
 * @return     next view id
*/
uint32_t rolling_flaws_navigation_submenu_stop_receiving_callback(void* context) {
    RollingFlaws* app = (RollingFlaws*)context;
    stop_listening(app->subghz);

    return RollingFlawsViewSubmenu;
}

uint32_t rolling_flaws_navigation_submenu_stop_sync_callback(void* context) {
    RollingFlaws* app = (RollingFlaws*)context;
    stop_listening(app->subghz);

    return RollingFlawsViewSubmenu;
}

bool rolling_flaws_view_dispatcher_custom_event_callback(void* context, uint32_t event) {
    FURI_LOG_I(TAG, "Custom event received: %ld", event);
    if(event == RollingFlawsEventIdReceivedSignal) {
        RollingFlaws* app = (RollingFlaws*)context;
        stop_listening(app->subghz);

        furi_hal_vibro_on(true);
        furi_delay_ms(200);
        furi_hal_vibro_on(false);
        furi_delay_ms(100);

        furi_hal_vibro_on(true);
        furi_delay_ms(100);
        furi_hal_vibro_on(false);

        view_dispatcher_switch_to_view(app->view_dispatcher, RollingFlawsViewSubmenu);
        return true;
    }

    if(event == RollingFlawsEventIdCycleSignal) {
        RollingFlaws* app = (RollingFlaws*)context;
        stop_listening(app->subghz);
        uint32_t frequency = rolling_flaws_setting_frequency_get(app->model);
        app->model->opened = false;
        start_listening(app->subghz, frequency, decode_packet, app);
    }

    return false;
}

void rolling_flaws_submenu_callback(void* context, uint32_t index) {
    RollingFlaws* app = (RollingFlaws*)context;

    switch(index) {
    case RollingFlawsSubmenuIndexConfigure:
        view_dispatcher_switch_to_view(app->view_dispatcher, RollingFlawsViewConfigure);
        break;
    case RollingFlawsSubmenuIndexResetCountToZero:
        app->model->count = 0x0;
        app->model->future_count = 0xFFFFFFFF;
        furi_hal_vibro_on(true);
        furi_delay_ms(200);
        furi_hal_vibro_on(false);
        break;
    case RollingFlawsSubmenuIndexTransmit:
        app->model->count++;
        app->model->future_count = 0xFFFFFFFF;
        send_keeloq_count(
            rolling_flaws_setting_fix_get(app->model),
            app->model->count - 2,
            rolling_flaws_setting_protocol_mf_name_get(app->model),
            rolling_flaws_setting_frequency_get(app->model));
        furi_hal_vibro_on(true);
        furi_delay_ms(100);
        furi_hal_vibro_on(false);
        furi_delay_ms(100);
        furi_hal_vibro_on(true);
        furi_delay_ms(200);
        furi_hal_vibro_on(false);
        break;
    case RollingFlawsSubmenuIndexReceive: {
        uint32_t frequency = rolling_flaws_setting_frequency_get(app->model);
        app->model->opened = false;
        start_listening(app->subghz, frequency, decode_packet, app);
        view_dispatcher_switch_to_view(app->view_dispatcher, RollingFlawsViewReceiveSignals);
    } break;
    case RollingFlawsSubmenuIndexSyncRemote: {
        uint32_t frequency = rolling_flaws_setting_frequency_get(app->model);
        start_listening(app->subghz, frequency, sync_packet, app);
        view_dispatcher_switch_to_view(app->view_dispatcher, RollingFlawsViewReceiveSync);
    } break;
    case RollingFlawsSubmenuIndexAbout:
        view_dispatcher_switch_to_view(app->view_dispatcher, RollingFlawsViewAbout);
        break;
    default:
        break;
    }
}

void rolling_flaws_receive_sync_draw_callback(Canvas* canvas, void* model) {
    UNUSED(model);
    canvas_set_font(canvas, FontPrimary);
    canvas_draw_str(canvas, 13, 30, "Syncing rolling code:");

    canvas_set_font(canvas, FontSecondary);
    canvas_draw_str(canvas, 13, 45, "Press remote button now.");
}

void rolling_flaws_receive_signal_draw_callback(Canvas* canvas, void* model) {
    RollingFlawsModel* my_model = ((RollingFlawsRefModel*)model)->model;

    FuriString* str = furi_string_alloc(32);

    canvas_set_bitmap_mode(canvas, 1);
    canvas_set_font(canvas, FontPrimary);
    canvas_draw_str(canvas, 13, 8, "Rolling code receiver");
    canvas_set_font(canvas, FontSecondary);
    furi_string_printf(str, "Count:   %04X", (uint16_t)my_model->count);
    canvas_draw_str(canvas, 2, 34, furi_string_get_cstr(str));
    canvas_set_font(canvas, FontSecondary);
    if(my_model->future_count > 0xFFFF) {
        canvas_draw_str(canvas, 2, 44, "Future:   none");
    } else {
        furi_string_printf(str, "Future:   %04X", (uint16_t)my_model->future_count);
        canvas_draw_str(canvas, 2, 44, furi_string_get_cstr(str));
    }
    canvas_set_font(canvas, FontSecondary);
    canvas_draw_str(canvas, 3, 20, rolling_flaws_setting_protocol_display_name_get(my_model));
    canvas_set_font(canvas, FontSecondary);
    furi_string_printf(str, "Fix: %08lX", rolling_flaws_setting_fix_get(my_model));
    canvas_draw_str(canvas, 2, 54, furi_string_get_cstr(str));
    canvas_set_font(canvas, FontSecondary);
    furi_string_printf(str, "RX: %s", furi_string_get_cstr(my_model->key));
    canvas_draw_str(canvas, 2, 64, furi_string_get_cstr(str));
    if(my_model->opened) {
        canvas_draw_icon(canvas, 100, 15, &I_Unlock_10x8);
        canvas_set_font(canvas, FontPrimary);
        canvas_draw_str(canvas, 82, 33, "OPENED!");
    } else {
        canvas_draw_icon(canvas, 100, 15, &I_Lock_10x8);
        canvas_set_font(canvas, FontPrimary);
        canvas_draw_str(canvas, 85, 33, "CLOSED");
    }

    canvas_set_font(canvas, FontSecondary);
    furi_string_printf(str, "%sMHz", rolling_flaws_setting_frequency_name_get(my_model));
    canvas_draw_str(canvas, 75, 43, furi_string_get_cstr(str));

    canvas_set_font(canvas, FontSecondary);
    canvas_draw_str(canvas, 82, 54, furi_string_get_cstr(my_model->status));

    furi_string_free(str);
}

bool rolling_flaws_view_input_ignore_callback(InputEvent* event, void* context) {
    UNUSED(event);
    UNUSED(context);

    return false;
}

bool rolling_flaws_view_input_callback(InputEvent* event, void* context) {
    RollingFlaws* app = (RollingFlaws*)context;
    RollingFlawsModel* my_model = app->model;

    FURI_LOG_I(TAG, "Input event received: %d", event->type);
    if(event->type == InputTypeShort) {
        FURI_LOG_I(TAG, "Input key: %d", event->key);
        if(event->key == InputKeyLeft) {
            if(my_model->count == 0) {
                my_model->count = 0xFFFF;
            } else {
                my_model->count--;
            }
            __gui_redraw();
            return true;
        }
        if(event->key == InputKeyRight) {
            if(my_model->count == 0xFFFF) {
                my_model->count = 0;
            } else {
                my_model->count++;
            }
            __gui_redraw();
            return true;
        }
        if(event->key == InputKeyOk) {
            my_model->opened = false;
            view_dispatcher_send_custom_event(
                app->view_dispatcher, RollingFlawsEventIdCycleSignal);
            __gui_redraw();
            return true;
        }
    }

    return false;
}

RollingFlaws* rolling_flaws_alloc() {
    RollingFlaws* app = (RollingFlaws*)malloc(sizeof(RollingFlaws));

    Gui* gui = furi_record_open(RECORD_GUI);

    app->subghz = rolling_flaws_subghz_alloc();

    app->model = malloc(sizeof(RollingFlawsModel));
    app->model->key = furi_string_alloc();
    app->model->custom_mf = furi_string_alloc();
    app->model->status = furi_string_alloc();
    app->model->custom_fix = 0x24321234;
    app->model->count = 0x0;
    app->model->future_count = 0xFFFFFFFF;
    app->model->opened = false;

    app->view_dispatcher = view_dispatcher_alloc();
    view_dispatcher_enable_queue(app->view_dispatcher);
    view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
    view_dispatcher_set_custom_event_callback(
        app->view_dispatcher, rolling_flaws_view_dispatcher_custom_event_callback);

    app->submenu = submenu_alloc();
    submenu_add_item(
        app->submenu,
        "Config",
        RollingFlawsSubmenuIndexConfigure,
        rolling_flaws_submenu_callback,
        app);
    submenu_add_item(
        app->submenu,
        "Reset count to 0",
        RollingFlawsSubmenuIndexResetCountToZero,
        rolling_flaws_submenu_callback,
        app);
    submenu_add_item(
        app->submenu,
        "Transmit Signal",
        RollingFlawsSubmenuIndexTransmit,
        rolling_flaws_submenu_callback,
        app);
    submenu_add_item(
        app->submenu,
        "Receive Signals",
        RollingFlawsSubmenuIndexReceive,
        rolling_flaws_submenu_callback,
        app);
    submenu_add_item(
        app->submenu,
        "Sync Remote",
        RollingFlawsSubmenuIndexSyncRemote,
        rolling_flaws_submenu_callback,
        app);
    submenu_add_item(
        app->submenu, "About", RollingFlawsSubmenuIndexAbout, rolling_flaws_submenu_callback, app);
    view_set_previous_callback(
        submenu_get_view(app->submenu), rolling_flaws_navigation_exit_callback);
    view_dispatcher_add_view(
        app->view_dispatcher, RollingFlawsViewSubmenu, submenu_get_view(app->submenu));
    view_dispatcher_switch_to_view(app->view_dispatcher, RollingFlawsViewSubmenu);

    rolling_flaw_populate_variable_item_list(app);
    view_set_previous_callback(
        variable_item_list_get_view(app->variable_item_list_config),
        rolling_flaws_navigation_submenu_callback);
    view_dispatcher_add_view(
        app->view_dispatcher,
        RollingFlawsViewConfigure,
        variable_item_list_get_view(app->variable_item_list_config));

    app->view_receive_signals = view_alloc();
    view_set_context(app->view_receive_signals, app);
    view_set_draw_callback(app->view_receive_signals, rolling_flaws_receive_signal_draw_callback);
    view_set_input_callback(app->view_receive_signals, rolling_flaws_view_input_callback);
    view_set_previous_callback(
        app->view_receive_signals, rolling_flaws_navigation_submenu_stop_receiving_callback);
    view_allocate_model(
        app->view_receive_signals, ViewModelTypeLockFree, sizeof(RollingFlawsRefModel));
    RollingFlawsRefModel* refmodel = view_get_model(app->view_receive_signals);
    refmodel->model = app->model;
    view_dispatcher_add_view(
        app->view_dispatcher, RollingFlawsViewReceiveSignals, app->view_receive_signals);

    app->view_receive_sync = view_alloc();
    view_set_context(app->view_receive_sync, app);
    view_set_draw_callback(app->view_receive_sync, rolling_flaws_receive_sync_draw_callback);
    view_set_input_callback(app->view_receive_sync, rolling_flaws_view_input_ignore_callback);
    view_set_previous_callback(
        app->view_receive_sync, rolling_flaws_navigation_submenu_stop_sync_callback);
    view_allocate_model(
        app->view_receive_sync, ViewModelTypeLockFree, sizeof(RollingFlawsRefModel));
    refmodel = view_get_model(app->view_receive_sync);
    refmodel->model = app->model;
    view_dispatcher_add_view(
        app->view_dispatcher, RollingFlawsViewReceiveSync, app->view_receive_sync);

    app->widget_about = widget_alloc();
    widget_add_text_scroll_element(app->widget_about, 0, 0, 128, 64, ROLLING_FLAWS_ABOUT_TEXT);
    view_set_previous_callback(
        widget_get_view(app->widget_about), rolling_flaws_navigation_submenu_callback);
    view_dispatcher_add_view(
        app->view_dispatcher, RollingFlawsViewAbout, widget_get_view(app->widget_about));

    app->notifications = furi_record_open(RECORD_NOTIFICATION);

#ifdef BACKLIGHT_ALWAYS_ON
    notification_message(app->notifications, &sequence_display_backlight_enforce_on);
#endif

    return app;
}

void rolling_flaws_free(RollingFlaws* app) {
#ifdef BACKLIGHT_ALWAYS_ON
    notification_message(app->notifications, &sequence_display_backlight_enforce_auto);
#endif
    furi_record_close(RECORD_NOTIFICATION);

    view_dispatcher_remove_view(app->view_dispatcher, RollingFlawsViewAbout);
    widget_free(app->widget_about);
    view_dispatcher_remove_view(app->view_dispatcher, RollingFlawsViewReceiveSignals);
    view_free(app->view_receive_signals);
    view_dispatcher_remove_view(app->view_dispatcher, RollingFlawsViewReceiveSync);
    view_free(app->view_receive_sync);
    view_dispatcher_remove_view(app->view_dispatcher, RollingFlawsViewConfigure);
    variable_item_list_free(app->variable_item_list_config);
    view_dispatcher_remove_view(app->view_dispatcher, RollingFlawsViewSubmenu);
    submenu_free(app->submenu);
    view_dispatcher_free(app->view_dispatcher);
    furi_record_close(RECORD_GUI);

    rolling_flaws_subghz_free(app->subghz);

    free(app);
}

int32_t rolling_flaws_app(void* p) {
    UNUSED(p);

    RollingFlaws* app = rolling_flaws_alloc();
    view_dispatcher_run(app->view_dispatcher);

    rolling_flaws_free(app);
    return 0;
}