#include <gtk/gtk.h>
#include <gdk/gdkx.h>
#include <X11/Xlib.h>
#include <X11/XKBlib.h>
#include <X11/cursorfont.h>
#include <assert.h>
#include <string>
#include <pulse/pulseaudio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/prctl.h>
#include <fcntl.h>
#include <pwd.h>

typedef struct {
    Display *display;
    GtkButton *select_window_button;
    Window selected_window;
} SelectWindowUserdata;

typedef struct {
    GtkApplication *app;
    GtkStack *stack;
    GtkWidget *common_settings_page;
    GtkWidget *replay_page;
    GtkWidget *recording_page;
    GtkWidget *streaming_page;
} PageNavigationUserdata;

static SelectWindowUserdata select_window_userdata;
static PageNavigationUserdata page_navigation_userdata;
static GtkWidget *save_icon;
static Cursor crosshair_cursor;
static GtkSpinButton *fps_entry;
static GtkComboBoxText *audio_input_menu;
static GtkComboBoxText *stream_service_input_menu;
static GtkButton *file_chooser_button;
static GtkButton *directory_chooser_button;
static GtkButton *stream_button;
static GtkButton *record_button;
static GtkButton *replay_button;
static GtkButton *replay_back_button;
static GtkButton *record_back_button;
static GtkButton *stream_back_button;
static GtkButton *start_recording_button;
static GtkButton *start_replay_button;
static GtkButton *start_streaming_button;
static GtkEntry *stream_id_entry;
static GtkSpinButton *replay_time_entry;
static bool replaying = false;
static bool recording = false;
static bool streaming = false;
static std::string replay_output_path;
static pid_t gpu_screen_recorder_process = -1;
static pid_t ffmpeg_process = -1;

static const char* get_home_dir() {
    const char *home_dir = getenv("HOME");
    if(!home_dir) {
        passwd *pw = getpwuid(getuid());
        home_dir = pw->pw_dir;
    }
    return home_dir;
}

static std::string get_date_str() {
    char str[128];
    time_t now = time(NULL);
    struct tm *t = localtime(&now);
    strftime(str, sizeof(str)-1, "%Y-%m-%d_%H-%M-%S", t);
    return str;
}

static void enable_stream_record_button_if_info_filled() {
    if(gtk_combo_box_get_active(GTK_COMBO_BOX(audio_input_menu)) == -1)
        return;

    if(select_window_userdata.selected_window == None)
        return;
    
    gtk_widget_set_sensitive(GTK_WIDGET(replay_button), true);
    gtk_widget_set_sensitive(GTK_WIDGET(record_button), true);
    gtk_widget_set_sensitive(GTK_WIDGET(stream_button), true);
}

/* TODO: Look at xwininfo source to figure out how to make this work for different types of window managers */
static GdkFilterReturn filter_callback(GdkXEvent *xevent, GdkEvent *event, gpointer userdata) {
    SelectWindowUserdata *select_window_userdata = (SelectWindowUserdata*)userdata;
    XEvent *ev = (XEvent*)xevent;
    //assert(ev->type == ButtonPress);
    if(ev->type != ButtonPress)
        return GDK_FILTER_CONTINUE;

    Window target_win = ev->xbutton.subwindow;

    if(target_win == None) {
        fprintf(stderr, "Selected the root window! (no window selected)\n");
        return GDK_FILTER_CONTINUE;
    }

    int status = XUngrabPointer(select_window_userdata->display, CurrentTime);
    if(!status) {
        fprintf(stderr, "failed to ungrab pointer!\n");
        exit(1);
    }

    std::string window_name = "Window ID: " + std::to_string(target_win) + " | Name: ";
    XTextProperty wm_name_prop;
    if(XGetWMName(select_window_userdata->display, target_win, &wm_name_prop) && wm_name_prop.nitems > 0) {
        char **list_return = NULL;
        int num_items = 0;
        int ret = XmbTextPropertyToTextList(select_window_userdata->display, &wm_name_prop, &list_return, &num_items);
        if((ret == Success || ret > 0) && list_return) {
            for(int i = 0; i < num_items; ++i) {
                window_name += list_return[i];
            }
            XFreeStringList(list_return);
        } else {
            window_name += (char*)wm_name_prop.value;
        }
    } else {
        window_name += "(no name)";
    }

    fprintf(stderr, "window name: %s\n", window_name.c_str());
    gtk_button_set_label(select_window_userdata->select_window_button, window_name.c_str());
    select_window_userdata->selected_window = target_win;

    GdkScreen *screen = gdk_screen_get_default();
    GdkWindow *root_window = gdk_screen_get_root_window(screen);
    gdk_window_remove_filter(root_window, filter_callback, select_window_userdata);

    enable_stream_record_button_if_info_filled();

    return GDK_FILTER_REMOVE;
}

static gboolean on_select_window_button_click(GtkButton *button, gpointer userdata) {
    GtkApplication *app = (GtkApplication*)userdata;
    Display *display = gdk_x11_get_default_xdisplay();
    select_window_userdata.display = display;
    select_window_userdata.select_window_button = button;
    select_window_userdata.selected_window = None;

    GdkScreen *screen = gdk_screen_get_default();
    GdkWindow *root_window = gdk_screen_get_root_window(screen);
    gdk_window_set_events(root_window, GDK_BUTTON_PRESS_MASK);
    gdk_window_add_filter(root_window, filter_callback, &select_window_userdata);

    Window root = GDK_WINDOW_XID(root_window);

    /* Grab the pointer using target cursor, letting it room all over */
    int status = XGrabPointer(display, root, False,
                ButtonPressMask, GrabModeAsync,
                GrabModeAsync, root, crosshair_cursor, CurrentTime);
    if (status != GrabSuccess) {
        fprintf(stderr, "failed to grab pointer!\n");
        //GNotification *notification = g_notification_new("Failed to grab pointer to window selection!");
        //g_notification_set_priority(notification, G_NOTIFICATION_PRIORITY_HIGH);
        //g_application_send_notification(app, "select-window", notification);
    }

    return true;
}

static gboolean on_start_replay_click(GtkButton *button, gpointer userdata) {
    PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata;
    gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->replay_page);
    return true;
}

static gboolean on_start_recording_click(GtkButton *button, gpointer userdata) {
    PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata;
    gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->recording_page);

    std::string video_filepath = get_home_dir();
    video_filepath += "/Videos/Video_" + get_date_str() + ".mp4";
    gtk_button_set_label(file_chooser_button, video_filepath.c_str());
    return true;
}

static gboolean on_start_streaming_click(GtkButton *button, gpointer userdata) {
    PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata;
    gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->streaming_page);
    return true;
}

static gboolean on_streaming_recording_page_back_click(GtkButton *button, gpointer userdata) {
    PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata;
    gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->common_settings_page);
    return true;
}

static gboolean on_file_chooser_button_click(GtkButton *button, gpointer userdata) {
    GtkWidget *file_chooser_dialog = gtk_file_chooser_dialog_new("Where do you want to save the video?", nullptr, GTK_FILE_CHOOSER_ACTION_SAVE,
        "Cancel", GTK_RESPONSE_CANCEL,
        "Save", GTK_RESPONSE_ACCEPT,
        nullptr);
    gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(file_chooser_dialog), "video.mp4");

    int res = gtk_dialog_run(GTK_DIALOG(file_chooser_dialog));
    if(res == GTK_RESPONSE_ACCEPT) {
        char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(file_chooser_dialog));
        printf("filename: %s\n", filename);
        gtk_button_set_label(button, filename);
        g_free(filename);
    }
    gtk_widget_destroy(file_chooser_dialog);
    return true;
}

static gboolean on_directory_chooser_button_click(GtkButton *button, gpointer userdata) {
    GtkWidget *file_chooser_dialog = gtk_file_chooser_dialog_new("Where do you want to save the replays?", nullptr, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
        "Cancel", GTK_RESPONSE_CANCEL,
        "Save", GTK_RESPONSE_ACCEPT,
        nullptr);
    //gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(file_chooser_dialog), "video.mp4");

    int res = gtk_dialog_run(GTK_DIALOG(file_chooser_dialog));
    if(res == GTK_RESPONSE_ACCEPT) {
        char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(file_chooser_dialog));
        printf("directory: %s\n", filename);
        gtk_button_set_label(button, filename);
        g_free(filename);
    }
    gtk_widget_destroy(file_chooser_dialog);
    return true;
}

/* TODO: XSetErrorHandler to prevent program crash, show notifications or inline error message instead of fprintf(stderr), more validations... */

static gboolean on_start_replay_button_click(GtkButton *button, gpointer userdata) {
    GtkApplication *app = (GtkApplication*)userdata;

    if(replaying) {
        if(gpu_screen_recorder_process != -1) {
            kill(gpu_screen_recorder_process, SIGINT);
            int status;
            if(waitpid(gpu_screen_recorder_process, &status, 0) == -1) {
                perror("waitpid failed");
                /* Ignore... */
            }
        }
        gtk_button_set_label(button, "Start replay");
        replaying = false;
        gpu_screen_recorder_process = -1;
        gtk_widget_set_sensitive(GTK_WIDGET(replay_back_button), true);

        GNotification *notification = g_notification_new("GPU Screen Recorder");
        std::string notification_body = "Replay saved to " + replay_output_path;
        g_notification_set_body(notification, notification_body.c_str());
        g_notification_set_priority(notification, G_NOTIFICATION_PRIORITY_LOW);
        g_application_send_notification(&app->parent, "gpu-screen-recorder-replay", notification);
        return true;
    }

    const gchar *dir = gtk_button_get_label(directory_chooser_button);
    int fps = gtk_spin_button_get_value_as_int(fps_entry);
    int replay_time = gtk_spin_button_get_value_as_int(replay_time_entry);

    gchar* audio_input_str = gtk_combo_box_text_get_active_text(audio_input_menu);

    if(select_window_userdata.selected_window == None) {
        fprintf(stderr, "No window selected!\n");
        return true;
    }

    replaying = true;
    gtk_widget_set_sensitive(GTK_WIDGET(replay_back_button), false);

    std::string window_str = std::to_string(select_window_userdata.selected_window);
    std::string fps_str = std::to_string(fps);
    std::string replay_time_str = std::to_string(replay_time);

    replay_output_path = dir;
    replay_output_path += "/Video_" + get_date_str() + ".mp4";

    pid_t parent_pid = getpid();
    pid_t pid = fork();
    if(pid == -1) {
        perror("failed to fork");
        exit(3);
    } else if(pid == 0) { /* child process */
        if(prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) {
            perror("prctl(PR_SET_PDEATHSIG, SIGTERM) failed");
            exit(3);
        }

        if(getppid() != parent_pid)
            exit(3);
        
        if(audio_input_str && strcmp(audio_input_str, "None") != 0) {
            const char *args[] = { "gpu-screen-recorder", "-w", window_str.c_str(), "-c", "mp4", "-f", fps_str.c_str(), "-a", audio_input_str, "-r", replay_time_str.c_str(), "-o", replay_output_path.c_str(), NULL };
            execvp(args[0], (char* const*)args);
        } else {
            const char *args[] = { "gpu-screen-recorder", "-w", window_str.c_str(), "-c", "mp4", "-f", fps_str.c_str(), "-r", replay_time_str.c_str(), "-o", replay_output_path.c_str(), NULL };
            execvp(args[0], (char* const*)args);
        }
        perror("failed to launch gpu-screen-recorder");
    } else { /* parent process */
        gpu_screen_recorder_process = pid;
        gtk_button_set_label(button, "Stop replaying and save");
    }

    g_free(audio_input_str);
    return true;
}

static gboolean on_start_recording_button_click(GtkButton *button, gpointer userdata) {
    if(recording) {
        if(gpu_screen_recorder_process != -1) {
            kill(gpu_screen_recorder_process, SIGINT);
            int status;
            if(waitpid(gpu_screen_recorder_process, &status, 0) == -1) {
                perror("waitpid failed");
                /* Ignore... */
            }
        }
        gtk_button_set_label(button, "Start recording");
        recording = false;
        gpu_screen_recorder_process = -1;
        gtk_widget_set_sensitive(GTK_WIDGET(record_back_button), true);

        std::string video_filepath = get_home_dir();
        video_filepath += "/Videos/Video_" + get_date_str() + ".mp4";
        gtk_button_set_label(file_chooser_button, video_filepath.c_str());
        return true;
    }

    const gchar *filename = gtk_button_get_label(file_chooser_button);
    int fps = gtk_spin_button_get_value_as_int(fps_entry);

    gchar* audio_input_str = gtk_combo_box_text_get_active_text(audio_input_menu);

    if(select_window_userdata.selected_window == None) {
        fprintf(stderr, "No window selected!\n");
        return true;
    }

    recording = true;
    gtk_widget_set_sensitive(GTK_WIDGET(record_back_button), false);

    std::string window_str = std::to_string(select_window_userdata.selected_window);
    std::string fps_str = std::to_string(fps);

    pid_t parent_pid = getpid();
    pid_t pid = fork();
    if(pid == -1) {
        perror("failed to fork");
        exit(3);
    } else if(pid == 0) { /* child process */
        if(prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) {
            perror("prctl(PR_SET_PDEATHSIG, SIGTERM) failed");
            exit(3);
        }

        if(getppid() != parent_pid)
            exit(3);
        
        if(audio_input_str && strcmp(audio_input_str, "None") != 0) {
            const char *args[] = { "gpu-screen-recorder", "-w", window_str.c_str(), "-c", "mp4", "-f", fps_str.c_str(), "-a", audio_input_str, "-o", filename, NULL };
            execvp(args[0], (char* const*)args);
        } else {
            const char *args[] = { "gpu-screen-recorder", "-w", window_str.c_str(), "-c", "mp4", "-f", fps_str.c_str(), "-o", filename, NULL };
            execvp(args[0], (char* const*)args);
        }
        perror("failed to launch gpu-screen-recorder");
    } else { /* parent process */
        gpu_screen_recorder_process = pid;
        gtk_button_set_label(button, "Stop recording");
    }

    g_free(audio_input_str);
    return true;
}

#define PIPE_READ_END 0
#define PIPE_WRITE_END 1

static pid_t launch_ffmpeg_rtmp_process(const char *url, int *pipe_write_end) {
    int pipes[2];
    if(pipe(pipes) == -1) {
        perror("failed to create pipe");
        exit(3);
    }

    pid_t parent_pid = getpid();
    pid_t pid = fork();
    if(pid == -1) {
        perror("failed to fork");
        exit(3);
    } else if(pid == 0) { /* child process */
        if(prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) {
            perror("prctl(PR_SET_PDEATHSIG, SIGTERM) failed");
            exit(3);
        }

        if(getppid() != parent_pid)
            exit(3);

        dup2(pipes[PIPE_READ_END], STDIN_FILENO);
        close(pipes[PIPE_WRITE_END]);
        
        const char *args[] = { "ffmpeg", "-i", "pipe:0", "-c:v", "copy", "-f", "flv", "--", url, NULL };
        execvp(args[0], (char* const*)args);
        perror("failed to launch ffmpeg");
        exit(127);
    } else { /* parent process */
        *pipe_write_end = pipes[PIPE_WRITE_END];
        close(pipes[PIPE_READ_END]);
    }

    return pid;
}

static gboolean on_start_streaming_button_click(GtkButton *button, gpointer userdata) {
    if(streaming) {
        if(gpu_screen_recorder_process != -1) {
            kill(gpu_screen_recorder_process, SIGINT);
            int status;
            if(waitpid(gpu_screen_recorder_process, &status, 0) == -1) {
                perror("waitpid failed");
                /* Ignore... */
            }
        }

        if(ffmpeg_process != -1)
            kill(ffmpeg_process, SIGKILL);

        gtk_button_set_label(button, "Start streaming");
        streaming = false;
        gpu_screen_recorder_process = -1;
        ffmpeg_process = -1;
        gtk_widget_set_sensitive(GTK_WIDGET(stream_back_button), true);
        return true;
    }

    const char *stream_id_str = gtk_entry_get_text(stream_id_entry);
    int fps = gtk_spin_button_get_value_as_int(fps_entry);

    gchar* audio_input_str = gtk_combo_box_text_get_active_text(audio_input_menu);

    if(select_window_userdata.selected_window == None) {
        fprintf(stderr, "No window selected!\n");
        return true;
    }

    streaming = true;
    gtk_widget_set_sensitive(GTK_WIDGET(stream_back_button), false);

    std::string window_str = std::to_string(select_window_userdata.selected_window);
    std::string fps_str = std::to_string(fps);

    int pipe_write_end;
    std::string stream_url = "rtmp://live.twitch.tv/app/";
    stream_url += stream_id_str;
    ffmpeg_process = launch_ffmpeg_rtmp_process(stream_url.c_str(), &pipe_write_end);

    pid_t parent_pid = getpid();
    pid_t pid = fork();
    if(pid == -1) {
        perror("failed to fork");
        exit(3);
    } else if(pid == 0) { /* child process */
        if(prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) {
            perror("prctl(PR_SET_PDEATHSIG, SIGTERM) failed");
            exit(3);
        }

        if(getppid() != parent_pid)
            exit(3);

        // Redirect stdout to output_file
        dup2(pipe_write_end, STDOUT_FILENO);
        
        if(audio_input_str && strcmp(audio_input_str, "None") != 0) {
            const char *args[] = { "gpu-screen-recorder", "-w", window_str.c_str(), "-c", "flv", "-f", fps_str.c_str(), "-a", audio_input_str, NULL };
            execvp(args[0], (char* const*)args);
        } else {
            const char *args[] = { "gpu-screen-recorder", "-w", window_str.c_str(), "-c", "flv", "-f", fps_str.c_str(), NULL };
            execvp(args[0], (char* const*)args);
        }
        perror("failed to launch gpu-screen-recorder");
        exit(127);
    } else { /* parent process */
        gpu_screen_recorder_process = pid;
        close(pipe_write_end);
        gtk_button_set_label(button, "Stop streaming");
    }

    g_free(audio_input_str);
    return true;
}

static void gtk_widget_set_margin(GtkWidget *widget, int top, int bottom, int left, int right) {
    gtk_widget_set_margin_top(widget, top);
    gtk_widget_set_margin_bottom(widget, bottom);
    gtk_widget_set_margin_start(widget, left);
    gtk_widget_set_margin_end(widget, right);
}

static void pa_state_cb(pa_context *c, void *userdata) {
    pa_context_state state = pa_context_get_state(c);
    int *pa_ready = (int*)userdata;
    switch(state) {
        case PA_CONTEXT_UNCONNECTED:
        case PA_CONTEXT_CONNECTING:
        case PA_CONTEXT_AUTHORIZING:
        case PA_CONTEXT_SETTING_NAME:
        default:
            break;
        case PA_CONTEXT_FAILED:
        case PA_CONTEXT_TERMINATED:
            *pa_ready = 2;
            break;
        case PA_CONTEXT_READY:
            *pa_ready = 1;
            break;
    }
}

static void pa_sourcelist_cb(pa_context *ctx, const pa_source_info *source_info, int eol, void *userdata) {
    if(eol > 0)
        return;

    gtk_combo_box_text_append_text(audio_input_menu, source_info->name);
}

static void populate_audio_input_menu_with_pulseaudio_monitors() {
    pa_mainloop *main_loop = pa_mainloop_new();

    pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder-gtk");
    pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL);
    int state = 0;
    int pa_ready = 0;
    pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready);

    pa_operation *pa_op = NULL;

    for(;;) {
        // Not ready
        if(pa_ready == 0) {
            pa_mainloop_iterate(main_loop, 1, NULL);
            continue;
        }

        switch(state) {
            case 0: {
                pa_op = pa_context_get_source_info_list(ctx, pa_sourcelist_cb, nullptr);
                ++state;
                break;
            }
        }

        // Couldn't get connection to the server
        if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) {
            if(pa_op)
                pa_operation_unref(pa_op);
            pa_context_disconnect(ctx);
            pa_context_unref(ctx);
            pa_mainloop_free(main_loop);
            return;
        }

        pa_mainloop_iterate(main_loop, 1, NULL);
    }

    pa_mainloop_free(main_loop);
}

static void audio_input_change_callback(GtkComboBox *widget, gpointer userdata) {
    enable_stream_record_button_if_info_filled();
}

static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *app) {
    GtkGrid *grid = GTK_GRID(gtk_grid_new());
    gtk_stack_add_named(stack, GTK_WIDGET(grid), "common-settings");
    gtk_widget_set_vexpand(GTK_WIDGET(grid), true);
    gtk_widget_set_hexpand(GTK_WIDGET(grid), true);
    gtk_grid_set_row_spacing(grid, 10);
    gtk_grid_set_column_spacing(grid, 10);
    gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10);

    GtkGrid *select_window_grid = GTK_GRID(gtk_grid_new());
    gtk_grid_attach(grid, GTK_WIDGET(select_window_grid), 0, 0, 2, 1);
    gtk_grid_attach(select_window_grid, gtk_label_new("Window: "), 0, 0, 1, 1);
    GtkButton *select_window_button = GTK_BUTTON(gtk_button_new_with_label("Select window..."));
    gtk_widget_set_hexpand(GTK_WIDGET(select_window_button), true);
    g_signal_connect(select_window_button, "clicked", G_CALLBACK(on_select_window_button_click), app);
    gtk_grid_attach(select_window_grid, GTK_WIDGET(select_window_button), 1, 0, 1, 1);

    GtkGrid *fps_grid = GTK_GRID(gtk_grid_new());
    gtk_grid_attach(grid, GTK_WIDGET(fps_grid), 0, 1, 2, 1);
    gtk_grid_attach(fps_grid, gtk_label_new("Frame rate: "), 0, 0, 1, 1);
    fps_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(10.0, 250.0, 1.0));
    gtk_spin_button_set_value(fps_entry, 60.0);
    gtk_widget_set_hexpand(GTK_WIDGET(fps_entry), true);
    gtk_grid_attach(fps_grid, GTK_WIDGET(fps_entry), 1, 0, 1, 1);

    GtkGrid *audio_grid = GTK_GRID(gtk_grid_new());
    gtk_grid_attach(grid, GTK_WIDGET(audio_grid), 0, 2, 2, 1);
    gtk_grid_attach(audio_grid, gtk_label_new("Audio input: "), 0, 0, 1, 1);
    audio_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new());
    gtk_combo_box_text_append_text(audio_input_menu, "None");
    populate_audio_input_menu_with_pulseaudio_monitors();
    g_signal_connect(audio_input_menu, "changed", G_CALLBACK(audio_input_change_callback), nullptr);
    gtk_widget_set_hexpand(GTK_WIDGET(audio_input_menu), true);
    gtk_grid_attach(audio_grid, GTK_WIDGET(audio_input_menu), 1, 0, 1, 1);
    gtk_combo_box_set_active(GTK_COMBO_BOX(audio_input_menu), 0);

    GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new());
    gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, 3, 2, 1);
    gtk_grid_set_column_spacing(start_button_grid, 10);

    stream_button = GTK_BUTTON(gtk_button_new_with_label("Stream"));
    gtk_widget_set_hexpand(GTK_WIDGET(stream_button), true);
    gtk_grid_attach(start_button_grid, GTK_WIDGET(stream_button), 0, 0, 1, 1);

    record_button = GTK_BUTTON(gtk_button_new_with_label("Record"));
    gtk_widget_set_hexpand(GTK_WIDGET(record_button), true);
    gtk_grid_attach(start_button_grid, GTK_WIDGET(record_button), 1, 0, 1, 1);

    replay_button = GTK_BUTTON(gtk_button_new_with_label("Replay"));
    gtk_widget_set_hexpand(GTK_WIDGET(replay_button), true);
    gtk_grid_attach(start_button_grid, GTK_WIDGET(replay_button), 2, 0, 1, 1);

    gtk_widget_set_sensitive(GTK_WIDGET(replay_button), false);
    gtk_widget_set_sensitive(GTK_WIDGET(record_button), false);
    gtk_widget_set_sensitive(GTK_WIDGET(stream_button), false);

    return GTK_WIDGET(grid);
}

static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) {
    std::string video_filepath = get_home_dir();
    video_filepath += "/Videos";

    GtkGrid *grid = GTK_GRID(gtk_grid_new());
    gtk_stack_add_named(stack, GTK_WIDGET(grid), "replay");
    gtk_widget_set_vexpand(GTK_WIDGET(grid), true);
    gtk_widget_set_hexpand(GTK_WIDGET(grid), true);
    gtk_grid_set_row_spacing(grid, 10);
    gtk_grid_set_column_spacing(grid, 10);
    gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10);

    GtkWidget *hotkey_label = gtk_label_new("Press Alt+F1 to start/stop replay");
    gtk_grid_attach(grid, hotkey_label, 0, 0, 2, 1);
    gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 1, 2, 1);

    GtkGrid *file_chooser_grid = GTK_GRID(gtk_grid_new());
    gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, 2, 2, 1);
    gtk_grid_set_column_spacing(file_chooser_grid, 10);
    GtkWidget *file_chooser_label = gtk_label_new("Where do you want to save the replays?");
    gtk_grid_attach(file_chooser_grid, GTK_WIDGET(file_chooser_label), 0, 0, 1, 1);
    directory_chooser_button = GTK_BUTTON(gtk_button_new_with_label(video_filepath.c_str()));
    gtk_button_set_image(directory_chooser_button, save_icon);
    gtk_button_set_always_show_image(directory_chooser_button, true);
    gtk_button_set_image_position(directory_chooser_button, GTK_POS_RIGHT);
    gtk_widget_set_hexpand(GTK_WIDGET(directory_chooser_button), true);
    g_signal_connect(directory_chooser_button, "clicked", G_CALLBACK(on_directory_chooser_button_click), nullptr);
    gtk_grid_attach(file_chooser_grid, GTK_WIDGET(directory_chooser_button), 1, 0, 1, 1);

    GtkGrid *replay_time_grid = GTK_GRID(gtk_grid_new());
    gtk_grid_attach(grid, GTK_WIDGET(replay_time_grid), 0, 3, 2, 1);
    gtk_grid_attach(replay_time_grid, gtk_label_new("Replay time: "), 0, 0, 1, 1);
    replay_time_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(10.0, 1200.0, 1.0));
    gtk_spin_button_set_value(replay_time_entry, 30.0);
    gtk_widget_set_hexpand(GTK_WIDGET(replay_time_entry), true);
    gtk_grid_attach(replay_time_grid, GTK_WIDGET(replay_time_entry), 1, 0, 1, 1);

    GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new());
    gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, 4, 2, 1);
    gtk_grid_set_column_spacing(start_button_grid, 10);
    replay_back_button = GTK_BUTTON(gtk_button_new_with_label("Back"));
    gtk_widget_set_hexpand(GTK_WIDGET(replay_back_button), true);
    gtk_grid_attach(start_button_grid, GTK_WIDGET(replay_back_button), 0, 0, 1, 1);
    start_replay_button = GTK_BUTTON(gtk_button_new_with_label("Start replay"));
    gtk_widget_set_hexpand(GTK_WIDGET(start_replay_button), true);
    g_signal_connect(start_replay_button, "clicked", G_CALLBACK(on_start_replay_button_click), app);
    gtk_grid_attach(start_button_grid, GTK_WIDGET(start_replay_button), 1, 0, 1, 1);

    return GTK_WIDGET(grid);
}

static GtkWidget* create_recording_page(GtkStack *stack) {
    GtkGrid *grid = GTK_GRID(gtk_grid_new());
    gtk_stack_add_named(stack, GTK_WIDGET(grid), "recording");
    gtk_widget_set_vexpand(GTK_WIDGET(grid), true);
    gtk_widget_set_hexpand(GTK_WIDGET(grid), true);
    gtk_grid_set_row_spacing(grid, 10);
    gtk_grid_set_column_spacing(grid, 10);
    gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10);

    GtkWidget *hotkey_label = gtk_label_new("Press Alt+F1 to start/stop recording");
    gtk_grid_attach(grid, hotkey_label, 0, 0, 2, 1);
    gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 1, 2, 1);

    GtkGrid *file_chooser_grid = GTK_GRID(gtk_grid_new());
    gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, 2, 2, 1);
    gtk_grid_set_column_spacing(file_chooser_grid, 10);
    GtkWidget *file_chooser_label = gtk_label_new("Where do you want to save the video?");
    gtk_grid_attach(file_chooser_grid, GTK_WIDGET(file_chooser_label), 0, 0, 1, 1);
    file_chooser_button = GTK_BUTTON(gtk_button_new_with_label(""));
    gtk_button_set_image(file_chooser_button, save_icon);
    gtk_button_set_always_show_image(file_chooser_button, true);
    gtk_button_set_image_position(file_chooser_button, GTK_POS_RIGHT);
    gtk_widget_set_hexpand(GTK_WIDGET(file_chooser_button), true);
    g_signal_connect(file_chooser_button, "clicked", G_CALLBACK(on_file_chooser_button_click), nullptr);
    gtk_grid_attach(file_chooser_grid, GTK_WIDGET(file_chooser_button), 1, 0, 1, 1);

    GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new());
    gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, 3, 2, 1);
    gtk_grid_set_column_spacing(start_button_grid, 10);
    record_back_button = GTK_BUTTON(gtk_button_new_with_label("Back"));
    gtk_widget_set_hexpand(GTK_WIDGET(record_back_button), true);
    gtk_grid_attach(start_button_grid, GTK_WIDGET(record_back_button), 0, 0, 1, 1);
    start_recording_button = GTK_BUTTON(gtk_button_new_with_label("Start recording"));
    gtk_widget_set_hexpand(GTK_WIDGET(start_recording_button), true);
    g_signal_connect(start_recording_button, "clicked", G_CALLBACK(on_start_recording_button_click), nullptr);
    gtk_grid_attach(start_button_grid, GTK_WIDGET(start_recording_button), 1, 0, 1, 1);

    return GTK_WIDGET(grid);
}

static void stream_service_change_callback(GtkComboBox *widget, gpointer userdata) {
    enable_stream_record_button_if_info_filled();
}

static GtkWidget* create_streaming_page(GtkStack *stack) {
    GtkGrid *grid = GTK_GRID(gtk_grid_new());
    gtk_stack_add_named(stack, GTK_WIDGET(grid), "streaming");
    gtk_widget_set_vexpand(GTK_WIDGET(grid), true);
    gtk_widget_set_hexpand(GTK_WIDGET(grid), true);
    gtk_grid_set_row_spacing(grid, 10);
    gtk_grid_set_column_spacing(grid, 10);
    gtk_widget_set_margin(GTK_WIDGET(grid), 10, 10, 10, 10);

    GtkWidget *hotkey_label = gtk_label_new("Press Alt+F1 to start/stop streaming");
    gtk_grid_attach(grid, hotkey_label, 0, 0, 2, 1);
    gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 1, 2, 1);

    GtkGrid *stream_service_grid = GTK_GRID(gtk_grid_new());
    gtk_grid_attach(grid, GTK_WIDGET(stream_service_grid), 0, 2, 2, 1);
    gtk_grid_attach(stream_service_grid, gtk_label_new("Stream service: "), 0, 0, 1, 1);
    stream_service_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new());
    gtk_combo_box_text_append_text(stream_service_input_menu, "Twitch");
    //gtk_combo_box_text_append_text(stream_service_input_menu, "Custom");
    g_signal_connect(stream_service_input_menu, "changed", G_CALLBACK(stream_service_change_callback), nullptr);
    gtk_combo_box_set_active(GTK_COMBO_BOX(stream_service_input_menu), 0);
    gtk_widget_set_hexpand(GTK_WIDGET(stream_service_input_menu), true);
    gtk_grid_attach(stream_service_grid, GTK_WIDGET(stream_service_input_menu), 1, 0, 1, 1);

    GtkGrid *stream_id_grid = GTK_GRID(gtk_grid_new());
    gtk_grid_attach(grid, GTK_WIDGET(stream_id_grid), 0, 3, 2, 1);
    gtk_grid_attach(stream_id_grid, gtk_label_new("Stream id: "), 0, 0, 1, 1);
    stream_id_entry = GTK_ENTRY(gtk_entry_new());
    gtk_widget_set_hexpand(GTK_WIDGET(stream_id_entry), true);
    gtk_grid_attach(stream_id_grid, GTK_WIDGET(stream_id_entry), 1, 0, 1, 1);

    GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new());
    gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, 4, 2, 1);
    gtk_grid_set_column_spacing(start_button_grid, 10);
    stream_back_button = GTK_BUTTON(gtk_button_new_with_label("Back"));
    gtk_widget_set_hexpand(GTK_WIDGET(stream_back_button), true);
    gtk_grid_attach(start_button_grid, GTK_WIDGET(stream_back_button), 0, 0, 1, 1);
    start_streaming_button = GTK_BUTTON(gtk_button_new_with_label("Start streaming"));
    gtk_widget_set_hexpand(GTK_WIDGET(start_streaming_button), true);
    g_signal_connect(start_streaming_button, "clicked", G_CALLBACK(on_start_streaming_button_click), nullptr);
    gtk_grid_attach(start_button_grid, GTK_WIDGET(start_streaming_button), 1, 0, 1, 1);

    return GTK_WIDGET(grid);
}

static gboolean on_destroy_window(GtkWidget *widget, GdkEvent *event, gpointer data) {
    if(gpu_screen_recorder_process != -1) {
        kill(gpu_screen_recorder_process, SIGINT);
        int status;
        if(waitpid(gpu_screen_recorder_process, &status, 0) == -1) {
            perror("waitpid failed");
            /* Ignore... */
        }
    }
    if(ffmpeg_process != -1)
        kill(ffmpeg_process, SIGKILL);
    return true;
}

typedef gboolean (*KeyPressHandler)(GtkButton *button, gpointer userdata);
static void keypress_toggle_recording(bool recording_state, GtkButton *record_button, KeyPressHandler keypress_handler, GtkApplication *app) {
    if(!gtk_widget_get_sensitive(GTK_WIDGET(record_button)))
        return;

    if(!recording_state) {
        keypress_handler(record_button, app);
    } else if(recording_state) {
        keypress_handler(record_button, app);
    }
}

static bool hotkey_pressed = false;
static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent *event, gpointer userdata) {
    PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata;
    XEvent *ev = (XEvent*)xevent;
    Display *display = gdk_x11_get_default_xdisplay();

    if((ev->type == KeyPress || ev->type == KeyRelease) && XKeycodeToKeysym(display, ev->xkey.keycode, 0) == XK_F1 && (ev->xkey.state & Mod1Mask)) {
        if(ev->type == KeyPress) {
            if(hotkey_pressed)
                return GDK_FILTER_CONTINUE;
            hotkey_pressed = true;
        } else if(ev->type == KeyRelease) {
            hotkey_pressed = false;
            return GDK_FILTER_CONTINUE;
        }

        GtkWidget *visible_page = gtk_stack_get_visible_child(page_navigation_userdata->stack);
        if(visible_page == page_navigation_userdata->recording_page) {
            keypress_toggle_recording(recording, start_recording_button, on_start_recording_button_click, page_navigation_userdata->app);
        } else if(visible_page == page_navigation_userdata->streaming_page) {
            keypress_toggle_recording(streaming, start_streaming_button, on_start_streaming_button_click, page_navigation_userdata->app);
        } else if(visible_page == page_navigation_userdata->replay_page) {
            keypress_toggle_recording(replaying, start_replay_button, on_start_replay_button_click, page_navigation_userdata->app);
        }
    }

    return GDK_FILTER_CONTINUE;
}

static int xerror_dummy(Display *dpy, XErrorEvent *ee) {
    return 0;
}

static void grabkeys(Display *display) {
	unsigned int numlockmask = 0;
    KeyCode numlock_keycode = XKeysymToKeycode(display, XK_Num_Lock);
    XModifierKeymap *modmap = XGetModifierMapping(display);
    for(int i = 0; i < 8; ++i) {
        for(int j = 0; j < modmap->max_keypermod; ++j) {
            if(modmap->modifiermap[i * modmap->max_keypermod + j] == numlock_keycode)
                numlockmask = (1 << i); 
        }
    }
	XFreeModifiermap(modmap);

    XErrorHandler prev_error_handler = XSetErrorHandler(xerror_dummy);
    
	Window root_window = DefaultRootWindow(display);
    unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask };
    for(int i = 0; i < 4; ++i)
        XGrabKey(display, XKeysymToKeycode(display, XK_F1), Mod1Mask|modifiers[i], root_window, False, GrabModeAsync, GrabModeAsync);
    
    XSetErrorHandler(prev_error_handler);
}

static void activate(GtkApplication *app, gpointer userdata) { 
    GtkWidget *window = gtk_application_window_new(app);
    g_signal_connect(window, "destroy", G_CALLBACK(on_destroy_window), nullptr);
    gtk_window_set_title(GTK_WINDOW(window), "GPU Screen Recorder");
    gtk_window_set_resizable(GTK_WINDOW(window), false);

    crosshair_cursor = XCreateFontCursor(gdk_x11_get_default_xdisplay(), XC_crosshair);
    save_icon = gtk_image_new_from_stock("gtk-save", GTK_ICON_SIZE_BUTTON);

    GtkStack *stack = GTK_STACK(gtk_stack_new());
    gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(stack));
    gtk_stack_set_transition_type(stack, GTK_STACK_TRANSITION_TYPE_NONE);
    gtk_stack_set_transition_duration(stack, 0);
    gtk_stack_set_homogeneous(stack, false);
    GtkWidget *common_settings_page = create_common_settings_page(stack, app);
    GtkWidget *replay_page = create_replay_page(app, stack);
    GtkWidget *recording_page = create_recording_page(stack);
    GtkWidget *streaming_page = create_streaming_page(stack);
    gtk_stack_set_visible_child(stack, common_settings_page);

    page_navigation_userdata.app = app;
    page_navigation_userdata.stack = stack;
    page_navigation_userdata.common_settings_page = common_settings_page;
    page_navigation_userdata.replay_page = replay_page;
    page_navigation_userdata.recording_page = recording_page;
    page_navigation_userdata.streaming_page = streaming_page;

    g_signal_connect(replay_button, "clicked", G_CALLBACK(on_start_replay_click), &page_navigation_userdata);
    g_signal_connect(replay_back_button, "clicked", G_CALLBACK(on_streaming_recording_page_back_click), &page_navigation_userdata);

    g_signal_connect(record_button, "clicked", G_CALLBACK(on_start_recording_click), &page_navigation_userdata);
    g_signal_connect(record_back_button, "clicked", G_CALLBACK(on_streaming_recording_page_back_click), &page_navigation_userdata);

    g_signal_connect(stream_button, "clicked", G_CALLBACK(on_start_streaming_click), &page_navigation_userdata);
    g_signal_connect(stream_back_button, "clicked", G_CALLBACK(on_streaming_recording_page_back_click), &page_navigation_userdata);

    Display *display = gdk_x11_get_default_xdisplay();
    grabkeys(display);
    GdkWindow *root_window = gdk_get_default_root_window();
    //gdk_window_set_events(root_window, GDK_BUTTON_PRESS_MASK);
    gdk_window_add_filter(root_window, hotkey_filter_callback, &page_navigation_userdata);
    Bool sup = False;
    XkbSetDetectableAutoRepeat(display, True, &sup);

    gtk_widget_show_all(window);
}

int main(int argc, char **argv) {
    GtkApplication *app = gtk_application_new("org.dec05eba.gpu-screen-recorder", G_APPLICATION_FLAGS_NONE);
    g_signal_connect(app, "activate", G_CALLBACK(activate), nullptr);
    int status = g_application_run(G_APPLICATION(app), argc, argv);
    g_object_unref(app);
    return status;
}