#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <cairo.h>
#include <sys/time.h>
#include <sys/timeb.h>
#include <gtk/gtk.h>
#include <math.h>

#include "clock.h"
#include "clock-map.h"
#include "clock-sunpos.h"
#include "clock-marshallers.h"

G_DEFINE_TYPE (ClockMap, clock_map, GTK_TYPE_WIDGET)

enum {
	NEED_LOCATIONS,
	LAST_SIGNAL
};

enum {
        MARKER_NORMAL = 0,
        MARKER_HILIGHT,
        MARKER_CURRENT,
        MARKER_NB
};

static char *marker_files[MARKER_NB] = {
        ICONDIR "/clock-map-location-marker.png",
        ICONDIR "/clock-map-location-hilight.png",
        ICONDIR "/clock-map-location-current.png"
};

static guint signals[LAST_SIGNAL] = { 0 };

typedef struct {
        time_t last_refresh;

        gint width;
        gint height;

	guint highlight_timeout_id;

        GdkPixbuf *stock_map_pixbuf;
        GdkPixbuf *location_marker_pixbuf[MARKER_NB];

        GdkPixbuf *location_map_pixbuf;

        /* The shadow itself */
        GdkPixbuf *shadow_pixbuf;

        /* The map with the shadow composited onto it */
        GdkPixbuf *shadow_map_pixbuf;
} ClockMapPrivate;

static void clock_map_finalize (GObject *);
static void clock_map_size_allocate (GtkWidget *this,
					 GtkAllocation *allocation);
#if GTK_CHECK_VERSION (3, 0, 0)
static gboolean clock_map_draw (GtkWidget *this,
				      cairo_t *cr);
static void clock_map_get_preferred_width (GtkWidget *widget,
					   gint *minimum_width,
					   gint *natural_width);
static void clock_map_get_preferred_height (GtkWidget *widget,
					    gint *minimum_height,
					    gint *natural_height);
#else
static gboolean clock_map_expose (GtkWidget *this,
				      GdkEventExpose *expose);
static void clock_map_size_request (GtkWidget *this,
				    GtkRequisition *requisition);
#endif
static void clock_map_place_locations (ClockMap *this);
static void clock_map_render_shadow (ClockMap *this);
static void clock_map_display (ClockMap *this);

#define PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), CLOCK_MAP_TYPE, ClockMapPrivate))

ClockMap *
clock_map_new (void)
{
        ClockMap *this;

        this = g_object_new (CLOCK_MAP_TYPE, NULL);

        return this;
}

static void
clock_map_class_init (ClockMapClass *this_class)
{
        GObjectClass *g_obj_class = G_OBJECT_CLASS (this_class);
        GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (g_obj_class);

        g_obj_class->finalize = clock_map_finalize;

        /* GtkWidget signals */

        widget_class->size_allocate = clock_map_size_allocate;
#if GTK_CHECK_VERSION (3, 0, 0)
        widget_class->draw = clock_map_draw;
	widget_class->get_preferred_width = clock_map_get_preferred_width;
	widget_class->get_preferred_height = clock_map_get_preferred_height;
#else
        widget_class->expose_event = clock_map_expose;
        widget_class->size_request = clock_map_size_request;
#endif

        g_type_class_add_private (this_class, sizeof (ClockMapPrivate));

	/**
	 * ClockMap::need-locations
	 *
	 * The map widget emits this signal when it needs to know which
	 * locations to display.
	 *
	 * Returns: the handler should return a (GList *) of (ClockLocation *).
	 * The map widget will not modify this list, so the caller should keep
	 * it alive.
	 */
	signals[NEED_LOCATIONS] = g_signal_new ("need-locations",
						G_TYPE_FROM_CLASS (g_obj_class),
						G_SIGNAL_RUN_LAST,
						G_STRUCT_OFFSET (ClockMapClass, need_locations),
						NULL,
						NULL,
						_clock_marshal_POINTER__VOID,
						G_TYPE_POINTER, 0);
}

static void
clock_map_init (ClockMap *this)
{
        int i;
        ClockMapPrivate *priv = PRIVATE (this);

        gtk_widget_set_has_window (GTK_WIDGET (this), FALSE);

	priv->last_refresh = 0;
	priv->width = 0;
	priv->height = 0;
	priv->highlight_timeout_id = 0;
        priv->stock_map_pixbuf = NULL;

        g_assert (sizeof (marker_files)/sizeof (char *) == MARKER_NB);

        for (i = 0; i < MARKER_NB; i++) {
                priv->location_marker_pixbuf[i] = gdk_pixbuf_new_from_file
                                                  (marker_files[i], NULL);
        }
}

static void
clock_map_finalize (GObject *g_obj)
{
        ClockMapPrivate *priv = PRIVATE (g_obj);
	int i;

	if (priv->highlight_timeout_id) {
		g_source_remove (priv->highlight_timeout_id);
		priv->highlight_timeout_id = 0;
	}

        if (priv->stock_map_pixbuf) {
                g_object_unref (priv->stock_map_pixbuf);
                priv->stock_map_pixbuf = NULL;
        }

	for (i = 0; i < MARKER_NB; i++) {
        	if (priv->location_marker_pixbuf[i]) {
                	g_object_unref (priv->location_marker_pixbuf[i]);
                	priv->location_marker_pixbuf[i] = NULL;
        	}
	}

        if (priv->location_map_pixbuf) {
                g_object_unref (priv->location_map_pixbuf);
                priv->location_map_pixbuf = NULL;
        }

        if (priv->shadow_pixbuf) {
                g_object_unref (priv->shadow_pixbuf);
                priv->shadow_pixbuf = NULL;
        }

        if (priv->shadow_map_pixbuf) {
                g_object_unref (priv->shadow_map_pixbuf);
                priv->shadow_map_pixbuf = NULL;
        }

        G_OBJECT_CLASS (clock_map_parent_class)->finalize (g_obj);
}

void
clock_map_refresh (ClockMap *this)
{
        ClockMapPrivate *priv = PRIVATE (this);
	GtkWidget *widget = GTK_WIDGET (this);
	GtkAllocation allocation;

	gtk_widget_get_allocation (widget, &allocation);

        /* Only do something if we have some space allocated.
         * Note that 1x1 is not really some space... */
        if (allocation.width <= 1 || allocation.height <= 1)
                return;

        /* Allocation changed => we reload the map */
	if (priv->width != allocation.width ||
	    priv->height != allocation.height) {
                if (priv->stock_map_pixbuf) {
                        g_object_unref (priv->stock_map_pixbuf);
                        priv->stock_map_pixbuf = NULL;
                }

                priv->width = allocation.width;
                priv->height = allocation.height;
        }

        if (!priv->stock_map_pixbuf) {
                GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file_at_scale
                        (ICONDIR "/clock-map.png",
                         priv->width, priv->height, FALSE, NULL);

                priv->stock_map_pixbuf = pixbuf;
        }

        clock_map_place_locations (this);

        clock_map_display (this);
}

static gboolean
#if GTK_CHECK_VERSION (3, 0, 0)
clock_map_draw (GtkWidget *this, cairo_t *cr)
#else
clock_map_expose (GtkWidget *this, GdkEventExpose *event)
#endif
{
        ClockMapPrivate *priv = PRIVATE (this);
#if GTK_CHECK_VERSION (3, 0, 0)
	int width, height;
	GtkStyleContext *context;
	GdkRGBA color;

	context = gtk_widget_get_style_context (this);
	gtk_style_context_get_color (context, GTK_STATE_FLAG_ACTIVE, &color);
#else
	GtkStyle *style;
	GdkWindow *window;
	GtkAllocation allocation;
	GdkRectangle region;
        cairo_t *cr;

	style = gtk_widget_get_style (this);
#endif

#if !GTK_CHECK_VERSION (3, 0, 0)
	window = gtk_widget_get_window (this);
	gtk_widget_get_allocation (this, &allocation);
#endif

	if (!priv->shadow_map_pixbuf) {
                g_warning ("Needed to refresh the map in expose event.");
		clock_map_refresh (CLOCK_MAP (this));
        }

#if GTK_CHECK_VERSION (3, 0, 0)
	width = gdk_pixbuf_get_width (priv->shadow_map_pixbuf);
	height = gdk_pixbuf_get_height (priv->shadow_map_pixbuf);

	gdk_cairo_set_source_pixbuf (cr, priv->shadow_map_pixbuf, 0, 0);
	cairo_rectangle (cr, 0, 0, width, height);
	cairo_paint (cr);
#else
        cr = gdk_cairo_create (window);

	region.x = allocation.x;
	region.y = allocation.y;
	region.width = gdk_pixbuf_get_width (priv->shadow_map_pixbuf);
	region.height = gdk_pixbuf_get_height (priv->shadow_map_pixbuf);

	gdk_rectangle_intersect (&region, &(event->area), &region);
	gdk_draw_pixbuf (window,
			 style->black_gc,
			 priv->shadow_map_pixbuf,
			 region.x - allocation.x,
			 region.y - allocation.y,
			 region.x,
			 region.y,
			 region.width,
			 region.height,
			 GDK_RGB_DITHER_NORMAL,
			 0, 0);
#endif

        /* draw a simple outline */
#if GTK_CHECK_VERSION (3, 0, 0)
	cairo_rectangle (cr, 0.5, 0.5, width - 1, height - 1);
	gdk_cairo_set_source_rgba (cr, &color);
#else
        cairo_rectangle (
                cr,
                allocation.x + 0.5, allocation.y + 0.5,
                gdk_pixbuf_get_width (priv->shadow_map_pixbuf) - 1,
                gdk_pixbuf_get_height (priv->shadow_map_pixbuf) - 1);

        cairo_set_source_rgb (
                cr,
                style->mid [GTK_STATE_ACTIVE].red   / 65535.0,
                style->mid [GTK_STATE_ACTIVE].green / 65535.0,
                style->mid [GTK_STATE_ACTIVE].blue  / 65535.0);
#endif

        cairo_set_line_width (cr, 1.0);
        cairo_stroke (cr);

#if !GTK_CHECK_VERSION (3, 0, 0)
        cairo_destroy (cr);
#endif

	return FALSE;
}

#if GTK_CHECK_VERSION (3, 0, 0)
static void
clock_map_get_preferred_width (GtkWidget *this,
                                gint *minimum_width,
                                gint *natural_width)
{
        *minimum_width = *natural_width = 250;
}

static void
clock_map_get_preferred_height (GtkWidget *this,
                                 gint *minimum_height,
                                 gint *natural_height)
{
        *minimum_height = *natural_height = 125;
}
#else
static void
clock_map_size_request (GtkWidget *this, GtkRequisition *requisition)
{
        requisition->width = 250;
        requisition->height = 125;
}
#endif

static void
clock_map_size_allocate (GtkWidget *this, GtkAllocation *allocation)
{
        ClockMapPrivate *priv = PRIVATE (this);

	if (GTK_WIDGET_CLASS (clock_map_parent_class)->size_allocate)
		GTK_WIDGET_CLASS (clock_map_parent_class)->size_allocate (this, allocation);

	if (priv->width != allocation->width ||
	    priv->height != allocation->height)
                clock_map_refresh (CLOCK_MAP (this));
}

static void
clock_map_mark (ClockMap *this, gfloat latitude, gfloat longitude, gint mark)
{
        ClockMapPrivate *priv = PRIVATE (this);
        GdkPixbuf *marker = priv->location_marker_pixbuf[mark];
        GdkPixbuf *partial = NULL;

        int x, y;
        int width, height;
        int marker_width, marker_height;
        int dest_x, dest_y, dest_width, dest_height;

        width = gdk_pixbuf_get_width (priv->location_map_pixbuf);
        height = gdk_pixbuf_get_height (priv->location_map_pixbuf);

        x = (width / 2.0 + (width / 2.0) * longitude / 180.0);
        y = (height / 2.0 - (height / 2.0) * latitude / 90.0);

        marker_width = gdk_pixbuf_get_width (marker);
        marker_height = gdk_pixbuf_get_height (marker);

        dest_x = x - marker_width / 2;
        dest_y = y - marker_height / 2;
        dest_width = marker_width;
        dest_height = marker_height;

        /* create a small partial pixbuf if the mark is too close to
           the north or south pole */
        if (dest_y < 0) {
                partial = gdk_pixbuf_new_subpixbuf
                        (marker, 0, dest_y + marker_height,
                         marker_width, -dest_y);

                dest_y = 0.0;
                marker_height = gdk_pixbuf_get_height (partial);
        } else if (dest_y + dest_height > height) {
                partial = gdk_pixbuf_new_subpixbuf
                        (marker, 0, 0, marker_width, height - dest_y);
                marker_height = gdk_pixbuf_get_height (partial);
        }

        if (partial) {
                marker = partial;
        }

        /* handle the cases where the marker needs to be placed across
           the 180 degree longitude line */
        if (dest_x < 0) {
                /* split into our two pixbufs for the left and right edges */
                GdkPixbuf *lhs = NULL;
                GdkPixbuf *rhs = NULL;

                lhs = gdk_pixbuf_new_subpixbuf
                        (marker, -dest_x, 0, marker_width + dest_x, marker_height);

                gdk_pixbuf_composite (lhs, priv->location_map_pixbuf,
                                      0, dest_y,
                                      gdk_pixbuf_get_width (lhs),
                                      gdk_pixbuf_get_height (lhs),
                                      0, dest_y,
                                      1.0, 1.0, GDK_INTERP_NEAREST, 0xFF);

                rhs = gdk_pixbuf_new_subpixbuf
                        (marker, 0, 0, -dest_x, marker_height);

                gdk_pixbuf_composite (rhs, priv->location_map_pixbuf,
                                      width - gdk_pixbuf_get_width (rhs) - 1,
                                      dest_y,
                                      gdk_pixbuf_get_width (rhs),
                                      gdk_pixbuf_get_height (rhs),
                                      width - gdk_pixbuf_get_width (rhs) - 1,
                                      dest_y,
                                      1.0, 1.0, GDK_INTERP_NEAREST, 0xFF);

                g_object_unref (lhs);
                g_object_unref (rhs);
        } else if (dest_x + dest_width > width) {
                /* split into our two pixbufs for the left and right edges */
                GdkPixbuf *lhs = NULL;
                GdkPixbuf *rhs = NULL;

                lhs = gdk_pixbuf_new_subpixbuf
                        (marker, width - dest_x, 0, marker_width - width + dest_x, marker_height);

                gdk_pixbuf_composite (lhs, priv->location_map_pixbuf,
                                      0, dest_y,
                                      gdk_pixbuf_get_width (lhs),
                                      gdk_pixbuf_get_height (lhs),
                                      0, dest_y,
                                      1.0, 1.0, GDK_INTERP_NEAREST, 0xFF);

                rhs = gdk_pixbuf_new_subpixbuf
                        (marker, 0, 0, width - dest_x, marker_height);

                gdk_pixbuf_composite (rhs, priv->location_map_pixbuf,
                                      width - gdk_pixbuf_get_width (rhs) - 1,
                                      dest_y,
                                      gdk_pixbuf_get_width (rhs),
                                      gdk_pixbuf_get_height (rhs),
                                      width - gdk_pixbuf_get_width (rhs) - 1,
                                      dest_y,
                                      1.0, 1.0, GDK_INTERP_NEAREST, 0xFF);

                g_object_unref (lhs);
                g_object_unref (rhs);
        } else {
                gdk_pixbuf_composite (marker, priv->location_map_pixbuf,
                                      dest_x, dest_y,
                                      gdk_pixbuf_get_width (marker),
                                      gdk_pixbuf_get_height (marker),
                                      dest_x, dest_y,
                                      1.0, 1.0, GDK_INTERP_NEAREST, 0xFF);
        }

        if (partial != NULL) {
                g_object_unref (partial);
        }
}

/**
 * Return value: %TRUE if @loc can be placed on the map, %FALSE otherwise.
 **/
static gboolean
clock_map_place_location (ClockMap *this, ClockLocation *loc, gboolean hilight)
{
        gfloat latitude, longitude;
	gint marker;

        clock_location_get_coords (loc, &latitude, &longitude);
        /* 0/0 means unset, basically */
        if (latitude == 0 && longitude == 0)
                return FALSE;

	if (hilight)
		marker = MARKER_HILIGHT;
	else if (clock_location_is_current (loc))
		marker = MARKER_CURRENT;
	else
		marker = MARKER_NORMAL;

        clock_map_mark (this, latitude, longitude, marker);

        return TRUE;
}

static void
clock_map_place_locations (ClockMap *this)
{
        ClockMapPrivate *priv = PRIVATE (this);
        GList *locs;
        ClockLocation *loc;

        if (priv->location_map_pixbuf) {
                g_object_unref (priv->location_map_pixbuf);
                priv->location_map_pixbuf = NULL;
        }

        priv->location_map_pixbuf = gdk_pixbuf_copy (priv->stock_map_pixbuf);

	locs = NULL;
	g_signal_emit (this, signals[NEED_LOCATIONS], 0, &locs);

        while (locs) {
                loc = CLOCK_LOCATION (locs->data);

                clock_map_place_location (this, loc, FALSE);

                locs = locs->next;
        }

#if 0
        /* map_mark test suite for the edge cases */

        /* points around longitude 180 */
        clock_map_mark (this, 0.0, 180.0);
        clock_map_mark (this, -15.0, -178.0);
        clock_map_mark (this, -30.0, -176.0);
        clock_map_mark (this, 15.0, 178.0);
        clock_map_mark (this, 30.0, 176.0);

        clock_map_mark (this, 90.0, 180.0);
        clock_map_mark (this, -90.0, 180.0);

        /* north pole & friends */
        clock_map_mark (this, 90.0, 0.0);
        clock_map_mark (this, 88.0, -15.0);
        clock_map_mark (this, 92.0, 15.0);

        /* south pole & friends */
        clock_map_mark (this, -90.0, 0.0);
        clock_map_mark (this, -88.0, -15.0);
        clock_map_mark (this, -92.0, 15.0);
#endif
}

static void
clock_map_compute_vector (gdouble lat, gdouble lon, gdouble *vec)
{
        gdouble lat_rad, lon_rad;
        lat_rad = lat * (M_PI/180.0);
        lon_rad = lon * (M_PI/180.0);

        vec[0] = sin(lon_rad) * cos(lat_rad);
        vec[1] = sin(lat_rad);
        vec[2] = cos(lon_rad) * cos(lat_rad);
}

static guchar
clock_map_is_sunlit (gdouble pos_lat, gdouble pos_long,
                         gdouble sun_lat, gdouble sun_long)
{
        gdouble pos_vec[3];
        gdouble sun_vec[3];
        gdouble dot;

        /* twilight */
        gdouble epsilon = 0.01;

        clock_map_compute_vector (pos_lat, pos_long, pos_vec);
        clock_map_compute_vector (sun_lat, sun_long, sun_vec);

        /* compute the dot product of the two */
        dot = pos_vec[0]*sun_vec[0] + pos_vec[1]*sun_vec[1]
                + pos_vec[2]*sun_vec[2];

        if (dot > epsilon) {
                return 0x00;
        }

        if (dot < -epsilon) {
                return 0xFF;
        }

        return (guchar)(-128 * ((dot / epsilon) - 1));
}

static void
clock_map_render_shadow_pixbuf (GdkPixbuf *pixbuf)
{
        int x, y;
        int height, width;
        int n_channels, rowstride;
        guchar *pixels, *p;
        gdouble sun_lat, sun_lon;
        time_t now = time (NULL);

        n_channels = gdk_pixbuf_get_n_channels (pixbuf);
        rowstride = gdk_pixbuf_get_rowstride (pixbuf);
        pixels = gdk_pixbuf_get_pixels (pixbuf);

        width = gdk_pixbuf_get_width (pixbuf);
        height = gdk_pixbuf_get_height (pixbuf);

        sun_position (now, &sun_lat, &sun_lon);

        for (y = 0; y < height; y++) {
                gdouble lat = (height / 2.0 - y) / (height / 2.0) * 90.0;

                for (x = 0; x < width; x++) {
                        guchar shade;
                        gdouble lon =
                                (x - width / 2.0) / (width / 2.0) * 180.0;

                        shade = clock_map_is_sunlit (lat, lon,
                                                         sun_lat, sun_lon);

                        p = pixels + y * rowstride + x * n_channels;
                        p[3] = shade;
                }
        }
}

static void
clock_map_render_shadow (ClockMap *this)
{
        ClockMapPrivate *priv = PRIVATE (this);

        if (priv->shadow_pixbuf) {
                g_object_unref (priv->shadow_pixbuf);
        }

        priv->shadow_pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8,
                                              priv->width, priv->height);

        /* Initialize to all shadow */
        gdk_pixbuf_fill (priv->shadow_pixbuf, 0x6d9ccdff);

        clock_map_render_shadow_pixbuf (priv->shadow_pixbuf);

        if (priv->shadow_map_pixbuf) {
                g_object_unref (priv->shadow_map_pixbuf);
        }

        priv->shadow_map_pixbuf = gdk_pixbuf_copy (priv->location_map_pixbuf);

        gdk_pixbuf_composite (priv->shadow_pixbuf, priv->shadow_map_pixbuf,
                              0, 0, priv->width, priv->height,
                              0, 0, 1, 1, GDK_INTERP_NEAREST, 0x66);
}

static void
clock_map_display (ClockMap *this)
{
        ClockMapPrivate *priv = PRIVATE (this);

        if (priv->width > 0 || priv->height > 0)
                clock_map_render_shadow (this);
	gtk_widget_queue_draw (GTK_WIDGET (this));

        time (&priv->last_refresh);
}

typedef struct {
       ClockMap *map;
       ClockLocation *location;
       int count;
} BlinkData;

static gboolean
highlight (gpointer user_data)
{
       BlinkData *data = user_data;

       if (data->count == 6)
               return FALSE;

       if (data->count % 2 == 0) {
                 if (!clock_map_place_location (data->map,
                                                data->location, TRUE))
                         return FALSE;
       } else
                 clock_map_place_locations (data->map);
       clock_map_display (data->map);

       data->count++;

       return TRUE;
}

static void
highlight_destroy (gpointer user_data)
{
       BlinkData *data = user_data;
       ClockMapPrivate *priv;

       priv = PRIVATE (data->map);
       priv->highlight_timeout_id = 0;

       g_object_unref (data->location);
       g_free (data);
}

void
clock_map_blink_location (ClockMap *this, ClockLocation *loc)
{
       BlinkData *data;
       ClockMapPrivate *priv;

       priv = PRIVATE (this);

       g_return_if_fail (IS_CLOCK_MAP (this));
       g_return_if_fail (IS_CLOCK_LOCATION (loc));

       data = g_new0 (BlinkData, 1);
       data->map = this;
       data->location = g_object_ref (loc);

       if (priv->highlight_timeout_id) {
	       g_source_remove (priv->highlight_timeout_id);
	       clock_map_place_locations (this);
       }

       highlight (data);

       priv->highlight_timeout_id = g_timeout_add_full (G_PRIORITY_DEFAULT_IDLE,
							300, highlight, data,
							highlight_destroy);
}

static gboolean
clock_map_needs_refresh (ClockMap *this)
{
        ClockMapPrivate *priv = PRIVATE (this);
        time_t now_t;

        time (&now_t);

	/* refresh once per minute */
	return (ABS (now_t - priv->last_refresh) >= 60);
}

void
clock_map_update_time (ClockMap *this)
{

	g_return_if_fail (IS_CLOCK_MAP (this));

        if (!clock_map_needs_refresh (this)) {
                return;
        }

        clock_map_display (this);
}