#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#define HAVE_EXIF 1

#include "xviewer-map-plugin.h"

#include <gmodule.h>
#include <glib/gi18n-lib.h>

#include <xviewer/xviewer-exif-util.h>
#include <xviewer/xviewer-debug.h>
#include <xviewer/xviewer-thumb-view.h>
#include <xviewer/xviewer-image.h>
#include <xviewer/xviewer-sidebar.h>
#include <xviewer/xviewer-window.h>
#include <xviewer/xviewer-window-activatable.h>

#include <libpeas/peas.h>

#include <math.h>
#include <string.h>
#include <champlain/champlain.h>
#include <champlain-gtk/champlain-gtk.h>
#include <clutter-gtk/clutter-gtk.h>
#include <libexif/exif-data.h>
#include <libexif/exif-tag.h>

enum {
        PROP_0,
        PROP_WINDOW
};

static void
xviewer_window_activatable_iface_init (XviewerWindowActivatableInterface *iface);

G_DEFINE_DYNAMIC_TYPE_EXTENDED (XviewerMapPlugin, xviewer_map_plugin,
                PEAS_TYPE_EXTENSION_BASE, 0,
                G_IMPLEMENT_INTERFACE_DYNAMIC (XVIEWER_TYPE_WINDOW_ACTIVATABLE,
                                        xviewer_window_activatable_iface_init))

#define FACTOR 2.0

static void
xviewer_map_plugin_init (XviewerMapPlugin *plugin)
{
	xviewer_debug_message (DEBUG_PLUGINS, "XviewerMapPlugin initializing");
	if (gtk_clutter_init (NULL, NULL) != CLUTTER_INIT_SUCCESS)
	{
		g_warning ("XviewerMapPlugin: gtk_clutter_init failed!");
	}
}

static void
xviewer_map_plugin_finalize (GObject *object)
{
	xviewer_debug_message (DEBUG_PLUGINS, "XviewerMapPlugin finalizing");

	G_OBJECT_CLASS (xviewer_map_plugin_parent_class)->finalize (object);
}

static void
update_marker_image (ChamplainLabel *marker,
                     GtkIconSize size)
{
	GtkWidget *widget;
	ClutterActor *thumb;

	widget = gtk_button_new ();
	thumb = gtk_clutter_texture_new ();
	if (G_UNLIKELY (!gtk_clutter_texture_set_from_icon_name (GTK_CLUTTER_TEXTURE (thumb),
	                                                         widget,
	                                                         "mark-location",
	                                                         size, NULL)))
	{
		/* mark-location doesn't appear to be a "standard" icon yet,
		 * so try falling back to image-x-generic as before if
		 * it is not available */
		if (G_UNLIKELY (!gtk_clutter_texture_set_from_icon_name (GTK_CLUTTER_TEXTURE (thumb),
		                                                         widget,
		                                                         "image-x-generic",
		                                                         size, NULL)))
		{
			g_warning ("Could not load icon for map marker. "
			           "Please install a suitable icon theme!");
		}
	}

	/* don't need to unref widget because it is floating */

	champlain_label_set_image (marker, thumb);
}

#define MAP_EXIF_ENTRY_IS_GPS_RATIONAL(e) ( e && \
	 (e->format == EXIF_FORMAT_RATIONAL) && \
	 (e->components == 3) && \
	 (exif_entry_get_ifd(e) == EXIF_IFD_GPS) )

static gboolean
parse_exif_gps_coordinate (ExifEntry *entry,
			   gdouble *co,
			   ExifByteOrder byte_order)
{
	gsize val_size;
	ExifRational val;
	gdouble hour = 0, min = 0, sec = 0;

	if (G_UNLIKELY (!MAP_EXIF_ENTRY_IS_GPS_RATIONAL (entry)))
		return FALSE;

	val_size = exif_format_get_size (EXIF_FORMAT_RATIONAL);

	val = exif_get_rational (entry->data, byte_order);
	if (val.denominator != 0)
		hour = (gdouble) val.numerator /
		       (gdouble) val.denominator;

	val = exif_get_rational (entry->data + val_size, byte_order);
	if (val.denominator != 0)
		min = (gdouble) val.numerator /
		      (gdouble) val.denominator;

	val = exif_get_rational (entry->data + (2 * val_size), byte_order);
	if (val.denominator != 0)
		sec = (gdouble) val.numerator /
		      (gdouble) val.denominator;

	if (G_LIKELY (co != NULL)) {
		*co = hour + (min / 60.0) + (sec / 3600.0);
	}

	return TRUE;
}

static gboolean
get_coordinates (XviewerImage *image,
		 gdouble *latitude,
		 gdouble *longitude)
{
	ExifData *exif_data;
	gchar buffer[32];
	gdouble lon, lat;

	exif_data = (ExifData *) xviewer_image_get_exif_info (image);

	if (exif_data) {
		ExifEntry *entry;
		ExifByteOrder byte_order;

		byte_order = exif_data_get_byte_order (exif_data);
		entry = exif_data_get_entry (exif_data,
					     EXIF_TAG_GPS_LONGITUDE);

		if (!parse_exif_gps_coordinate (entry, &lon, byte_order)
		    || (lon > 180.0)) {
			exif_data_unref (exif_data);
			return FALSE;
		}


		xviewer_exif_data_get_value (exif_data,
					 EXIF_TAG_GPS_LONGITUDE_REF,
					 buffer,
					 32);
		if (strcmp (buffer, "W") == 0)
			lon *= -1;

		entry = exif_data_get_entry (exif_data,
					     EXIF_TAG_GPS_LATITUDE);

		if (!parse_exif_gps_coordinate (entry, &lat, byte_order)
		    || (lat > 90.0)) {
			exif_data_unref (exif_data);
			return FALSE;
		}

		xviewer_exif_data_get_value (exif_data,
					 EXIF_TAG_GPS_LATITUDE_REF,
					 buffer,
					 32);
		if (strcmp (buffer, "S") == 0)
			lat *= -1;

		*longitude = lon;
		*latitude = lat;

		exif_data_unref (exif_data);
		return TRUE;
	}
	return FALSE;
}

static gboolean
change_image (ChamplainLabel *marker,
	      ClutterEvent *event,
	      XviewerMapPlugin *plugin)
{
	XviewerImage *image;

	image = g_object_get_data (G_OBJECT (marker), "image");

	if (!image)
		return FALSE;

	xviewer_thumb_view_set_current_image (XVIEWER_THUMB_VIEW (plugin->thumbview),
					  image, TRUE);

	return FALSE;
}

static void
create_marker (XviewerImage *image,
	       XviewerMapPlugin *plugin)
{
	gdouble lon, lat;

	if (!image)
		return;

	if (!xviewer_image_has_data (image, XVIEWER_IMAGE_DATA_EXIF) &&
	    !xviewer_image_load (image, XVIEWER_IMAGE_DATA_EXIF, NULL, NULL))
		return;

	if (get_coordinates (image, &lat, &lon)) {
		ChamplainLabel *marker;

		marker = CHAMPLAIN_LABEL (champlain_label_new ());
		champlain_label_set_draw_background (CHAMPLAIN_LABEL (marker), FALSE);
		update_marker_image (marker, GTK_ICON_SIZE_MENU);

		g_object_set_data_full (G_OBJECT (image), "marker", marker, (GDestroyNotify) clutter_actor_destroy);
		g_object_set_data (G_OBJECT (marker), "image", image);

		champlain_location_set_location (CHAMPLAIN_LOCATION (marker),
						    lat,
						    lon);
		champlain_marker_layer_add_marker (plugin->layer, CHAMPLAIN_MARKER (marker));

		g_signal_connect (marker,
				  "button-release-event",
				  G_CALLBACK (change_image),
				  plugin);
	}

}

static void
selection_changed_cb (XviewerThumbView *view,
		      XviewerMapPlugin *plugin)
{
	XviewerImage *image;
	ChamplainLabel *marker;

	if (!xviewer_thumb_view_get_n_selected (view))
		return;

	image = xviewer_thumb_view_get_first_selected_image (view);

	g_return_if_fail (image != NULL);

	marker = g_object_get_data (G_OBJECT (image), "marker");

	if (marker) {
		gdouble lat, lon;

		g_object_get (marker,
			      "latitude", &lat,
			      "longitude", &lon,
			      NULL);

		champlain_view_go_to (CHAMPLAIN_VIEW (plugin->map),
		                      lat,
		                      lon);

		/* Reset the previous selection */
		if (plugin->marker)
			update_marker_image (plugin->marker, GTK_ICON_SIZE_MENU);

		plugin->marker = marker;
		update_marker_image (plugin->marker, GTK_ICON_SIZE_LARGE_TOOLBAR);
		gtk_widget_set_sensitive (plugin->jump_to_button, TRUE);
	}
	else {
		gtk_widget_set_sensitive (plugin->jump_to_button, FALSE);

		/* Reset the previous selection */
		if (plugin->marker)
			update_marker_image (plugin->marker, GTK_ICON_SIZE_MENU);

		plugin->marker = NULL;
	}

	g_object_unref (image);
}

static void
jump_to (GtkWidget *widget,
	 XviewerMapPlugin *plugin)
{
	if (!plugin->marker)
		return;

	gdouble lat, lon;

	g_object_get (plugin->marker,
		      "latitude", &lat,
		      "longitude", &lon,
		      NULL);

	champlain_view_go_to (CHAMPLAIN_VIEW (plugin->map),
	                      lat,
	                      lon);
}

static void
zoom_in (GtkWidget *widget,
	 ChamplainView *view)
{
	champlain_view_zoom_in (view);
}

static void
zoom_out (GtkWidget *widget,
	  ChamplainView *view)
{
	champlain_view_zoom_out (view);
}

static gboolean
for_each_thumb (GtkTreeModel *model,
		GtkTreePath *path,
		GtkTreeIter *iter,
		XviewerMapPlugin *plugin)
{
	XviewerImage *image = NULL;

	gtk_tree_model_get (model, iter,
			    XVIEWER_LIST_STORE_XVIEWER_IMAGE, &image,
			    -1);

	if (!image) {
		return FALSE;
	}

	create_marker (image, plugin);

	g_object_unref (image);
	return FALSE;
}

static void
prepared_cb (XviewerWindow *window,
	     XviewerMapPlugin *plugin)
{
	GList *markers;

	plugin->store = xviewer_window_get_store (plugin->window);

	if (!plugin->store)
		return;

	if (plugin->win_prepared_id > 0) {
		g_signal_handler_disconnect (window, plugin->win_prepared_id);
		plugin->win_prepared_id = 0;
	}

	/* At this point, the collection has been filled */
	gtk_tree_model_foreach (GTK_TREE_MODEL (plugin->store),
				(GtkTreeModelForeachFunc) for_each_thumb,
				plugin);

	plugin->thumbview = xviewer_window_get_thumb_view (window);
	plugin->selection_changed_id = g_signal_connect (G_OBJECT (plugin->thumbview),
						       "selection-changed",
						       G_CALLBACK (selection_changed_cb),
						       plugin);

	/* Call the callback because if the plugin is activated after
	 *  the image is loaded, selection_changed isn't emited
	 */
	selection_changed_cb (XVIEWER_THUMB_VIEW (plugin->thumbview), plugin);

	/* zoom in and the be sure that all markers are visible.
	 * This is useful to have a good starting zoom level where
	 * you can see a available markers on the map
	 */
	markers = champlain_marker_layer_get_markers (plugin->layer);
	if(markers != NULL)
	{
		champlain_view_set_zoom_level (plugin->map, 15);
		champlain_view_ensure_layers_visible (plugin->map, FALSE);
		g_list_free (markers);
	}
}

static void
impl_activate (XviewerWindowActivatable *activatable)
{
	XviewerMapPlugin *plugin = XVIEWER_MAP_PLUGIN (activatable);
	GtkWidget *sidebar, *vbox, *bbox, *button, *viewport;
	GtkWidget *embed;
	ClutterActor *scale;

	xviewer_debug (DEBUG_PLUGINS);

	/* This is a workaround until bug 590692 is fixed. */
	viewport = gtk_frame_new (NULL);
	gtk_frame_set_shadow_type (GTK_FRAME (viewport), GTK_SHADOW_ETCHED_IN);
	/*viewport = gtk_viewport_new (NULL, NULL);
	gtk_viewport_set_shadow_type (GTK_VIEWPORT (viewport),
				      GTK_SHADOW_ETCHED_IN);*/

	embed = gtk_champlain_embed_new ();
	plugin->map = gtk_champlain_embed_get_view (GTK_CHAMPLAIN_EMBED (embed));
	g_object_set (G_OBJECT (plugin->map),
		"zoom-level", 3,
		"kinetic-mode", TRUE,
		"goto-animation-duration", 1000,
		NULL);
	scale = champlain_scale_new ();
	champlain_scale_connect_view (CHAMPLAIN_SCALE (scale), plugin->map);
	/* align to the bottom left */
	champlain_view_bin_layout_add (plugin->map, scale,
		CLUTTER_BIN_ALIGNMENT_START,
		CLUTTER_BIN_ALIGNMENT_END);

	gtk_container_add (GTK_CONTAINER (viewport), embed);

	vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
	bbox = gtk_toolbar_new ();

	button = GTK_WIDGET (gtk_tool_button_new (NULL, NULL));
	gtk_tool_button_set_icon_name (GTK_TOOL_BUTTON (button), "go-jump-symbolic");
	gtk_widget_set_tooltip_text (button, _("Jump to current image's location"));
	g_signal_connect (button,
			  "clicked",
			  G_CALLBACK (jump_to),
			  plugin);
	gtk_container_add (GTK_CONTAINER (bbox), button);
	plugin->jump_to_button = button;

	button = GTK_WIDGET (gtk_separator_tool_item_new ());
	gtk_container_add (GTK_CONTAINER (bbox), button);

	button = GTK_WIDGET (gtk_tool_button_new (NULL, NULL));
	gtk_tool_button_set_icon_name (GTK_TOOL_BUTTON (button), "zoom-in-symbolic");
	gtk_widget_set_tooltip_text (button, _("Zoom in"));
	g_signal_connect (button,
			  "clicked",
			  G_CALLBACK (zoom_in),
			  plugin->map);
	gtk_container_add (GTK_CONTAINER (bbox), button);

	button = GTK_WIDGET (gtk_tool_button_new (NULL, NULL));
	gtk_tool_button_set_icon_name (GTK_TOOL_BUTTON (button), "zoom-out-symbolic");
	gtk_widget_set_tooltip_text (button, _("Zoom out"));
	g_signal_connect (button,
			  "clicked",
			  G_CALLBACK (zoom_out),
			  plugin->map);
	gtk_container_add (GTK_CONTAINER (bbox), button);

	plugin->layer = champlain_marker_layer_new_full (CHAMPLAIN_SELECTION_SINGLE);
	champlain_view_add_layer (CHAMPLAIN_VIEW (plugin->map), CHAMPLAIN_LAYER (plugin->layer));

	sidebar = xviewer_window_get_sidebar (plugin->window);
	plugin->viewport = vbox;
	gtk_box_pack_start (GTK_BOX (vbox), bbox, FALSE, FALSE, 0);
	gtk_widget_set_vexpand (viewport, TRUE);
	gtk_container_add (GTK_CONTAINER (vbox), viewport);
	xviewer_sidebar_add_page (XVIEWER_SIDEBAR (sidebar), _("Map"), vbox);
	gtk_widget_show_all (vbox);

	plugin->win_prepared_id = g_signal_connect (G_OBJECT (plugin->window),
						    "prepared",
						    G_CALLBACK (prepared_cb),
						    plugin);
	/* Call the callback once in case the window is already ready */
	prepared_cb (plugin->window, plugin);
}

static void
impl_deactivate (XviewerWindowActivatable *activatable)
{
	XviewerMapPlugin *plugin = XVIEWER_MAP_PLUGIN (activatable);
	GtkWidget *sidebar, *thumbview;

	xviewer_debug (DEBUG_PLUGINS);

	sidebar = xviewer_window_get_sidebar (plugin->window);
	xviewer_sidebar_remove_page (XVIEWER_SIDEBAR (sidebar), plugin->viewport);

	thumbview = xviewer_window_get_thumb_view (plugin->window);
	if (thumbview && plugin->selection_changed_id > 0) {
		g_signal_handler_disconnect (thumbview,
					     plugin->selection_changed_id);
		plugin->selection_changed_id = 0;
	}

	if (plugin->window && plugin->win_prepared_id > 0) {
		g_signal_handler_disconnect (plugin->window,
					     plugin->win_prepared_id);
		plugin->win_prepared_id = 0;
	}

}

static void
xviewer_map_plugin_dispose (GObject *object)
{
        XviewerMapPlugin *plugin = XVIEWER_MAP_PLUGIN (object);

        if (plugin->window != NULL) {
                g_object_unref (plugin->window);
                plugin->window = NULL;
        }

        G_OBJECT_CLASS (xviewer_map_plugin_parent_class)->dispose (object);
}

static void
xviewer_map_plugin_get_property (GObject    *object,
			     guint       prop_id,
			     GValue     *value,
			     GParamSpec *pspec)
{
        XviewerMapPlugin *plugin = XVIEWER_MAP_PLUGIN (object);

        switch (prop_id)
        {
        case PROP_WINDOW:
                g_value_set_object (value, plugin->window);
                break;

        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
                break;
        }
}

static void
xviewer_map_plugin_set_property (GObject      *object,
			     guint         prop_id,
			     const GValue *value,
			     GParamSpec   *pspec)
{
        XviewerMapPlugin *plugin = XVIEWER_MAP_PLUGIN (object);

        switch (prop_id)
        {
        case PROP_WINDOW:
                plugin->window = XVIEWER_WINDOW (g_value_dup_object (value));
                break;

        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
                break;
        }
}

static void
xviewer_map_plugin_class_init (XviewerMapPluginClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->finalize = xviewer_map_plugin_finalize;
        object_class->dispose = xviewer_map_plugin_dispose;
        object_class->set_property = xviewer_map_plugin_set_property;
        object_class->get_property = xviewer_map_plugin_get_property;

        g_object_class_override_property (object_class, PROP_WINDOW, "window");
}

static void
xviewer_map_plugin_class_finalize (XviewerMapPluginClass *klass)
{
        /* Dummy needed for G_DEFINE_DYNAMIC_TYPE_EXTENDED */
}

static void
xviewer_window_activatable_iface_init (XviewerWindowActivatableInterface *iface)
{
        iface->activate = impl_activate;
        iface->deactivate = impl_deactivate;
}

G_MODULE_EXPORT void
peas_register_types (PeasObjectModule *module)
{
        xviewer_map_plugin_register_type (G_TYPE_MODULE (module));
        peas_object_module_register_extension_type (module,
                                                    XVIEWER_TYPE_WINDOW_ACTIVATABLE,
                                                    XVIEWER_TYPE_MAP_PLUGIN);
}
