/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * See the COPYING file for license information.
 *
 * Guillaume Chazarain <guichaz@gmail.com>
 */

/*************************
 * The top level widgets *
 *************************/

#include <string.h>             /* memcpy() */
#include <gdk/gdkx.h>           /* gdk_net_wm_supports() */

#include "gliv.h"
#include "windows.h"
#include "options.h"
#include "gliv-image.h"
#include "callbacks.h"
#include "messages.h"
#include "math_floats.h"
#include "str_utils.h"
#include "matrix.h"
#include "scrollbars.h"
#include "gl_widget.h"
#include "menus.h"
#include "cursors.h"
#include "loading.h"
#include "help.h"

extern rt_struct *rt;
extern options_struct *options;
extern GlivImage *current_image;
extern GtkWidget *gl_widget;
extern GtkMenuBar *menu_bar;

/* What is in the window. */
static GtkVBox *widgets;

static GtkWindow *main_window;
static GtkWindow *fs_window;

/* The main status bar and its subdivisions. */
static GtkHBox *status_bar;
static GtkEntry *entry_ident;
static GtkEntry *entry_size;
static GtkEntry *entry_state;

/*
 * Called when resizing the window because the image changed or a
 * widget has been toggled.
 */
static void resize_window(gint width, gint height, gboolean first_time)
{
    GtkRequisition req;

    if (options->resize_win == FALSE && first_time == FALSE)
        return;

    if (first_time && options->initial_geometry)
        return;

    width = MAX(width, 320);
    height = MAX(height, 240);

    if (width == rt->wid_size->width && height == rt->wid_size->height)
        return;

    /*
     * First, we let GTK resize the window around the
     * width x height widget,
     */
    gtk_widget_set_size_request(gl_widget, width, height);
    gtk_widget_size_request(GTK_WIDGET(main_window), &req);
    gtk_window_resize(main_window, req.width, req.height);

    /* then, we make the widget downsizable. */
    gtk_widget_set_size_request(gl_widget, 1, 1);
}

void show_message(GtkWidget * widget, const gchar * name)
{
    /* In windowed mode we let the WM choose the position. */
    if (options->fullscreen)
        gtk_window_set_position(GTK_WINDOW(widget), GTK_WIN_POS_MOUSE);

    if (name != NULL)
        gtk_window_set_title(GTK_WINDOW(widget), name);

    gtk_widget_show_all(widget);
}

gint run_modal_dialog(GtkDialog * dialog)
{
    gint response;

    gtk_window_set_transient_for(GTK_WINDOW(dialog), get_current_window());
    gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);

    /* In windowed mode we let the WM choose the position. */
    if (options->fullscreen)
        gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_MOUSE);

    /* We want the cursor to be visible when the dialog is shown. */
    set_hide_cursor_enabled(FALSE);
    response = gtk_dialog_run(GTK_DIALOG(dialog));
    set_hide_cursor_enabled(TRUE);

    return response;
}

static gint get_entry_border(GtkEntry * entry)
{
    gint interior_focus, focus_width;
    gint border;

    gtk_widget_style_get(GTK_WIDGET(entry),
                         "interior-focus", &interior_focus,
                         "focus-line-width", &focus_width, NULL);

    border = entry->has_frame ? GTK_WIDGET(entry)->style->xthickness : 0;

    if (!interior_focus)
        border += focus_width;

    return border + 2;          /* + 2 : INNER_BORDER */
}

static gint status_bar_free_space(void)
{
    gint space = 0;
    GtkEntry *entries[] = { entry_ident, entry_size, entry_state, NULL };
    GtkEntry **ptr;

    for (ptr = entries; *ptr != NULL; ptr++) {
        GtkRequisition req;

        gtk_widget_size_request(GTK_WIDGET(*ptr), &req);
        space += GTK_WIDGET(*ptr)->allocation.width - req.width;
    }

    return space;
}

static void set_entry_text(GtkEntry * entry, const gchar * text)
{
    GtkRequisition old;
    GtkRequisition req;
    gboolean accept_resize;

    gtk_entry_set_text(entry, text);

    /* Compute the full width */
    gtk_widget_size_request(GTK_WIDGET(entry), &old);
    memcpy(&req, &old, sizeof(GtkRequisition));
    pango_layout_get_pixel_size(gtk_entry_get_layout(entry), &req.width, NULL);
    req.width += 2 * get_entry_border(entry);

    if (req.width > old.width) {
        gint free_space = status_bar_free_space();

        accept_resize = req.width - old.width <= free_space;
    } else
        accept_resize = old.width - req.width > 20;

    if (accept_resize)
        gtk_widget_set_size_request(GTK_WIDGET(entry), req.width, req.height);
}

static gint ident_basename_pos(const gchar * ident)
{
    gint pos;
    gint last1 = 0, last2 = 0;

    for (pos = 0; *ident != '\0'; ident = g_utf8_next_char(ident)) {
        if (g_utf8_get_char(ident) == '/') {
            last2 = last1;
            last1 = pos;
        }
        pos++;
    }

    return last2;
}

static void set_ident_text(const gchar * ident)
{
    gint pos;
    const gchar *loading = currently_loading();

    pos = ident_basename_pos(ident);

    if (loading) {
        gchar *concat = g_strconcat(ident, " ", _("loading"), ":",
                                    loading, NULL);
        gtk_entry_set_text(entry_ident, concat);
        g_free(concat);
    } else
        gtk_entry_set_text(entry_ident, ident);

    gtk_editable_set_position(GTK_EDITABLE(entry_ident), pos);
}

/* To add an entry in the status bar. */
static GtkEntry *add_entry(gboolean grow, const gchar * init)
{
    GtkEntry *entry;

    entry = GTK_ENTRY(gtk_entry_new());
    gtk_editable_set_editable(GTK_EDITABLE(entry), FALSE);
    gtk_box_pack_start(GTK_BOX(status_bar), GTK_WIDGET(entry), grow, TRUE, 0);

    /* We don't want the blinking cursor. */
    GTK_WIDGET_UNSET_FLAGS(GTK_WIDGET(entry), GTK_CAN_FOCUS);

    set_entry_text(entry, init);

    return entry;
}

static void create_status_bar(void)
{
    status_bar = GTK_HBOX(gtk_hbox_new(FALSE, 0));

    entry_ident = add_entry(TRUE, _("No image loaded"));
    entry_size = add_entry(FALSE, _("width x height"));
    entry_state = add_entry(FALSE, _("zoom% (angle)"));
}

/* Returns TRUE if the widgets has been toggled. */
gboolean toggle_widgets(GtkWidget ** widgets, gint nb_widgets, gboolean * flag)
{
    /* We are not reentrant. */
    static gboolean toggling = FALSE;
    gint w, h;
    gint i;

    if (toggling)
        return FALSE;

    toggling = TRUE;
    w = rt->wid_size->width;
    h = rt->wid_size->height;

    for (i = 0; i < nb_widgets; i++) {
        if (*flag == FALSE)
            gtk_widget_show(widgets[i]);
        else
            gtk_widget_hide(widgets[i]);
    }

    *flag ^= TRUE;
    toggling = FALSE;
    return TRUE;
}

static gboolean toggle_widget(GtkWidget * widget, gboolean * flag)
{
    return toggle_widgets(&widget, 1, flag);
}

gboolean toggle_menu_bar(void)
{
    toggle_widget(GTK_WIDGET(menu_bar), &options->menu_bar);
    return FALSE;
}

gboolean toggle_status_bar(void)
{
    toggle_widget(GTK_WIDGET(status_bar), &options->status_bar);
    return FALSE;
}

/* To take into account the new filename, zoom, angle, symmetry. */
void update_status_bar(void)
{
    gchar *size_str, *state_str;
    const gchar *sym;
    gfloat zoom, angle;

    if (current_image == NULL)
        /* Loading first image */
        set_ident_text("");
    else if (current_image->ident != NULL) {
        /* Filename and dimensions status, only the first time. */
        set_ident_text(current_image->ident);
        g_free(current_image->ident);
        current_image->ident = NULL;

        size_str = g_strdup_printf("%dx%d",
                                   current_image->width, current_image->height);
        set_entry_text(entry_size, size_str);
        g_free(size_str);
    }

    /* The state */

    zoom = get_matrix_zoom() * 100.0;
    angle = get_matrix_angle() * 180.0 / PI;
    sym = is_matrix_symmetry()? " /" : "";

    /* We don't want -0.000 */
    if (float_equal(angle, 0.0))
        angle = 0.0;

    state_str = g_strdup_printf(_("%.3f%% (%.3f deg%s)"), zoom, angle, sym);
    set_entry_text(entry_state, state_str);

    g_free(state_str);
}

/* Does the WM support the EWMH protocol? */
static gboolean has_net_wm(void)
{
    return gdk_net_wm_supports(gdk_atom_intern("_NET_WM_STATE_FULLSCREEN",
                                               FALSE));
}

/*
 * On the first map, all widgets are shown, we have to hide
 * those that are not requested.
 */
static gboolean first_map(void)
{
    if (options->menu_bar == FALSE)
        gtk_widget_hide(GTK_WIDGET(menu_bar));

    if (options->status_bar == FALSE)
        gtk_widget_hide(GTK_WIDGET(status_bar));

    if (options->scrollbars == FALSE)
        hide_scrollbars();

    if (options->fullscreen && has_net_wm())
        toggle_fullscreen(TRUE);

    return FALSE;
}

static void set_initial_geometry(void)
{
    GdkGeometry size_hints = {
        1, 1, 0, 0, 1, 1, 1, 1, 0.0, 0.0, GDK_GRAVITY_NORTH_WEST
    };

    if (!options->initial_geometry)
        goto fail;

    if (options->fullscreen) {
        g_printerr(_("Ignoring initial geometry (%s) as"
                     " fullscreen mode is requested\n"),
                   options->initial_geometry);
        goto fail;
    }

    gtk_window_set_geometry_hints(main_window, GTK_WIDGET(main_window),
                                  &size_hints,
                                  GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE |
                                  GDK_HINT_RESIZE_INC);

    if (!gtk_window_parse_geometry(main_window, options->initial_geometry)) {
        g_printerr(_("Cannot parse geometry: %s\n"), options->initial_geometry);
        goto fail;
    }

    return;

  fail:
    g_free(options->initial_geometry);
    options->initial_geometry = NULL;
}

void create_windows(void)
{
    GtkWindow *first_window;
    GtkAccelGroup *accel_group;
    GtkHBox *hbox;
    GdkPixbuf *logo = get_gliv_logo();

    if (logo != NULL)
        gtk_window_set_default_icon(logo);

    rt->scr_width = gdk_screen_width();
    rt->scr_height = gdk_screen_height();

    /*
     * The main window: seen when not in fullscreen,
     * or when using the NET WM protocol.
     */
    main_window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
    gtk_window_set_title(main_window, "GLiv");
    install_callbacks(main_window);

    /* The window seen in fullscreen mode. */
    fs_window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
    gtk_window_set_title(fs_window, _("GLiv in fullscreen"));

    /* Make it fullscreen. */
    gtk_window_move(fs_window, 0, 0);
    gtk_window_set_default_size(fs_window, rt->scr_width, rt->scr_height);
    gtk_window_set_decorated(fs_window, FALSE);
    install_callbacks(fs_window);

    /* The widgets. */
    create_gl_widget();
    accel_group = create_menus();
    create_status_bar();

    /* Bind keyboard accelerators to windows. */
    gtk_window_add_accel_group(main_window, accel_group);
    gtk_window_add_accel_group(fs_window, accel_group);

    /* Collection. */
    widgets = GTK_VBOX(gtk_vbox_new(FALSE, 0));

    /* The menu bar on the top. */
    gtk_box_pack_start(GTK_BOX(widgets), GTK_WIDGET(menu_bar), FALSE, FALSE, 0);

    /* The OpenGL widget and the vertical scrollbar under the menu bar. */
    hbox = GTK_HBOX(gtk_hbox_new(FALSE, 0));
    gtk_box_pack_start_defaults(GTK_BOX(hbox), gl_widget);
    gtk_box_pack_start(GTK_BOX(hbox), get_new_scrollbar(FALSE), FALSE, FALSE,
                       0);
    gtk_box_pack_start_defaults(GTK_BOX(widgets), GTK_WIDGET(hbox));

    /* The horizontal scrollbar under them. */
    gtk_box_pack_start(GTK_BOX(widgets), get_new_scrollbar(TRUE), FALSE, FALSE,
                       0);

    /* The status bar in the bottom. */
    gtk_box_pack_end(GTK_BOX(widgets), GTK_WIDGET(status_bar), FALSE, FALSE, 0);

    first_window = (options->fullscreen && !has_net_wm())?
        fs_window : main_window;

    gtk_container_add(GTK_CONTAINER(first_window), GTK_WIDGET(widgets));
    g_signal_connect_after(first_window, "map", G_CALLBACK(first_map), NULL);
    gtk_widget_show_all(GTK_WIDGET(widgets));
    set_initial_geometry();
    gtk_widget_show_all(GTK_WIDGET(first_window));
}

/*
 * Called when going from fullscreen mode to window mode, or when
 * the displayed image changed in window mode.
 */
void goto_window(GlivImage * im, gboolean first_time)
{
    gint new_width, new_height;

    if (im == NULL)
        im = current_image;

    if (im == NULL) {
        new_width = 0;
        new_height = 0;
    } else {
        new_width = im->width;
        new_height = im->height;
    }

    if (new_width >= rt->scr_width)
        new_width = 3 * rt->scr_width / 4;

    if (new_height >= rt->scr_height)
        new_height = 3 * rt->scr_height / 4;

    resize_window(new_width, new_height, first_time);
}

/*
 * Return TRUE if we managed to toggle the fullscreen mode using the
 * NET WM protocol.
 */
static gboolean net_wm_toggle_fullscreen(gboolean enable)
{
    if (get_current_window() != main_window || has_net_wm() == FALSE)
        return FALSE;

    if (enable)
        gtk_window_fullscreen(main_window);
    else
        gtk_window_unfullscreen(main_window);

    return TRUE;
}

void toggle_fullscreen(gboolean enable)
{
    /* We first try using the NET WM protocol, then with the old method. */
    if (net_wm_toggle_fullscreen(enable) == FALSE) {
        GtkWindow *new, *old;

        if (enable) {
            /* Go to fullscreen mode. */
            new = fs_window;
            old = main_window;
        } else {
            /* Go to window mode. */
            new = main_window;
            old = fs_window;
        }

        gtk_widget_show(GTK_WIDGET(new));
        gtk_widget_hide(GTK_WIDGET(old));

        gtk_widget_reparent(GTK_WIDGET(widgets), GTK_WIDGET(new));

    }

    if (enable == FALSE)
        goto_window(NULL, FALSE);

    options->fullscreen = enable;
    schedule_hide_cursor();
}

void update_window_title(void)
{
    const gchar *path;
    gchar *basename, *title;

    if (current_image->node->data == NULL)
        return;

    path = filename_to_utf8(current_image->node->data);
    basename = g_path_get_basename(path);
    title = g_strdup_printf("GLiv - %s", basename);
    gtk_window_set_title(main_window, title);
    g_free(basename);
    g_free(title);
}

GtkWindow *get_current_window(void)
{
    if (GTK_IS_WIDGET(gl_widget) && GTK_WIDGET_VISIBLE(gl_widget))
        return GTK_WINDOW(gtk_widget_get_toplevel(gl_widget));

    return NULL;
}

GtkWindow *new_window(const gchar * title)
{
    GtkWindow *win = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
    GtkWindow *parent = get_current_window();

    gtk_window_set_title(win, title);
    gtk_window_set_transient_for(win, parent);
    gtk_window_set_destroy_with_parent(win, TRUE);
    gtk_window_set_type_hint(win, GDK_WINDOW_TYPE_HINT_DIALOG);

    return win;
}

gboolean toggle_floating_windows(void)
{
    static GList *hidden_windows;
    GList *ptr;

    if (hidden_windows) {
        for (ptr = hidden_windows; ptr; ptr = ptr->next) {
            gtk_widget_show(GTK_WIDGET(ptr->data));
            g_object_unref(ptr->data);
        }

        g_list_free(hidden_windows);
        hidden_windows = NULL;
    } else {
        GList *toplevels = gtk_window_list_toplevels();

        /* First two windows are the main window and the full screen one */
        for (ptr = toplevels->next->next; ptr; ptr = ptr->next) {
            GtkWidget *widget = GTK_WIDGET(ptr->data);
            if (GTK_WIDGET_VISIBLE(widget)) {
                gtk_widget_hide(widget);
                hidden_windows = g_list_prepend(hidden_windows, widget);
                g_object_ref(widget);
            }
        }
        g_list_free(toplevels);
    }

    return FALSE;
}
