#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <gtk/gtk.h>

#include <stlink.h>
#include "gui.h"

#define MEM_READ_SIZE 1024

#ifndef G_VALUE_INIT
#define G_VALUE_INIT {0, {{0}}}
#endif

G_DEFINE_TYPE(STlinkGUI, stlink_gui, G_TYPE_OBJECT);

static void stlink_gui_dispose(GObject *gobject) {
    G_OBJECT_CLASS(stlink_gui_parent_class)->dispose(gobject);
}

static void stlink_gui_finalize(GObject *gobject) {
    G_OBJECT_CLASS(stlink_gui_parent_class)->finalize(gobject);
}

static void stlink_gui_class_init(STlinkGUIClass *klass) {
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    gobject_class->dispose  = stlink_gui_dispose;
    gobject_class->finalize = stlink_gui_finalize;
}

static void stlink_gui_init(STlinkGUI *self) {
    self->sl       = NULL;
    self->filename = NULL;

    self->progress.activity_mode = FALSE;
    self->progress.fraction      = 0;

    self->flash_mem.memory = NULL;
    self->flash_mem.size   = 0;
    self->flash_mem.base   = 0;

    self->file_mem.memory = NULL;
    self->file_mem.size   = 0;
    self->file_mem.base   = 0;
}

static gboolean set_info_error_message_idle(STlinkGUI *gui) {
    if (gui->error_message != NULL) {
        gchar *markup;

        markup = g_markup_printf_escaped("<b>%s</b>", gui->error_message);
        gtk_label_set_markup(gui->infolabel, markup);
        gtk_info_bar_set_message_type(gui->infobar, GTK_MESSAGE_ERROR);
        gtk_widget_show(GTK_WIDGET(gui->infobar));

        g_free(markup);
        g_free(gui->error_message);
        gui->error_message = NULL;
    }

    return(FALSE);
}

static void stlink_gui_set_info_error_message(STlinkGUI *gui, const gchar *message) {
    gui->error_message = g_strdup(message);
    g_idle_add((GSourceFunc)set_info_error_message_idle, gui);
}

static void stlink_gui_set_sensitivity(STlinkGUI *gui, gboolean sensitivity) {
    gtk_widget_set_sensitive(GTK_WIDGET(gui->open_button), sensitivity);

    if (sensitivity && gui->sl) {
        gtk_widget_set_sensitive(GTK_WIDGET(gui->disconnect_button), sensitivity);
    }

    if (sensitivity && !gui->sl) {
        gtk_widget_set_sensitive(GTK_WIDGET(gui->connect_button), sensitivity);
    }

    if (sensitivity && gui->sl && gui->filename) {
        gtk_widget_set_sensitive(GTK_WIDGET(gui->flash_button), sensitivity);
    }

    gtk_widget_set_sensitive(GTK_WIDGET(gui->export_button), sensitivity && (gui->sl != NULL));
}

static void mem_view_init_headers(GtkTreeView *view) {
    GtkCellRenderer *renderer;
    gint i;

    g_return_if_fail(view != NULL);

    renderer = gtk_cell_renderer_text_new();
    gtk_tree_view_insert_column_with_attributes(view, -1, "Address", renderer, "text", 0, /* column */ NULL);

    for (i = 0; i < 4; i++) {
        gchar *label;

        label = g_strdup_printf("%X", i * 4);
        renderer = gtk_cell_renderer_text_new();
        gtk_tree_view_insert_column_with_attributes(view, -1, label, renderer, "text", (i + 1), /* column */ NULL);
        g_free(label);
    }

    for (i = 0; i < 5; i++) {
        GtkTreeViewColumn *column = gtk_tree_view_get_column(view, i);
        gtk_tree_view_column_set_expand(column, TRUE);
    }
}

static void mem_view_add_as_hex(GtkListStore *store,
                                GtkTreeIter *iter,
                                gint column,
                                guint32 value) {
    gchar     *hex_str;

    hex_str = g_strdup_printf("0x%08X", value);
    gtk_list_store_set(store, iter, column, hex_str, -1);
    g_free(hex_str);
}

static void mem_view_add_buffer(GtkListStore *store,
                                GtkTreeIter *iter,
                                guint32 address,
                                guchar *buffer,
                                gint len) {
    guint32 *word;
    gint i, step;
    gint column = 0;

    step = sizeof(*word);

    for (i = 0; i < len; i += step) {
        word = (guint *)&buffer[i];

        if (column == 0) {
            gtk_list_store_append(store, iter); // new row
            mem_view_add_as_hex(store, iter, column, (address + i)); // add address
        }

        mem_view_add_as_hex(store, iter, (column + 1), *word);
        column = (column + 1) % step;
    }
}

static guint32 hexstr_to_guint32(const gchar *str, GError **err) {
    guint32 val;
    gchar *end_ptr;

    val = (guint32)strtoul(str, &end_ptr, 16);

    if ((errno == ERANGE && val == UINT_MAX) || (errno != 0 && val == 0)) {
        g_set_error(err, g_quark_from_string("hextou32"), 1, "Invalid hexstring");
        return(UINT32_MAX);
    }

    if (end_ptr == str) {
        g_set_error(err, g_quark_from_string("hextou32"), 2, "Invalid hexstring");
        return(UINT32_MAX);
    }

    return(val);
}

static void stlink_gui_update_mem_view(STlinkGUI *gui, struct mem_t *mem, GtkTreeView *view) {
    GtkListStore *store;
    GtkTreeIter iter;

    store = GTK_LIST_STORE(gtk_tree_view_get_model(view));

    mem_view_add_buffer(store, &iter, mem->base, mem->memory, (gint)mem->size);

    gtk_widget_hide(GTK_WIDGET(gui->progress.bar));
    gtk_progress_bar_set_fraction(gui->progress.bar, 0);
    stlink_gui_set_sensitivity(gui, TRUE);
}

static gboolean stlink_gui_update_devmem_view(STlinkGUI *gui) {
    stlink_gui_update_mem_view(gui, &gui->flash_mem, gui->devmem_treeview);
    return(FALSE);
}

static gpointer stlink_gui_populate_devmem_view(gpointer data) {
    guint off;
    stm32_addr_t addr;

    g_return_val_if_fail(STLINK_IS_GUI(data), NULL);
    STlinkGUI *gui = (STlinkGUI *)data;

    g_return_val_if_fail((gui != NULL), NULL);
    g_return_val_if_fail((gui->sl != NULL), NULL);

    addr = gui->sl->flash_base;

    if (gui->flash_mem.memory) {
        g_free(gui->flash_mem.memory);
    }

    gui->flash_mem.memory = g_malloc(gui->sl->flash_size);
    gui->flash_mem.size   = gui->sl->flash_size;
    gui->flash_mem.base   = gui->sl->flash_base;

    for (off = 0; off < gui->sl->flash_size; off += MEM_READ_SIZE) {
        guint n_read = MEM_READ_SIZE;

        if (off + MEM_READ_SIZE > gui->sl->flash_size) {
            n_read = (guint)gui->sl->flash_size - off;

            if (n_read & 3) { n_read = (n_read + 4) & ~(3); } // align if needed
        }

        stlink_read_mem32(gui->sl, addr + off, n_read); // reads to sl->q_buf

        if (gui->sl->q_len < 0) {
            stlink_gui_set_info_error_message(gui, "Failed to read memory");
            g_free(gui->flash_mem.memory);
            gui->flash_mem.memory = NULL;
            return(NULL);
        }

        memcpy(gui->flash_mem.memory + off, gui->sl->q_buf, n_read);
        gui->progress.fraction = (gdouble)(off + n_read) / gui->sl->flash_size;
    }

    g_idle_add((GSourceFunc)stlink_gui_update_devmem_view, gui);
    return(NULL);
}

static gboolean stlink_gui_update_filemem_view(STlinkGUI *gui) {
    gchar *basename;

    basename = g_path_get_basename(gui->filename);
    gtk_notebook_set_tab_label_text(
        gui->notebook, GTK_WIDGET(gtk_notebook_get_nth_page(gui->notebook, 1)), basename);
    g_free(basename);

    stlink_gui_update_mem_view(gui, &gui->file_mem, gui->filemem_treeview);
    return(FALSE);
}

static gpointer stlink_gui_populate_filemem_view(gpointer data) {
    guchar buffer[MEM_READ_SIZE];
    GFile *file;
    GFileInfo *file_info;
    GInputStream *input_stream;
    gint off;
    GError *err = NULL;

    g_return_val_if_fail(STLINK_IS_GUI(data), NULL);
    STlinkGUI *gui = (STlinkGUI *)data;

    g_return_val_if_fail(gui != NULL, NULL);
    g_return_val_if_fail(gui->filename != NULL, NULL);

    if (g_str_has_suffix(gui->filename, ".hex")) {
        /* If the file has prefix .hex - try to interpret it as Intel-HEX.
         * It's difficult to merge the world of standard functions and GLib, so do it simple:
         * Load whole file into buffer and copy the data to the destination afterwards.
         * In the meanwhile we loose the displaying of the progress and need double memory.
         */

        uint8_t* mem   = NULL;
        size_t size    = 0;
        uint32_t begin = 0;
        int res = stlink_parse_ihex(gui->filename, 0, &mem, &size, &begin);

        if (res == 0) {
            if (gui->file_mem.memory) {
                g_free(gui->file_mem.memory);
            }

            gui->file_mem.size   = size;
            gui->file_mem.memory = g_malloc(size);
            gui->file_mem.base   = begin;

            memcpy(gui->file_mem.memory, mem, size);
        } else {
            stlink_gui_set_info_error_message(gui, "Cannot interpret the file as Intel-HEX");
        }

        free(mem);
    } else {
        file = g_file_new_for_path(gui->filename);
        input_stream = G_INPUT_STREAM(g_file_read(file, NULL, &err));

        if (err) {
            stlink_gui_set_info_error_message(gui, err->message);
            g_error_free(err);
            goto out;
        }

        file_info = g_file_input_stream_query_info(
            G_FILE_INPUT_STREAM(input_stream), G_FILE_ATTRIBUTE_STANDARD_SIZE, NULL, &err);

        if (err) {
            stlink_gui_set_info_error_message(gui, err->message);
            g_error_free(err);
            goto out_input;
        }

        if (gui->file_mem.memory) { g_free(gui->file_mem.memory); }

        gui->file_mem.size   = g_file_info_get_size(file_info);
        gui->file_mem.memory = g_malloc(gui->file_mem.size);

        for (off = 0; off < (gint)gui->file_mem.size; off += MEM_READ_SIZE) {
            guint n_read = MEM_READ_SIZE;

            if (off + MEM_READ_SIZE > (gint)gui->file_mem.size) {
                n_read = (guint)gui->file_mem.size - off;
            }

            if (g_input_stream_read(
                    G_INPUT_STREAM(input_stream), &buffer, n_read, NULL, &err) == -1) {
                stlink_gui_set_info_error_message(gui, err->message);
                g_error_free(err);
                goto out_input;
            }

            memcpy(gui->file_mem.memory + off, buffer, n_read);
            gui->progress.fraction = (gdouble)(off + n_read) / gui->file_mem.size;
        }

out_input: g_object_unref(input_stream);
out:       g_object_unref(file);
    }

    g_idle_add((GSourceFunc)stlink_gui_update_filemem_view, gui);
    return(NULL);
}

static void mem_jmp(GtkTreeView *view,
                    GtkEntry *entry,
                    guint32 base_addr,
                    gsize size,
                    GError **err) {
    GtkTreeModel *model;
    guint32 jmp_addr;
    GtkTreeIter iter;

    jmp_addr = hexstr_to_guint32(gtk_entry_get_text(entry), err);

    if (err && *err) { return; }

    if (jmp_addr < base_addr || jmp_addr > base_addr + size) {
        g_set_error(err, g_quark_from_string("mem_jmp"), 1, "Invalid address");
        return;
    }

    model = gtk_tree_view_get_model(view);

    if (!model) { return; }

    if (gtk_tree_model_get_iter_first(model, &iter)) {
        do {
            guint32 addr;
            GValue value = G_VALUE_INIT;

            gtk_tree_model_get_value(model, &iter, 0, &value);

            if (G_VALUE_HOLDS_STRING(&value)) {
                addr = hexstr_to_guint32(g_value_get_string(&value), err);

                if (!*err) {
                    if (addr == (jmp_addr & 0xFFFFFFF0)) {
                        GtkTreeSelection *selection;
                        GtkTreePath *path;

                        selection = gtk_tree_view_get_selection(view);
                        path      = gtk_tree_model_get_path(model, &iter);

                        gtk_tree_selection_select_iter(selection, &iter);
                        gtk_tree_view_scroll_to_cell(view, path, NULL, TRUE, 0.0, 0.0);
                        gtk_tree_path_free(path);
                    }
                }
            }

            g_value_unset(&value);
        } while (gtk_tree_model_iter_next(model, &iter));
    }
}

static void devmem_jmp_cb(GtkWidget *widget, gpointer data) {
    STlinkGUI *gui;
    GError *err = NULL;
    (void)widget;

    gui = STLINK_GUI(data);

    mem_jmp(gui->devmem_treeview,
            gui->devmem_jmp_entry,
            gui->sl->flash_base,
            gui->sl->flash_size,
            &err);

    if (err) {
        stlink_gui_set_info_error_message(gui, err->message);
        g_error_free(err);
    }
}

static void filemem_jmp_cb(GtkWidget *widget, gpointer data) {
    STlinkGUI *gui;
    GError *err = NULL;
    (void)widget;

    gui = STLINK_GUI(data);

    g_return_if_fail(gui->filename != NULL);

    mem_jmp(gui->filemem_treeview,
            gui->filemem_jmp_entry,
            0,
            gui->file_mem.size,
            &err);

    if (err) {
        stlink_gui_set_info_error_message(gui, err->message);
        g_error_free(err);
    }
}

static gchar *dev_format_chip_id(guint32 chip_id) {
    const struct stlink_chipid_params *params;

    params = stlink_chipid_get_params(chip_id);

    if (!params) { return(g_strdup_printf("0x%x", chip_id)); }

    return(g_strdup(params->description));
}

static gchar *dev_format_mem_size(gsize flash_size) {
    return(g_strdup_printf("%u kB", (unsigned int)(flash_size / 1024)));
}


static void stlink_gui_set_connected(STlinkGUI *gui) {
    gchar *tmp_str;
    GtkListStore *store;
    GtkTreeIter iter;

    gtk_statusbar_push(gui->statusbar, gtk_statusbar_get_context_id(gui->statusbar, "conn"), "Connected");

    gtk_widget_set_sensitive(GTK_WIDGET(gui->device_frame), TRUE);
    gtk_widget_set_sensitive(GTK_WIDGET(gui->devmem_box), TRUE);
    gtk_widget_set_sensitive(GTK_WIDGET(gui->connect_button), FALSE);

    if (gui->filename) {
        gtk_widget_set_sensitive(GTK_WIDGET(gui->flash_button), TRUE);
    }

    tmp_str = dev_format_chip_id(gui->sl->chip_id);
    gtk_label_set_text(gui->chip_id_label, tmp_str);
    g_free(tmp_str);

    tmp_str = g_strdup_printf("0x%x", gui->sl->core_id);
    gtk_label_set_text(gui->core_id_label, tmp_str);
    g_free(tmp_str);

    tmp_str = dev_format_mem_size(gui->sl->flash_size);
    gtk_label_set_text(gui->flash_size_label, tmp_str);
    g_free(tmp_str);

    tmp_str = dev_format_mem_size(gui->sl->sram_size);
    gtk_label_set_text(gui->ram_size_label, tmp_str);
    g_free(tmp_str);

    tmp_str = g_strdup_printf("0x%08X", gui->sl->flash_base);
    gtk_entry_set_text(gui->devmem_jmp_entry, tmp_str);
    gtk_editable_set_editable(GTK_EDITABLE(gui->devmem_jmp_entry), TRUE);
    g_free(tmp_str);

    store = GTK_LIST_STORE(gtk_tree_view_get_model(gui->devmem_treeview));

    if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter)) {
        gtk_list_store_clear(store);
    }

    stlink_gui_set_sensitivity(gui, FALSE);
    gtk_notebook_set_current_page(gui->notebook, PAGE_DEVMEM);
    gtk_widget_show(GTK_WIDGET(gui->progress.bar));
    gtk_progress_bar_set_text(gui->progress.bar, "Reading memory");

    g_thread_new("devmem", (GThreadFunc)stlink_gui_populate_devmem_view, gui);
}

static void connect_button_cb(GtkWidget *widget, gpointer data) {
    STlinkGUI *gui;
    (void)widget;

    gui = STLINK_GUI(data);

    if (gui->sl != NULL) { return; }

    gui->sl = stlink_v1_open(0, 1); // try version 1 then version 2

    if (gui->sl == NULL) { gui->sl = stlink_open_usb(0, 1, NULL, 0); }

    if (gui->sl == NULL) {
        stlink_gui_set_info_error_message(gui, "Failed to connect to STLink.");
        return;
    }

    // code below taken from flash/main.c, refactoring might be in order
    if (stlink_current_mode(gui->sl) == STLINK_DEV_DFU_MODE) {
        stlink_exit_dfu_mode(gui->sl);
    }

    if (stlink_current_mode(gui->sl) != STLINK_DEV_DEBUG_MODE) {
        stlink_enter_swd_mode(gui->sl);
    }

    stlink_gui_set_connected(gui);
}

static void stlink_gui_set_disconnected(STlinkGUI *gui) {
    gtk_statusbar_push(gui->statusbar, gtk_statusbar_get_context_id(gui->statusbar, "conn"), "Disconnected");

    gtk_widget_set_sensitive(GTK_WIDGET(gui->device_frame), FALSE);
    gtk_widget_set_sensitive(GTK_WIDGET(gui->flash_button), FALSE);
    gtk_widget_set_sensitive(GTK_WIDGET(gui->export_button), FALSE);
    gtk_widget_set_sensitive(GTK_WIDGET(gui->disconnect_button), FALSE);
    gtk_widget_set_sensitive(GTK_WIDGET(gui->connect_button), TRUE);
}

static void disconnect_button_cb(GtkWidget *widget, gpointer data) {
    STlinkGUI *gui;
    (void)widget;

    gui = STLINK_GUI(data);

    if (gui->sl != NULL) {
        stlink_exit_debug_mode(gui->sl);
        stlink_close(gui->sl);
        gui->sl = NULL;
    }

    stlink_gui_set_disconnected(gui);
}


static void stlink_gui_open_file(STlinkGUI *gui) {
    GtkWidget *dialog;
    GtkListStore *store;
    GtkTreeIter iter;

    dialog = gtk_file_chooser_dialog_new("Open file",
                                         gui->window,
                                         GTK_FILE_CHOOSER_ACTION_OPEN,
                                         "_Cancel", GTK_RESPONSE_CANCEL,
                                         "_Open", GTK_RESPONSE_ACCEPT,
                                         NULL);

    if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
        gui->filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));

        store = GTK_LIST_STORE(gtk_tree_view_get_model(gui->filemem_treeview));

        if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter)) {
            gtk_list_store_clear(store);
        }

        stlink_gui_set_sensitivity(gui, FALSE);
        gtk_notebook_set_current_page(gui->notebook, PAGE_FILEMEM);
        gtk_widget_show(GTK_WIDGET(gui->progress.bar));
        gtk_progress_bar_set_text(gui->progress.bar, "Reading file");
        g_thread_new("file", (GThreadFunc)stlink_gui_populate_filemem_view, gui);
    }

    gtk_widget_destroy(dialog);
}

static void open_button_cb(GtkWidget *widget, gpointer data) {
    STlinkGUI *gui;
    (void)widget;

    gui = STLINK_GUI(data);
    stlink_gui_open_file(gui);
}

static gboolean stlink_gui_write_flash_update(STlinkGUI *gui) {
    stlink_gui_set_sensitivity(gui, TRUE);
    gui->progress.activity_mode = FALSE;
    gtk_widget_hide(GTK_WIDGET(gui->progress.bar));
    return(FALSE);
}

static gpointer stlink_gui_write_flash(gpointer data) {
    g_return_val_if_fail(STLINK_IS_GUI(data), NULL);
    STlinkGUI *gui = (STlinkGUI *)data;

    g_return_val_if_fail((gui->sl != NULL), NULL);
    g_return_val_if_fail((gui->filename != NULL), NULL);

    if (stlink_mwrite_flash(
            gui->sl, gui->file_mem.memory, (uint32_t)gui->file_mem.size, gui->sl->flash_base) < 0) {
        stlink_gui_set_info_error_message(gui, "Failed to write to flash");
    }

    g_idle_add((GSourceFunc)stlink_gui_write_flash_update, gui);
    return(NULL);
}

static void flash_button_cb(GtkWidget *widget, gpointer data) {
    STlinkGUI *gui;
    gchar *tmp_str;
    guint32 address;
    gint result;
    GError *err = NULL;
    (void)widget;

    gui = STLINK_GUI(data);
    g_return_if_fail(gui->sl != NULL);

    if (!g_strcmp0(gtk_entry_get_text(gui->flash_dialog_entry), "")) {
        tmp_str = g_strdup_printf("0x%08X", gui->sl->flash_base);
        gtk_entry_set_text(gui->flash_dialog_entry, tmp_str);
        g_free(tmp_str);
    }

    result = gtk_dialog_run(gui->flash_dialog);

    if (result == GTK_RESPONSE_OK) {
        address = hexstr_to_guint32(gtk_entry_get_text(gui->flash_dialog_entry), &err);

        if (err) {
            stlink_gui_set_info_error_message(gui, err->message);
        } else {
            if (address > gui->sl->flash_base + gui->sl->flash_size || address < gui->sl->flash_base) {
                stlink_gui_set_info_error_message(gui, "Invalid address");
            } else if (address + gui->file_mem.size > gui->sl->flash_base + gui->sl->flash_size) {
                stlink_gui_set_info_error_message(gui, "Binary overwrites flash");
            } else {
                stlink_gui_set_sensitivity(gui, FALSE);
                gtk_progress_bar_set_text(gui->progress.bar, "Writing to flash");
                gui->progress.activity_mode = TRUE;
                gtk_widget_show(GTK_WIDGET(gui->progress.bar));
                g_thread_new("flash", (GThreadFunc)stlink_gui_write_flash, gui);
            }
        }
    }
}

int export_to_file(const char*filename, const struct mem_t flash_mem) {
    printf("%s\n", filename);
    FILE * f = fopen(filename, "w");

    if (f == NULL) { return(-1); }

    for (gsize i = 0; i < flash_mem.size; i++)
        if (fputc(flash_mem.memory[i], f) == EOF) { return(-1); }

    fclose(f);
    return(0);
}

static void export_button_cb(GtkWidget *widget, gpointer data) {
    (void)widget;
    STlinkGUI * gui = STLINK_GUI(data);
    GtkWidget *dialog;
    dialog = gtk_file_chooser_dialog_new("Save as",
                                         gui->window,
                                         GTK_FILE_CHOOSER_ACTION_SAVE,
                                         "_Cancel",
                                         GTK_RESPONSE_CANCEL,
                                         "_Open",
                                         GTK_RESPONSE_ACCEPT,
                                         NULL);
    GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
    gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
    gint res = gtk_dialog_run(GTK_DIALOG(dialog));

    if (res == GTK_RESPONSE_ACCEPT) {
        char *filename;

        filename = gtk_file_chooser_get_filename(chooser);

        if (export_to_file(filename, gui->flash_mem) != 0) {
            stlink_gui_set_info_error_message(gui, "Failed to export flash");
        } else {
            stlink_gui_set_info_error_message(gui, "Export successful");
        }

        g_free(filename);
    }

    gtk_widget_destroy(dialog);
}

static gboolean progress_pulse_timeout(STlinkGUI *gui) {
    if (gui->progress.activity_mode) {
        gtk_progress_bar_pulse(gui->progress.bar);
    } else {
        gtk_progress_bar_set_fraction(gui->progress.bar, gui->progress.fraction);
    }

    return(TRUE);
}

static void notebook_switch_page_cb(GtkNotebook *notebook,
                                    GtkWidget *widget,
                                    guint page_num,
                                    gpointer data) {
    STlinkGUI *gui;
    (void)notebook;
    (void)widget;

    gui = STLINK_GUI(data);

    if (page_num == 1) {
        if (gui->filename == NULL) { stlink_gui_open_file(gui); }
    }
}

static void dnd_received_cb(GtkWidget *widget,
                            GdkDragContext *context,
                            gint x,
                            gint y,
                            GtkSelectionData *selection_data,
                            guint target_type,
                            guint timestamp,
                            gpointer data) {
    GFile *file_uri;
    gchar **file_list;
    const guchar *file_data;
    STlinkGUI *gui = STLINK_GUI(data);
    GtkListStore *store;
    GtkTreeIter iter;
    (void)widget;
    (void)x;
    (void)y;

    if (selection_data != NULL && gtk_selection_data_get_length(selection_data) > 0) {
        switch (target_type) {
        case TARGET_FILENAME:

            if (gui->filename) {
                g_free(gui->filename);
            }

            file_data = gtk_selection_data_get_data(selection_data);
            file_list = g_strsplit((gchar *)file_data, "\r\n", 0);

            file_uri = g_file_new_for_uri(file_list[0]);
            gui->filename = g_file_get_path(file_uri);

            g_strfreev(file_list);
            g_object_unref(file_uri);

            store = GTK_LIST_STORE(gtk_tree_view_get_model(gui->devmem_treeview));

            if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter)) {
                gtk_list_store_clear(store);
            }

            stlink_gui_set_sensitivity(gui, FALSE);
            gtk_notebook_set_current_page(gui->notebook, PAGE_FILEMEM);
            gtk_widget_show(GTK_WIDGET(gui->progress.bar));
            gtk_progress_bar_set_text(gui->progress.bar, "Reading file");
            g_thread_new("file", (GThreadFunc)stlink_gui_populate_filemem_view, gui);
            break;
        }
    }

    gtk_drag_finish(
        context,
        TRUE,
        gdk_drag_context_get_suggested_action(context) == GDK_ACTION_MOVE,
        timestamp);
}

void stlink_gui_init_dnd(STlinkGUI *gui) {
    GtkTargetEntry target_list[] = {
        { "text/uri-list", 0, TARGET_FILENAME },
    };

    gtk_drag_dest_set(
        GTK_WIDGET(gui->window),
        GTK_DEST_DEFAULT_ALL,
        target_list,
        G_N_ELEMENTS(target_list),
        GDK_ACTION_COPY);

    g_signal_connect(gui->window, "drag-data-received", G_CALLBACK(dnd_received_cb), gui);
}

static void stlink_gui_build_ui(STlinkGUI *gui) {
    GtkBuilder *builder;
    GtkListStore *devmem_store;
    GtkListStore *filemem_store;
    gchar *ui_file = STLINK_UI_DIR "/stlink-gui.ui";

    if (!g_file_test(ui_file, G_FILE_TEST_EXISTS)) { ui_file = "stlink-gui.ui"; }

    builder = gtk_builder_new();

    if (!gtk_builder_add_from_file(builder, ui_file, NULL)) {
        g_printerr("Failed to load UI file: %s\n", ui_file);
        exit(1);
    }

    gui->window = GTK_WINDOW(gtk_builder_get_object(builder, "window"));
    g_signal_connect(G_OBJECT(gui->window), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    /* Setup for toolbutton clicked callbacks */
    gui->open_button = GTK_TOOL_BUTTON(gtk_builder_get_object(builder, "open_button"));
    g_signal_connect(G_OBJECT(gui->open_button), "clicked", G_CALLBACK(open_button_cb), gui);

    gui->connect_button = GTK_TOOL_BUTTON(gtk_builder_get_object(builder, "connect_button"));
    g_signal_connect(G_OBJECT(gui->connect_button), "clicked", G_CALLBACK(connect_button_cb), gui);

    gui->disconnect_button = GTK_TOOL_BUTTON(gtk_builder_get_object(builder, "disconnect_button"));
    g_signal_connect(G_OBJECT(gui->disconnect_button), "clicked", G_CALLBACK(disconnect_button_cb), gui);

    gui->flash_button = GTK_TOOL_BUTTON(gtk_builder_get_object(builder, "flash_button"));
    g_signal_connect(G_OBJECT(gui->flash_button), "clicked", G_CALLBACK(flash_button_cb), gui);

    gui->export_button = GTK_TOOL_BUTTON(gtk_builder_get_object(builder, "export_button"));
    g_signal_connect(G_OBJECT(gui->export_button), "clicked", G_CALLBACK(export_button_cb), gui);

    gui->devmem_treeview = GTK_TREE_VIEW(gtk_builder_get_object(builder, "devmem_treeview"));
    mem_view_init_headers(gui->devmem_treeview);
    devmem_store = gtk_list_store_new(5,
                                      G_TYPE_STRING,
                                      G_TYPE_STRING,
                                      G_TYPE_STRING,
                                      G_TYPE_STRING,
                                      G_TYPE_STRING);
    gtk_tree_view_set_model(gui->devmem_treeview, GTK_TREE_MODEL(devmem_store));
    g_object_unref(devmem_store);

    gui->filemem_treeview = GTK_TREE_VIEW(gtk_builder_get_object(builder, "filemem_treeview"));
    mem_view_init_headers(gui->filemem_treeview);
    filemem_store = gtk_list_store_new(5,
                                       G_TYPE_STRING,
                                       G_TYPE_STRING,
                                       G_TYPE_STRING,
                                       G_TYPE_STRING,
                                       G_TYPE_STRING);
    gtk_tree_view_set_model(gui->filemem_treeview, GTK_TREE_MODEL(filemem_store));
    g_object_unref(filemem_store);

    gui->core_id_label = GTK_LABEL(gtk_builder_get_object(builder, "core_id_value"));
    gui->chip_id_label = GTK_LABEL(gtk_builder_get_object(builder, "chip_id_value"));
    gui->flash_size_label = GTK_LABEL(gtk_builder_get_object(builder, "flash_size_value"));
    gui->ram_size_label = GTK_LABEL(gtk_builder_get_object(builder, "ram_size_value"));
    gui->device_frame = GTK_FRAME(gtk_builder_get_object(builder, "device_frame"));

    gui->notebook = GTK_NOTEBOOK(gtk_builder_get_object(builder, "mem_notebook"));
    g_signal_connect(gui->notebook, "switch-page", G_CALLBACK(notebook_switch_page_cb), gui);

    gui->devmem_box = GTK_BOX(gtk_builder_get_object(builder, "devmem_box"));
    gui->filemem_box = GTK_BOX(gtk_builder_get_object(builder, "filemem_box"));

    gui->devmem_jmp_entry = GTK_ENTRY(gtk_builder_get_object(builder, "devmem_jmp_entry"));
    g_signal_connect(gui->devmem_jmp_entry, "activate", G_CALLBACK(devmem_jmp_cb), gui);

    gui->filemem_jmp_entry = GTK_ENTRY(gtk_builder_get_object(builder, "filemem_jmp_entry"));
    g_signal_connect(gui->filemem_jmp_entry, "activate", G_CALLBACK(filemem_jmp_cb), gui);
    gtk_editable_set_editable(GTK_EDITABLE(gui->filemem_jmp_entry), TRUE);

    gui->progress.bar = GTK_PROGRESS_BAR(gtk_builder_get_object(builder, "progressbar"));
    gtk_progress_bar_set_show_text(gui->progress.bar, TRUE);
    gui->progress.timer = g_timeout_add(100, (GSourceFunc)progress_pulse_timeout, gui);

    gui->statusbar = GTK_STATUSBAR(gtk_builder_get_object(builder, "statusbar"));

    gui->infobar = GTK_INFO_BAR(gtk_builder_get_object(builder, "infobar"));
    gtk_info_bar_add_button(gui->infobar, "_OK", GTK_RESPONSE_OK);
    gui->infolabel = GTK_LABEL(gtk_label_new(""));
    gtk_container_add(GTK_CONTAINER(gtk_info_bar_get_content_area(gui->infobar)), GTK_WIDGET(gui->infolabel));
    g_signal_connect(gui->infobar, "response", G_CALLBACK(gtk_widget_hide), NULL);

    /* Flash dialog */
    gui->flash_dialog = GTK_DIALOG(gtk_builder_get_object(builder, "flash_dialog"));
    g_signal_connect_swapped(gui->flash_dialog, "response", G_CALLBACK(gtk_widget_hide), gui->flash_dialog);
    gui->flash_dialog_ok = GTK_BUTTON(gtk_builder_get_object(builder, "flash_dialog_ok_button"));
    gui->flash_dialog_cancel = GTK_BUTTON(gtk_builder_get_object(builder, "flash_dialog_cancel_button"));
    gui->flash_dialog_entry = GTK_ENTRY(gtk_builder_get_object(builder, "flash_dialog_entry"));

    // make it so
    gtk_widget_show_all(GTK_WIDGET(gui->window));
    gtk_widget_hide(GTK_WIDGET(gui->infobar));
    gtk_widget_hide(GTK_WIDGET(gui->progress.bar));

    stlink_gui_set_disconnected(gui);
}

int main(int argc, char **argv) {
    STlinkGUI *gui;

    gtk_init(&argc, &argv);

    gui = g_object_new(STLINK_TYPE_GUI, NULL);
    stlink_gui_build_ui(gui);
    stlink_gui_init_dnd(gui);

    gtk_main();
    return(0);
}
