#include "totp_scene_generate_token.h"
#include <gui/gui.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include <totp_icons.h>
#include <roll_value.h>
#include "../../../services/fonts/font_provider.h"
#include "../../canvas_extensions.h"
#include "../../../types/token_info.h"
#include "../../../types/common.h"
#include "../../constants.h"
#include "../../../services/config/config.h"
#include "../../scene_director.h"
#include "../../../config/app/config.h"
#include "../../../workers/generate_totp_code/generate_totp_code.h"
#include "../../../workers/usb_type_code/usb_type_code.h"
#ifdef TOTP_BADBT_AUTOMATION_ENABLED
#include "../../../workers/bt_type_code/bt_type_code.h"
#endif

#define PROGRESS_BAR_MARGIN (3)
#define PROGRESS_BAR_HEIGHT (4)
#define DIV_CEIL(a, b) (((a) + (b) - 1) / (b))

typedef struct {
    uint8_t progress_bar_x;
    uint8_t progress_bar_width;
    uint8_t code_total_length;
    uint8_t code_offset_x;
    uint8_t code_offset_y;
    uint8_t group_space_width;
} UiPrecalculatedDimensions;

typedef struct {
    char last_code[TokenDigitsCountMax + 1];
    TotpUsbTypeCodeWorkerContext* usb_type_code_worker_context;
    NotificationMessage const* notification_sequence_new_token[8];
    NotificationMessage const* notification_sequence_automation[11];
    FuriMutex* last_code_update_sync;
    TotpGenerateCodeWorkerContext* generate_code_worker_context;
    UiPrecalculatedDimensions ui_precalculated_dimensions;
    FontInfo* active_font;
    NotificationApp* notification_app;
} SceneState;

static const NotificationSequence*
    get_notification_sequence_new_token(const PluginState* plugin_state, SceneState* scene_state) {
    if(scene_state->notification_sequence_new_token[0] == NULL) {
        NotificationMessage const** sequence = &scene_state->notification_sequence_new_token[0];
        *(sequence++) = &message_display_backlight_on;
        *(sequence++) = &message_green_255;
        if(plugin_state->notification_method & NotificationMethodVibro) {
            *(sequence++) = &message_vibro_on;
        }

        if(plugin_state->notification_method & NotificationMethodSound) {
            *(sequence++) = &message_note_c5;
        }

        *(sequence++) = &message_delay_50;

        if(plugin_state->notification_method & NotificationMethodVibro) {
            *(sequence++) = &message_vibro_off;
        }

        if(plugin_state->notification_method & NotificationMethodSound) {
            *(sequence++) = &message_sound_off;
        }

        *(sequence++) = NULL;
    }

    return (NotificationSequence*)scene_state->notification_sequence_new_token;
}

static const NotificationSequence*
    get_notification_sequence_automation(const PluginState* plugin_state, SceneState* scene_state) {
    if(scene_state->notification_sequence_automation[0] == NULL) {
        NotificationMessage const** sequence = &scene_state->notification_sequence_automation[0];

        *(sequence++) = &message_blue_255;
        if(plugin_state->notification_method & NotificationMethodVibro) {
            *(sequence++) = &message_vibro_on;
        }

        if(plugin_state->notification_method & NotificationMethodSound) {
            *(sequence++) = &message_note_d5; //-V525
            *(sequence++) = &message_delay_50;
            *(sequence++) = &message_note_e4;
            *(sequence++) = &message_delay_50;
            *(sequence++) = &message_note_f3;
        }

        *(sequence++) = &message_delay_50;

        if(plugin_state->notification_method & NotificationMethodVibro) {
            *(sequence++) = &message_vibro_off;
        }

        if(plugin_state->notification_method & NotificationMethodSound) {
            *(sequence++) = &message_sound_off;
        }

        *(sequence++) = NULL;
    }

    return (NotificationSequence*)scene_state->notification_sequence_automation;
}

static void update_totp_params(PluginState* const plugin_state, size_t token_index) {
    SceneState* scene_state = (SceneState*)plugin_state->current_scene_state;
    TokenInfoIteratorContext* iterator_context =
        totp_config_get_token_iterator_context(plugin_state);
    if(totp_token_info_iterator_go_to(iterator_context, token_index)) {
        totp_generate_code_worker_notify(
            scene_state->generate_code_worker_context, TotpGenerateCodeWorkerEventForceUpdate);
    }
}

static void draw_totp_code(Canvas* const canvas, const PluginState* const plugin_state) {
    const SceneState* scene_state = plugin_state->current_scene_state;
    const TokenInfoIteratorContext* iterator_context =
        totp_config_get_token_iterator_context(plugin_state);
    uint8_t code_length = totp_token_info_iterator_get_current_token(iterator_context)->digits;

    if(plugin_state->split_token_into_groups > 0) {
        uint8_t offset_x = scene_state->ui_precalculated_dimensions.code_offset_x;
        uint8_t char_width = scene_state->active_font->char_info[0].width;
        uint8_t group_length = DIV_CEIL(code_length, plugin_state->split_token_into_groups);
        for(uint8_t i = 0; i < code_length; i += group_length) {
            canvas_draw_str_ex(
                canvas,
                offset_x,
                scene_state->ui_precalculated_dimensions.code_offset_y,
                &scene_state->last_code[i],
                MIN(group_length, code_length - i),
                scene_state->active_font);
            offset_x +=
                (group_length * (char_width + scene_state->active_font->space_width) +
                 scene_state->ui_precalculated_dimensions.group_space_width);
        }
    } else {
        canvas_draw_str_ex(
            canvas,
            scene_state->ui_precalculated_dimensions.code_offset_x,
            scene_state->ui_precalculated_dimensions.code_offset_y,
            scene_state->last_code,
            code_length,
            scene_state->active_font);
    }
}

static void on_new_token_code_generated(bool time_left, void* context) {
    PluginState* const plugin_state = context;
    const TokenInfoIteratorContext* iterator_context =
        totp_config_get_token_iterator_context(plugin_state);
    if(totp_token_info_iterator_get_total_count(iterator_context) == 0) {
        return;
    }

    SceneState* scene_state = plugin_state->current_scene_state;
    const TokenInfo* current_token = totp_token_info_iterator_get_current_token(iterator_context);

    uint8_t char_width = scene_state->active_font->char_info[0].width;
    uint8_t group_spaces =
        plugin_state->split_token_into_groups > 0 ? plugin_state->split_token_into_groups - 1 : 0;
    uint8_t group_space_width = char_width;
    scene_state->ui_precalculated_dimensions.code_total_length =
        current_token->digits * (char_width + scene_state->active_font->space_width);
    uint8_t rest_available_width =
        SCREEN_WIDTH -
        MIN(SCREEN_WIDTH, scene_state->ui_precalculated_dimensions.code_total_length);
    if(group_space_width * group_spaces > rest_available_width) {
        group_space_width = rest_available_width / group_spaces;
    }
    scene_state->ui_precalculated_dimensions.code_total_length += group_space_width * group_spaces;
    scene_state->ui_precalculated_dimensions.group_space_width = group_space_width;
    scene_state->ui_precalculated_dimensions.code_offset_x =
        (SCREEN_WIDTH -
         MIN(SCREEN_WIDTH, scene_state->ui_precalculated_dimensions.code_total_length)) >>
        1;
    scene_state->ui_precalculated_dimensions.code_offset_y =
        SCREEN_HEIGHT_CENTER - (scene_state->active_font->height >> 1);

    if(time_left) {
        notification_message(
            scene_state->notification_app,
            get_notification_sequence_new_token(plugin_state, scene_state));
    }

    totp_scene_director_force_redraw(plugin_state);
}

static void on_code_lifetime_updated_generated(float code_lifetime_percent, void* context) {
    PluginState* const plugin_state = context;
    SceneState* scene_state = plugin_state->current_scene_state;
    scene_state->ui_precalculated_dimensions.progress_bar_width =
        (uint8_t)((float)(SCREEN_WIDTH - (PROGRESS_BAR_MARGIN << 1)) * code_lifetime_percent);
    scene_state->ui_precalculated_dimensions.progress_bar_x =
        ((SCREEN_WIDTH - (PROGRESS_BAR_MARGIN << 1) -
          scene_state->ui_precalculated_dimensions.progress_bar_width) >>
         1) +
        PROGRESS_BAR_MARGIN;
    totp_scene_director_force_redraw(plugin_state);
}

void totp_scene_generate_token_activate(PluginState* plugin_state) {
    SceneState* scene_state = malloc(sizeof(SceneState));
    furi_check(scene_state != NULL);

    plugin_state->current_scene_state = scene_state;
    FURI_LOG_D(LOGGING_TAG, "Timezone set to: %f", (double)plugin_state->timezone_offset);

    scene_state->last_code_update_sync = furi_mutex_alloc(FuriMutexTypeNormal);
    if(plugin_state->automation_method & AutomationMethodBadUsb) {
        scene_state->usb_type_code_worker_context = totp_usb_type_code_worker_start(
            scene_state->last_code,
            TokenDigitsCountMax + 1,
            scene_state->last_code_update_sync,
            plugin_state->automation_kb_layout,
            plugin_state->automation_initial_delay);
    }

    scene_state->active_font = totp_font_info_alloc();

    if(!totp_font_provider_get_font(plugin_state->active_font_index, scene_state->active_font)) {
        totp_font_provider_get_font(0, scene_state->active_font);
    }
    scene_state->notification_app = furi_record_open(RECORD_NOTIFICATION);
    scene_state->notification_sequence_automation[0] = NULL;
    scene_state->notification_sequence_new_token[0] = NULL;

#ifdef TOTP_BADBT_AUTOMATION_ENABLED

    if(plugin_state->automation_method & AutomationMethodBadBt) {
        if(plugin_state->bt_type_code_worker_context == NULL) {
            plugin_state->bt_type_code_worker_context = totp_bt_type_code_worker_init(
                *((uint16_t*)plugin_state->crypto_settings.crypto_verify_data),
                plugin_state->bt_type_code_worker_profile_index);
        }
        totp_bt_type_code_worker_start(
            plugin_state->bt_type_code_worker_context,
            scene_state->last_code,
            TokenDigitsCountMax + 1,
            scene_state->last_code_update_sync,
            plugin_state->automation_kb_layout,
            plugin_state->automation_initial_delay);
    }
#endif
    const TokenInfoIteratorContext* iterator_context =
        totp_config_get_token_iterator_context(plugin_state);
    scene_state->generate_code_worker_context = totp_generate_code_worker_start(
        scene_state->last_code,
        totp_token_info_iterator_get_current_token(iterator_context),
        scene_state->last_code_update_sync,
        plugin_state->timezone_offset,
        &plugin_state->crypto_settings);

    totp_generate_code_worker_set_code_generated_handler(
        scene_state->generate_code_worker_context, &on_new_token_code_generated, plugin_state);

    totp_generate_code_worker_set_lifetime_changed_handler(
        scene_state->generate_code_worker_context,
        &on_code_lifetime_updated_generated,
        plugin_state);

    update_totp_params(
        plugin_state, totp_token_info_iterator_get_current_token_index(iterator_context));
}

void totp_scene_generate_token_render(Canvas* const canvas, PluginState* plugin_state) {
    const TokenInfoIteratorContext* iterator_context =
        totp_config_get_token_iterator_context(plugin_state);
    if(totp_token_info_iterator_get_total_count(iterator_context) == 0) {
        canvas_draw_str_aligned(
            canvas,
            SCREEN_WIDTH_CENTER,
            SCREEN_HEIGHT_CENTER - 10,
            AlignCenter,
            AlignCenter,
            "Token list is empty");
        canvas_draw_str_aligned(
            canvas,
            SCREEN_WIDTH_CENTER,
            SCREEN_HEIGHT_CENTER + 10,
            AlignCenter,
            AlignCenter,
            "Press OK button to open menu");
        return;
    }

    const SceneState* scene_state = (SceneState*)plugin_state->current_scene_state;

    canvas_set_font(canvas, FontPrimary);
    const TokenInfo* token_info = totp_token_info_iterator_get_current_token(iterator_context);
    const char* token_name_cstr = furi_string_get_cstr(token_info->name);
    uint16_t token_name_width = canvas_string_width(canvas, token_name_cstr);
    if(SCREEN_WIDTH - token_name_width > 18) {
        canvas_draw_str_aligned(
            canvas,
            SCREEN_WIDTH_CENTER,
            SCREEN_HEIGHT_CENTER - 20,
            AlignCenter,
            AlignCenter,
            token_name_cstr);
    } else {
        canvas_draw_str_aligned(
            canvas, 9, SCREEN_HEIGHT_CENTER - 20, AlignLeft, AlignCenter, token_name_cstr);
        canvas_set_color(canvas, ColorWhite);
        canvas_draw_box(canvas, 0, SCREEN_HEIGHT_CENTER - 24, 9, 9);
        canvas_draw_box(canvas, SCREEN_WIDTH - 10, SCREEN_HEIGHT_CENTER - 24, 9, 9);
        canvas_set_color(canvas, ColorBlack);
    }

    draw_totp_code(canvas, plugin_state);

    if(token_info->type == TokenTypeTOTP) {
        canvas_draw_box(
            canvas,
            scene_state->ui_precalculated_dimensions.progress_bar_x,
            SCREEN_HEIGHT - PROGRESS_BAR_MARGIN - PROGRESS_BAR_HEIGHT,
            scene_state->ui_precalculated_dimensions.progress_bar_width,
            PROGRESS_BAR_HEIGHT);
    } else {
        char buffer[21];
        snprintf(&buffer[0], sizeof(buffer), "%" PRIu64, token_info->counter);
        canvas_set_font(canvas, FontSecondary);
        canvas_draw_str_aligned(
            canvas, SCREEN_WIDTH_CENTER, SCREEN_HEIGHT - 5, AlignCenter, AlignCenter, buffer);
    }

    if(totp_token_info_iterator_get_total_count(iterator_context) > 1) {
        canvas_draw_icon(canvas, 0, SCREEN_HEIGHT_CENTER - 24, &I_totp_arrow_left_8x9);
        canvas_draw_icon(
            canvas, SCREEN_WIDTH - 8, SCREEN_HEIGHT_CENTER - 24, &I_totp_arrow_right_8x9);
    }

    if(plugin_state->automation_method & AutomationMethodBadUsb) {
        canvas_draw_icon(
            canvas,
#ifdef TOTP_BADBT_AUTOMATION_ENABLED
            SCREEN_WIDTH_CENTER -
                (plugin_state->automation_method & AutomationMethodBadBt ? 33 : 15),
#else
            SCREEN_WIDTH_CENTER - 15,
#endif

            SCREEN_HEIGHT_CENTER + 12,
            &I_hid_usb_31x9);
    }

#ifdef TOTP_BADBT_AUTOMATION_ENABLED
    if(plugin_state->automation_method & AutomationMethodBadBt &&
       plugin_state->bt_type_code_worker_context != NULL &&
       totp_bt_type_code_worker_is_advertising(plugin_state->bt_type_code_worker_context)) {
        canvas_draw_icon(
            canvas,
            SCREEN_WIDTH_CENTER +
                (plugin_state->automation_method & AutomationMethodBadUsb ? 2 : -15),
            SCREEN_HEIGHT_CENTER + 12,
            &I_hid_ble_31x9);
    }
#endif
}

bool totp_scene_generate_token_handle_event(
    const PluginEvent* const event,
    PluginState* plugin_state) {
    if(event->type != EventTypeKey) {
        return true;
    }

    if(event->input.type == InputTypePress && event->input.key == InputKeyBack) {
        return false;
    }

    SceneState* scene_state;
    if(event->input.type == InputTypeLong) {
        if(event->input.key == InputKeyDown &&
           plugin_state->automation_method & AutomationMethodBadUsb) {
            scene_state = (SceneState*)plugin_state->current_scene_state;
            const TokenInfoIteratorContext* iterator_context =
                totp_config_get_token_iterator_context(plugin_state);
            totp_usb_type_code_worker_notify(
                scene_state->usb_type_code_worker_context,
                TotpUsbTypeCodeWorkerEventType,
                totp_token_info_iterator_get_current_token(iterator_context)->automation_features);
            notification_message(
                scene_state->notification_app,
                get_notification_sequence_automation(plugin_state, scene_state));
            return true;
        }
#ifdef TOTP_BADBT_AUTOMATION_ENABLED
        else if(
            event->input.key == InputKeyUp &&
            plugin_state->automation_method & AutomationMethodBadBt) {
            scene_state = (SceneState*)plugin_state->current_scene_state;
            const TokenInfoIteratorContext* iterator_context =
                totp_config_get_token_iterator_context(plugin_state);
            totp_bt_type_code_worker_notify(
                plugin_state->bt_type_code_worker_context,
                TotpBtTypeCodeWorkerEventType,
                totp_token_info_iterator_get_current_token(iterator_context)->automation_features);
            notification_message(
                scene_state->notification_app,
                get_notification_sequence_automation(plugin_state, scene_state));
            return true;
        }
#endif
        else if(event->input.key == InputKeyOk) {
            TokenInfoIteratorContext* iterator_context =
                totp_config_get_token_iterator_context(plugin_state);
            const TokenInfo* token_info =
                totp_token_info_iterator_get_current_token(iterator_context);
            if(token_info->type == TokenTypeHOTP) {
                scene_state = (SceneState*)plugin_state->current_scene_state;
                totp_token_info_iterator_current_token_inc_counter(iterator_context);
                totp_generate_code_worker_notify(
                    scene_state->generate_code_worker_context,
                    TotpGenerateCodeWorkerEventForceUpdate);
                notification_message(
                    scene_state->notification_app,
                    get_notification_sequence_new_token(plugin_state, scene_state));
            }
        }
    } else if(event->input.type == InputTypePress || event->input.type == InputTypeRepeat) {
        switch(event->input.key) {
        case InputKeyUp:
            break;
        case InputKeyDown:
            break;
        case InputKeyRight: {
            const TokenInfoIteratorContext* iterator_context =
                totp_config_get_token_iterator_context(plugin_state);
            size_t current_token_index =
                totp_token_info_iterator_get_current_token_index(iterator_context);
            totp_roll_value_size_t(
                &current_token_index,
                1,
                0,
                totp_token_info_iterator_get_total_count(iterator_context) - 1,
                RollOverflowBehaviorRoll);

            update_totp_params(plugin_state, current_token_index);
            break;
        }
        case InputKeyLeft: {
            const TokenInfoIteratorContext* iterator_context =
                totp_config_get_token_iterator_context(plugin_state);
            size_t current_token_index =
                totp_token_info_iterator_get_current_token_index(iterator_context);
            totp_roll_value_size_t(
                &current_token_index,
                -1,
                0,
                totp_token_info_iterator_get_total_count(iterator_context) - 1,
                RollOverflowBehaviorRoll);

            update_totp_params(plugin_state, current_token_index);
            break;
        }
        case InputKeyOk:
            break;
        case InputKeyBack:
            break;
        default:
            break;
        }
    } else if(event->input.type == InputTypeShort && event->input.key == InputKeyOk) {
        totp_scene_director_activate_scene(plugin_state, TotpSceneTokenMenu);
    }

    return true;
}

void totp_scene_generate_token_deactivate(PluginState* plugin_state) {
    if(plugin_state->current_scene_state == NULL) return;
    SceneState* scene_state = (SceneState*)plugin_state->current_scene_state;

    totp_generate_code_worker_stop(scene_state->generate_code_worker_context);

    furi_record_close(RECORD_NOTIFICATION);

    if(plugin_state->automation_method & AutomationMethodBadUsb) {
        totp_usb_type_code_worker_stop(scene_state->usb_type_code_worker_context);
    }
#ifdef TOTP_BADBT_AUTOMATION_ENABLED
    if(plugin_state->automation_method & AutomationMethodBadBt) {
        totp_bt_type_code_worker_stop(plugin_state->bt_type_code_worker_context);
    }
#endif

    furi_mutex_free(scene_state->last_code_update_sync);

    totp_font_info_free(scene_state->active_font);

    free(scene_state);
    plugin_state->current_scene_state = NULL;
}
