/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * gwin.c: gtk window management routines for pho, an image viewer.
 *
 * Copyright 2004,2009 by Akkana Peck.
 * You are free to use or modify this code under the Gnu Public License.
 */

#include "pho.h"
#include "dialogs.h"
#include "exif/phoexif.h"

#include <stdio.h>
#include <stdlib.h>       /* for exit() */
#include <string.h>
#include <unistd.h>

#include <gdk/gdk.h>

/* Some window managers don't deal well with windows that resize,
 * or don't retain focus if a resized window no longer contains
 * the mouse pointer.
 * Offer an option to make new windows instead.
 */
int gMakeNewWindows = 0;

/* The size our window frame adds on the top left of the image */
gint sFrameWidth = 10;
gint sFrameHeight = 28;
int sHaveFrameSize = 0;

gint gPhysMonitorWidth = 0;
gint gPhysMonitorHeight = 0;

/* Initialize the "black" color */
static GdkColor sBlack = { 0x0000, 0x0000, 0x0000 };

/* gtk related window attributes */
GtkWidget *gWin = 0;
static GtkWidget *sDrawingArea = 0;

/* This is so gross. GTK has no way to tell if a window has been exposed.
 * GTK_WIDGET_MAPPED and GTK_WIDGET_VISIBLE are both true before the
 * main loop has run or an expose event has happened.
 */
static int sExposed = 0;

static void NewWindow(); /* forward */

static void hide_cursor(GtkWidget* w)
{
    static char invisible_cursor_bits[] = { 0x0 };
    static GdkBitmap *empty_bitmap = 0;
    static GdkCursor* cursor = 0;

    if (empty_bitmap == 0 || cursor == 0) {
        empty_bitmap = gdk_bitmap_create_from_data(NULL,
                                                   invisible_cursor_bits,
                                                   1, 1);
        cursor = gdk_cursor_new_from_pixmap (empty_bitmap, empty_bitmap,
                                             &sBlack, &sBlack, 1, 1);
    }

    if (w->window != NULL)
        gdk_window_set_cursor(w->window, cursor);

    /* If we need to free this, do it this way:
    gdk_cursor_unref(cursor);
    g_object_unref(empty_bitmap);
     */
}

static void show_cursor(GtkWidget* w)
{
    gdk_window_set_cursor(w->window, NULL);
}

/*
 * Adjust gMonitorWidth/Height and sFrameWidth/Height
 * according to the display mode and current frames,
 * then resize the image accordingly.
 * Call this when the image size changes --
 * zoom, shift in/out of presentation mode, etc.
 */
static void AdjustScreenSize()
{
    if (gDisplayMode == PHO_DISPLAY_PRESENTATION) {
        gMonitorWidth = gPhysMonitorWidth;
        gMonitorHeight = gPhysMonitorHeight;
    }
    else {
        if (!sHaveFrameSize && GTK_WIDGET_MAPPED(gWin)) {
            gint width, height;
            GdkRectangle rect;

            if (gDebug)
                printf("AdjustScreenSize: Requesting frame size\n");
            gdk_drawable_get_size(gWin->window, &width, &height);
            gdk_window_get_frame_extents(gWin->window, &rect);
            sFrameWidth = rect.width - width;
            sFrameHeight = rect.height - height;
            sHaveFrameSize = 1;
        }

        if (gDebug)
            printf("AdjustScreenSize: Frame size: %d, %d\n",
                   sFrameWidth, sFrameHeight);

        gMonitorWidth = gPhysMonitorWidth - sFrameWidth;
        gMonitorHeight = gPhysMonitorHeight - sFrameHeight;
    }

    ScaleAndRotate(gCurImage, 0);
}

static void CenterWindow(GtkWidget* win)
{
    gint w, h;
    if (gDebug) printf("CenterWindow\n");
    gtk_window_get_size(GTK_WINDOW(gWin), &w, &h);
    gtk_window_set_gravity(GTK_WINDOW(win), GDK_GRAVITY_CENTER);
    gtk_window_move(GTK_WINDOW(win),
                    (gMonitorWidth - gCurImage->curWidth)/2,
                    (gMonitorHeight - gCurImage->curHeight)/2);
    gtk_window_set_gravity(GTK_WINDOW(win), GDK_GRAVITY_NORTH_WEST);
}

/* If we  resized, see if we might move off the screen.
 * Make sure we don't do that, assuming the image is still
 * small enough to fit. Then request focus from the window manager.
 * Try to move the window as little as possible.
 */
static void MaybeMove()
{
    gint x, y, w, h, nx, ny;

    if (gDebug) printf("MaybeMove\n");
    if (gCurImage->curWidth > gPhysMonitorWidth
        || gCurImage->curHeight > gPhysMonitorHeight)
        return;

    /* If we don't have a window yet, don't move it */
    if (!gWin || !GTK_WIDGET_MAPPED(gWin))
        return;

    /* If we're in presentation or keywords mode, never move the window */
    if (gDisplayMode == PHO_DISPLAY_PRESENTATION
        || gDisplayMode == PHO_DISPLAY_KEYWORDS)
        return;

    gtk_window_get_position(GTK_WINDOW(gWin), &x, &y);
    nx = x;  ny = y;
    gtk_window_get_size(GTK_WINDOW(gWin), &w, &h);
    /* printf("Currently (%d, %d) %d x %d\n", x, y, w, h); */

    /* See if it would overflow off the screen */
    if (x + gCurImage->curWidth > gMonitorWidth)
        nx = gMonitorWidth - gCurImage->curWidth;
    if (nx < 0)
        nx = 0;
    if (y + gCurImage->curHeight > gMonitorHeight)
        ny = gMonitorHeight - gCurImage->curHeight;
    if (ny < 0)
        ny = 0;

    if (x != nx || y != ny) {
        gtk_window_move(GTK_WINDOW(gWin), nx, ny);
    }

    /* Request focus from the window manager.
     * This is pretty much a no-op, but what the heck.
     * However, it also messes with NewWindow's ability
     * to get the frame size because it maps the window before
     * we call gtk_widget_show().
    gtk_window_present(GTK_WINDOW(gWin));
     */
}

double FracOfScreenSize() {
    if (gMonitorWidth == 0 || gMonitorHeight == 0)
        return 0.0;    /* Signal to re-call this function later */

    if (gMonitorWidth > gMonitorHeight)
        return gMonitorHeight * .95;
    return gMonitorWidth * .95;
}

/* Change the view (display and scale) modes,
 * and re-display the image if necessary.
 */
/* XXX Pho really needs a state table defining which commands
 * take which state to which new state! What a mess.
 */
int SetViewModes(int dispmode, int scalemode, double scalefactor)
{
    if (dispmode == gDisplayMode && scalemode == gScaleMode
        && scalefactor == gScaleRatio)
        return 0;
    if (gDebug)
        printf("SetViewModes(%d, %d, %f (was %d, %d, %f)\n",
               dispmode, scalemode, scalefactor,
               gDisplayMode, gScaleMode, gScaleRatio);

    if (dispmode == PHO_DISPLAY_KEYWORDS) {
        if (gDisplayMode != PHO_DISPLAY_KEYWORDS) {
            /* switching to keyword mode from some other mode */
            gScaleMode = PHO_SCALE_FIXED;
            gScaleRatio = FracOfScreenSize();
            /*
            if (gDebug)
                printf("Showing keywords dialog from SetViewModes\n");
            ShowKeywordsDialog();
             */
        }
        else {
            /* staying in keywords mode but changing some other factor:
             * don't force anything.
             */
            gScaleMode = scalemode;
            gScaleRatio = scalefactor;
        }
    }
    else {
        if (gDisplayMode == PHO_DISPLAY_KEYWORDS)  /* leaving keywords mode */
            HideKeywordsDialog();
        gScaleMode = scalemode;
        gScaleRatio = scalefactor;
    }

    if (dispmode != gDisplayMode) {
        gDisplayMode = dispmode;
        if (gWin && sDrawingArea && GTK_WIDGET_MAPPED(gWin))
            /* Changing an existing window */
        {
            if (dispmode == PHO_DISPLAY_PRESENTATION) {
                if (gDebug)
                    printf("\nSetViewModes: changing to presentation mode\n");
                hide_cursor(sDrawingArea);
                gtk_window_fullscreen(GTK_WINDOW(gWin));
                gtk_window_move(GTK_WINDOW(gWin), 0, 0);
                /* Is it still necessary to move a fullscreen window? */

                AdjustScreenSize();
            }
            else {
                if (gDebug)
                    printf("\nSetViewModes: changing to normal mode\n");
                show_cursor(sDrawingArea);
                gtk_window_unfullscreen(GTK_WINDOW(gWin));
                AdjustScreenSize();
                if (gDebug)
                    printf("Monitor size after AdjustScreenSize: %dx%d\n",
                           gMonitorWidth, gMonitorHeight);
                CenterWindow(gWin);
            }
        }
    }
    else {
        /* If we're not changing the display mode, then we're changing
         * the scale mode or scale factor, and may need to move to
         * keep the window under the mouse so as not to lose focus
         * or move off the screen.
         * XXX unfortunately this doesn't work when changing from keywords
         * to fullscreen/normal.
         */
        int ret = ScaleAndRotate(gCurImage, 0);
        if (ret != 0) return ret;
        MaybeMove();
    }

    if (gDisplayMode == PHO_DISPLAY_KEYWORDS) {
        ShowKeywordsDialog();
    }

    return 0;
}

/* DrawImage is called from the expose callback.
 * It assumes we already have the image in gImage.
 */
void DrawImage()
{
    int dstX = 0, dstY = 0;
    char title[BUFSIZ];
#   define TITLELEN ((sizeof title) / (sizeof *title))

    if (gDebug) {
        printf("DrawImage %s, %dx%d\n", gCurImage->filename,
               gCurImage->curWidth, gCurImage->curHeight);
    }
    if (gImage == 0 || gWin == 0 || sDrawingArea == 0) return;
    if (!sExposed) return;
    if (!GTK_WIDGET_MAPPED(gWin)) return;

    if (gDisplayMode == PHO_DISPLAY_PRESENTATION) {
        gint width, height;
        gdk_window_clear(sDrawingArea->window);

        /* Center the image. This has to be done according to
         * the current window size, not the phys monitor size,
         * because in the xinerama case, gtk_window_fullscreen()
         * only fullscreens the current monitor, not all of them.
         */
        gtk_window_get_size(GTK_WINDOW(gWin), &width, &height);

        /* If we have a presentation screen size set (e.g. for a projector
         * that has a different resolution from our native monitor),
         * Fudge the screen size and center based on a virtual screen
         * starting in the upper left corner of our current screen.
         * That way, it will center on the projector or other device.
         */
        if (gPresentationWidth > 0)
            width = gPresentationWidth;
        if (gPresentationHeight > 0)
            height = gPresentationHeight;

        dstX = (width - gCurImage->curWidth) / 2;
        dstY = (height - gCurImage->curHeight) / 2;
    }
    else {
        /* Update the titlebar */
        sprintf(title, "pho: %s (%d x %d)", gCurImage->filename,
                gCurImage->trueWidth, gCurImage->trueHeight);
        if (HasExif())
        {
            const char* date = ExifGetString(ExifDate);
            if (date && date[0]) {
                /* Make sure there's room */
                if (strlen(title) + strlen(date) + 3 < TITLELEN)
                    strcat(title, " (");
                strcat(title, date);
                strcat(title, ")");
            }
        }
        /* XXX replace these strcats with safer strncat */
        if (gScaleMode == PHO_SCALE_FULLSIZE)
            strcat(title, " (fullsize)");
        else if (gScaleMode == PHO_SCALE_FULLSCREEN)
            strcat(title, " (fullscreen)");
        else if (gScaleMode == PHO_SCALE_IMG_RATIO ||
                 gScaleMode == PHO_SCALE_SCREEN_RATIO) {
            char* titleEnd = title + strlen(title);
            if (gScaleRatio < 1)
                sprintf(titleEnd, " [%s/ %d]",
                        (gScaleMode == PHO_SCALE_IMG_RATIO ? "fullsize " : ""),
                        (int)(1. / gScaleRatio));
            else
                sprintf(titleEnd, " [%s* %d]",
                        (gScaleMode == PHO_SCALE_IMG_RATIO ? "fullsize " : ""),
                        (int)gScaleRatio);
        }
        else if (gScaleMode == PHO_SCALE_FIXED)
            strcat(title, " (fixed)");
        gtk_window_set_title(GTK_WINDOW(gWin), title);

        if (gDisplayMode == PHO_DISPLAY_KEYWORDS) {
            if (gDebug)
                printf("Showing keywords dialog from DrawImage\n");
            ShowKeywordsDialog(gCurImage);
        }
    }

    gdk_pixbuf_render_to_drawable(gImage, sDrawingArea->window,
                   sDrawingArea->style->fg_gc[GTK_WIDGET_STATE(sDrawingArea)],
                                  0, 0, dstX, dstY,
                                  gCurImage->curWidth, gCurImage->curHeight,
                                  GDK_RGB_DITHER_NONE, 0, 0);

    UpdateInfoDialog(gCurImage);
}

/* An expose event has a GdkRectangle area and a GdkRegion *region
 * as well as gint count of subsequent expose events.
 * Unfortunately count is always 0.
 */
static gint HandleExpose(GtkWidget* widget, GdkEventExpose* event)
{
    gint width, height;

    sExposed = 1;
    gdk_drawable_get_size(widget->window, &width, &height);
    if (gDebug) {
        printf("HandleExpose: area %dx%d +%d+%d in window %dx%d\n",
               event->area.width, event->area.height,
               event->area.x, event->area.y,
               width, height);
        if ((gDisplayMode != PHO_DISPLAY_PRESENTATION) &&
            (event->area.x != 0 || event->area.y != 0)) {
            if (event->area.width != width || event->area.height != height)
                printf("*** Expose different from window size!\n");
            if (event->area.width != gCurImage->curWidth ||
                event->area.height != gCurImage->curHeight)
                printf("** Expose different from actual image size of %dx%d!\n",
                       gCurImage->curWidth, gCurImage->curHeight);
        }
    }

    /* Make sure the window can resize smaller */
    if (!gMakeNewWindows)
        gtk_widget_set_size_request(GTK_WIDGET(gWin), 1, 1);

    if (!sHaveFrameSize && gDisplayMode != PHO_DISPLAY_PRESENTATION) {
        int oldw = sFrameWidth;
        int oldh = sFrameHeight;
        AdjustScreenSize();
        if (sFrameWidth != oldw || sFrameHeight != oldh) {
            gtk_window_resize(GTK_WINDOW(gWin),
                              gCurImage->curWidth, gCurImage->curHeight);
            /* Since we resized, we might no longer be over the cursor
             * and may have lost focus:
             */
            MaybeMove();
        }
    }

    DrawImage();

    return TRUE;
}

void EndSession()
{
    gCurImage = 0;
    UpdateInfoDialog();
    RememberKeywords();
    PrintNotes();
    gtk_main_quit();
    /* This doesn't always quit!  So make sure: */
    exit(0);
}

static gint HandleDelete(GtkWidget* widget, GdkEventKey* event, gpointer data)
{
    /* If you return FALSE in the "delete_event" signal handler,
     * GTK will emit the "destroy" signal. Returning TRUE means
     * you don't want the window to be destroyed.
     * This is useful for popping up 'are you sure you want to quit?'
     * type dialogs. */
    EndSession();
    return TRUE; /* Never called -- just here to keep gcc happy. */
}

/* Make a new window, destroying the old one. */
static void NewWindow()
{
    gint root_x = -1;
    gint root_y = -1;

    sExposed = 0;    /* reset the exposed flag */

    if (gDebug)
        printf("NewWindow()\n");

    if (gWin) {
        gdk_window_get_position(gWin->window, &root_x, &root_y);
        gtk_object_destroy(GTK_OBJECT(gWin));
    }

    gWin = gtk_window_new(GTK_WINDOW_TOPLEVEL);

    gtk_window_set_wmclass(GTK_WINDOW(gWin), "pho", "Pho");

    /* Window manager delete */
    gtk_signal_connect(GTK_OBJECT(gWin), "delete_event",
                       (GtkSignalFunc)HandleDelete, 0);

    /* This event occurs when we call gtk_widget_destroy() on the window,
     * or if we return FALSE in the "delete_event" callback.
    gtk_signal_connect(GTK_OBJECT(gWin), "destroy",
                       (GtkSignalFunc)HandleDestroy, 0);
     */

    /* KeyPress events on the drawing area don't come through --
     * they have to be on the window.
     */
    gtk_signal_connect(GTK_OBJECT(gWin), "key_press_event",
                       (GtkSignalFunc)HandleGlobalKeys, 0);

    sDrawingArea = gtk_drawing_area_new();
    gtk_container_add(GTK_CONTAINER(gWin), sDrawingArea);
    gtk_widget_show(sDrawingArea);

    /* This can't be done in expose: it causes one of those extra
     * spurious expose events that gtk so loves.
     */
    gtk_widget_modify_bg(sDrawingArea, GTK_STATE_NORMAL, &sBlack);

    /* We hope this has already been done: LoadImageAndRotate
     * should have called ScaleAndRotate when the image was loaded.
    AdjustScreenSize();
     */
    if (gDisplayMode == PHO_DISPLAY_PRESENTATION) {
        gtk_drawing_area_size(GTK_DRAWING_AREA(sDrawingArea),
                              gPhysMonitorWidth, gPhysMonitorHeight);
        gtk_window_fullscreen(GTK_WINDOW(gWin));
    }
    else {
        gtk_drawing_area_size(GTK_DRAWING_AREA(sDrawingArea),
                              gCurImage->curWidth, gCurImage->curHeight);
        gtk_window_unfullscreen(GTK_WINDOW(gWin));
    }

    gtk_signal_connect(GTK_OBJECT(sDrawingArea), "expose_event",
                       (GtkSignalFunc)HandleExpose, 0);
    /* To track in/out of fullscreen mode, use configure_event
     * or window_state_event.
     */

    gtk_widget_show(gWin);

    /* Must come after show(), hide_cursor needs a window */
    if (gDisplayMode == PHO_DISPLAY_PRESENTATION)
        hide_cursor(sDrawingArea);

}

/**
 * gdk_window_focus:
 * @window: a #GdkWindow
 * @timestamp: timestamp of the event triggering the window focus
 *
 * Sets keyboard focus to @window. If @window is not onscreen this
 * will not work. In most cases, gtk_window_present() should be used on
 * a #GtkWindow, rather than calling this function.
 *
 * For Pho: this is a replacement for gdk_window_focus
 * due to the issue in http://bugzilla.gnome.org/show_bug.cgi?id=150668
 * 
 **/
#define GDK_WINDOW_DISPLAY(win)       gdk_drawable_get_display(win)
#define GDK_WINDOW_SCREEN(win)	      gdk_drawable_get_screen(win)
#define GDK_WINDOW_XROOTWIN(win)      GDK_ROOT_WINDOW()

/* PrepareWindow is responsible for making the window the right
 * size and position, so the user doesn't see flickering.
 * It may actually make a new window, or it may just resize and/or
 * reposition the existing window.
 */
void PrepareWindow()
{
    if (gMakeNewWindows || gWin == 0) {
        NewWindow();
        return;
    }

    /* If the window is new but hasn't been mapped yet,
     * there's nothing we can do from here.
     */
    if (!GTK_WIDGET_MAPPED(gWin))
        return;

    /* Otherwise, resize and reposition the current window. */

    if (gDisplayMode == PHO_DISPLAY_PRESENTATION) {
        /* XXX shouldn't have to do this every time */
        gtk_drawing_area_size(GTK_DRAWING_AREA(sDrawingArea),
                              gPhysMonitorWidth, gPhysMonitorHeight);
    }
    else {
        gint winwidth, winheight;

        gdk_drawable_get_size(gWin->window, &winwidth, &winheight);
        gdk_drawable_get_size(sDrawingArea->window, &winwidth, &winheight);

        /* We need to size the actual window, not just the drawing area.
         * Resizing the drawing area will resize the window for many
         * window managers, but not for metacity.
         *
         * Worse, metacity maximizes a window if the initial size is
         * bigger in either dimension than the screen size.
         * Since we can't be sure about the size of the wm decorations,
         * we will probably hit this and get unintentionally maximized,
         * after which metacity refuses to resize the window any smaller.
         * (Mac OS X apparently does this too.)
         * So force non-maximal mode.  (Users who want a maximized
         * window will probably prefer fullscreen mode anyway.)
         */
        gtk_window_unfullscreen(GTK_WINDOW(gWin));
        gtk_window_unmaximize(GTK_WINDOW(gWin));

        /* XXX Without the next line, we may get no expose events!
         * Likewise, if the next line doesn't actually resize anything
         * we may not get an expose event.
         */
        if (gCurImage->curWidth != winwidth
            || gCurImage->curHeight != winheight) {
            gtk_window_resize(GTK_WINDOW(gWin),
                              gCurImage->curWidth, gCurImage->curHeight);

            /* Unfortunately, on OS X this resize may not work,
             * if it puts part ofthe window off-screen; in which case
             * we won't get an Expose event. So if that happened,
             * force a redraw:
             */
            /* Just as unfortunately, get_size probably isn't reliable
             * on Linux either, until after the window manager has had
             * a chance to act on the resize request
             * (at which time we'll get another Configure notify).
             * So the dilemma is: we need to call DrawImage now in the
             * case where there will be no further events. But if there
             * are further events, we want to wait for them and not
             */
            gdk_drawable_get_size(sDrawingArea->window, &winwidth, &winheight);
            if (gCurImage->curWidth != winwidth
                || gCurImage->curHeight != winheight) {
                if (gDebug)
                    printf("Resize didn't work! Forcing redraw\n");
                DrawImage();
            }
        }

        /* If we didn't resize the window, then we won't get an expose
         * event, and hence DrawImage won't be called. So call it explicitly
         * -- but not if we haven't displayed a window yet.
         * If we do that, we get duplicate calls to DrawImage
         * plus in keywords mode, the keywords dialog gets set as
         * transient too early. (I hate window management.)
         */
        else if (GTK_WIDGET_VISIBLE(gWin)) {
            DrawImage();
        }

        /* Try to ensure that the window will be over the cursor
         * (so it will still have focus -- some window managers will
         * lose focus otherwise). But not in Keywords mode, where the
         * mouse should be over the Keywords dialog, not necessarily
         * the image window.
         */
        MaybeMove();
    }

    /* Want to request the focus here, but
     * neither gtk_window_present nor gdk_window_focus seem to work.
     */
}

