/*
   Copyright (C) 2011 Bastien Nocera
   Copyright (C) 2016 Collabora Ltd.

   The Gnome Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public License as
   published by the Free Software Foundation; either version 2 of the
   License, or (at your option) any later version.

   The Gnome Library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public
   License along with the Gnome Library; see the file COPYING.LIB.  If not,
   write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
   Boston, MA 02110-1301  USA.

   Authors: Bastien Nocera <hadess@hadess.net>
            Philip Withnall <philip.withnall@collabora.co.uk>
 */

#include <string.h>
#include <stdlib.h>
#include <locale.h>
#include <gio/gio.h>
#include <geocode-glib/geocode-backend.h>
#include <geocode-glib/geocode-forward.h>
#include <geocode-glib/geocode-bounding-box.h>
#include <geocode-glib/geocode-error.h>
#include <geocode-glib/geocode-glib-private.h>
#include <geocode-glib/geocode-nominatim.h>

/**
 * SECTION:geocode-forward
 * @short_description: Geocode forward geocoding object
 * @include: geocode-glib/geocode-glib.h
 *
 * Contains functions for geocoding using the
 * <ulink url="http://wiki.openstreetmap.org/wiki/Nominatim">OSM Nominatim APIs</ulink>
 **/

struct _GeocodeForwardPrivate {
	GHashTable *ht;
	guint       answer_count;
	GeocodeBoundingBox *search_area;
	gboolean bounded;

	GeocodeBackend  *backend;
};

enum {
        PROP_0,

        PROP_ANSWER_COUNT,
        PROP_SEARCH_AREA,
        PROP_BOUNDED
};

G_DEFINE_TYPE (GeocodeForward, geocode_forward, G_TYPE_OBJECT)

static void
geocode_forward_get_property (GObject	 *object,
			      guint	  property_id,
			      GValue	 *value,
			      GParamSpec *pspec)
{
	GeocodeForward *forward = GEOCODE_FORWARD (object);

	switch (property_id) {
		case PROP_ANSWER_COUNT:
			g_value_set_uint (value,
					  geocode_forward_get_answer_count (forward));
			break;

		case PROP_SEARCH_AREA:
			g_value_set_object (value,
					    geocode_forward_get_search_area (forward));
			break;

		case PROP_BOUNDED:
			g_value_set_boolean (value,
					     geocode_forward_get_bounded (forward));
			break;

		default:
			/* We don't have any other property... */
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
			break;
	}
}

static void
geocode_forward_set_property(GObject	   *object,
			     guint	    property_id,
			     const GValue *value,
			     GParamSpec   *pspec)
{
	GeocodeForward *forward = GEOCODE_FORWARD (object);

	switch (property_id) {
		case PROP_ANSWER_COUNT:
			geocode_forward_set_answer_count (forward,
							  g_value_get_uint (value));
			break;

		case PROP_SEARCH_AREA:
			geocode_forward_set_search_area (forward,
							 g_value_get_object (value));
			break;

		case PROP_BOUNDED:
			geocode_forward_set_bounded (forward,
						     g_value_get_boolean (value));
			break;

		default:
			/* We don't have any other property... */
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
			break;
	}
}

static void
geocode_forward_finalize (GObject *gforward)
{
	GeocodeForward *forward = (GeocodeForward *) gforward;

	g_clear_pointer (&forward->priv->ht, g_hash_table_unref);
	g_clear_object (&forward->priv->backend);

	G_OBJECT_CLASS (geocode_forward_parent_class)->finalize (gforward);
}

static void
geocode_forward_class_init (GeocodeForwardClass *klass)
{
	GObjectClass *gforward_class = G_OBJECT_CLASS (klass);
	GParamSpec *pspec;

	gforward_class->finalize = geocode_forward_finalize;
	gforward_class->get_property = geocode_forward_get_property;
	gforward_class->set_property = geocode_forward_set_property;


	g_type_class_add_private (klass, sizeof (GeocodeForwardPrivate));

	/**
	* GeocodeForward:answer-count:
	*
	* The number of requested results to a search query.
	*/
	pspec = g_param_spec_uint ("answer-count",
				   "Answer count",
				   "The number of requested results",
				   0,
				   G_MAXINT,
				   DEFAULT_ANSWER_COUNT,
				   G_PARAM_READWRITE |
				   G_PARAM_STATIC_STRINGS);
	g_object_class_install_property (gforward_class, PROP_ANSWER_COUNT, pspec);

	/**
	* GeocodeForward:search-area:
	*
	* The bounding box that limits the search area.
	* If #GeocodeForward:bounded property is set to #TRUE only results from
	* this area is returned.
	*/
	pspec = g_param_spec_object ("search-area",
				     "Search area",
				     "The area to limit search within",
				     GEOCODE_TYPE_BOUNDING_BOX,
				     G_PARAM_READWRITE |
				     G_PARAM_STATIC_STRINGS);
	g_object_class_install_property (gforward_class, PROP_SEARCH_AREA, pspec);

	/**
	* GeocodeForward:bounded:
	*
	* If set to #TRUE then only results in the #GeocodeForward:search-area
	* bounding box are returned.
	* If set to #FALSE the #GeocodeForward:search-area is treated like a
	* preferred area for results.
	*/
	pspec = g_param_spec_boolean ("bounded",
				      "Bounded",
				      "Bind search results to search-area",
				      FALSE,
				      G_PARAM_READWRITE |
				      G_PARAM_STATIC_STRINGS);
	g_object_class_install_property (gforward_class, PROP_BOUNDED, pspec);
}

static void
free_value (GValue *value)
{
	g_value_unset (value);
	g_free (value);
}

static void
geocode_forward_init (GeocodeForward *forward)
{
	forward->priv = G_TYPE_INSTANCE_GET_PRIVATE ((forward), GEOCODE_TYPE_FORWARD, GeocodeForwardPrivate);
	forward->priv->ht = g_hash_table_new_full (g_str_hash, g_str_equal,
	                                           g_free,
	                                           (GDestroyNotify) free_value);
	forward->priv->answer_count = DEFAULT_ANSWER_COUNT;
	forward->priv->search_area = NULL;
	forward->priv->bounded = FALSE;
}

static void
ensure_backend (GeocodeForward *object)
{
	/* If no backend is specified, default to the GNOME Nominatim backend */
	if (object->priv->backend == NULL)
		object->priv->backend = GEOCODE_BACKEND (geocode_nominatim_get_gnome ());
}

/**
 * geocode_forward_new_for_params:
 * @params: (transfer none) (element-type utf8 GValue): a #GHashTable with string keys, and #GValue values.
 *
 * Creates a new #GeocodeForward to perform geocoding with. The
 * #GHashTable is in the format used by Telepathy, and documented
 * on <ulink url="http://telepathy.freedesktop.org/spec/Connection_Interface_Location.html#Mapping:Location">Telepathy's specification site</ulink>.
 *
 * See also: <ulink url="http://xmpp.org/extensions/xep-0080.html">XEP-0080 specification</ulink>.
 *
 * Returns: a new #GeocodeForward. Use g_object_unref() when done.
 **/
GeocodeForward *
geocode_forward_new_for_params (GHashTable *params)
{
	GeocodeForward *forward;
	GHashTableIter iter;
	const gchar *key;
	const GValue *value;

	g_return_val_if_fail (params != NULL, NULL);

	if (g_hash_table_lookup (params, "lat") != NULL &&
	    g_hash_table_lookup (params, "long") != NULL) {
		g_warning ("You already have longitude and latitude in those parameters");
	}

	forward = g_object_new (GEOCODE_TYPE_FORWARD, NULL);

	g_hash_table_iter_init (&iter, params);

	while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &value)) {
		GValue *value_copy = g_new0 (GValue, 1);
		g_value_init (value_copy, G_VALUE_TYPE (value));
		g_value_copy (value, value_copy);
		g_hash_table_insert (forward->priv->ht, g_strdup (key), value_copy);
	}

	return forward;
}

/**
 * geocode_forward_new_for_string:
 * @str: a string containing a free-form description of the location
 *
 * Creates a new #GeocodeForward to perform forward geocoding with. The
 * string is in free-form format.
 *
 * Returns: a new #GeocodeForward. Use g_object_unref() when done.
 **/
GeocodeForward *
geocode_forward_new_for_string (const char *location)
{
	GeocodeForward *forward;
	GValue *location_value;

	g_return_val_if_fail (location != NULL, NULL);

	forward = g_object_new (GEOCODE_TYPE_FORWARD, NULL);

	location_value = g_new0 (GValue, 1);
	g_value_init (location_value, G_TYPE_STRING);
	g_value_set_string (location_value, location);
	g_hash_table_insert (forward->priv->ht, g_strdup ("location"),
	                     location_value);

	return forward;
}

static void
backend_forward_search_ready (GeocodeBackend *backend,
                              GAsyncResult   *res,
                              GTask          *task)
{
	GList *places;  /* (element-type GeocodePlace) */
	GError *error = NULL;

	places = geocode_backend_forward_search_finish (backend, res, &error);
	if (places != NULL)
		g_task_return_pointer (task, places, (GDestroyNotify) g_list_free);
	else
		g_task_return_error (task, error);
	g_object_unref (task);
}

/**
 * geocode_forward_search_async:
 * @forward: a #GeocodeForward representing a query
 * @cancellable: optional #GCancellable forward, %NULL to ignore.
 * @callback: a #GAsyncReadyCallback to call when the request is satisfied
 * @user_data: the data to pass to callback function
 *
 * Asynchronously performs a forward geocoding
 * query using a web service. Use geocode_forward_search() to do the same
 * thing synchronously.
 *
 * When the operation is finished, @callback will be called. You can then call
 * geocode_forward_search_finish() to get the result of the operation.
 **/
void
geocode_forward_search_async (GeocodeForward      *forward,
			      GCancellable        *cancellable,
			      GAsyncReadyCallback  callback,
			      gpointer             user_data)
{
	GTask *task;

	g_return_if_fail (GEOCODE_IS_FORWARD (forward));
	g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));

	ensure_backend (forward);
	g_assert (forward->priv->backend != NULL);

	task = g_task_new (forward, cancellable, callback, user_data);
	geocode_backend_forward_search_async (forward->priv->backend,
	                                      forward->priv->ht,
	                                      cancellable,
	                                      (GAsyncReadyCallback) backend_forward_search_ready,
	                                      g_object_ref (task));
	g_object_unref (task);
}

/**
 * geocode_forward_search_finish:
 * @forward: a #GeocodeForward representing a query
 * @res: a #GAsyncResult.
 * @error: a #GError.
 *
 * Finishes a forward geocoding operation. See geocode_forward_search_async().
 *
 * Returns: (element-type GeocodePlace) (transfer full): A list of
 * places or %NULL in case of errors. Free the returned instances with
 * g_object_unref() and the list with g_list_free() when done.
 **/
GList *
geocode_forward_search_finish (GeocodeForward       *forward,
			       GAsyncResult        *res,
			       GError             **error)
{
	g_return_val_if_fail (GEOCODE_IS_FORWARD (forward), NULL);
	g_return_val_if_fail (G_IS_ASYNC_RESULT (res), NULL);
	g_return_val_if_fail (error == NULL || *error == NULL, NULL);

	return g_task_propagate_pointer (G_TASK (res), error);
}

/**
 * geocode_forward_search:
 * @forward: a #GeocodeForward representing a query
 * @error: a #GError
 *
 * Gets the result of a forward geocoding
 * query using the current backend (see geocode_forward_set_backend()). By
 * default the GNOME Nominatim server is used. See #GeocodeBackend for more
 * information.
 *
 * If no results are found, a %GEOCODE_ERROR_NO_MATCHES error is returned.
 *
 * Returns: (element-type GeocodePlace) (transfer full): A list of
 * places or %NULL in case of errors. Free the returned instances with
 * g_object_unref() and the list with g_list_free() when done.
 **/
GList *
geocode_forward_search (GeocodeForward      *forward,
			GError             **error)
{
	g_return_val_if_fail (GEOCODE_IS_FORWARD (forward), NULL);
	g_return_val_if_fail (error == NULL || *error == NULL, NULL);

	ensure_backend (forward);
	g_assert (forward->priv->backend != NULL);

	return geocode_backend_forward_search (forward->priv->backend,
	                                       forward->priv->ht,
	                                       NULL,
	                                       error);
}

/**
 * geocode_forward_set_answer_count:
 * @forward: a #GeocodeForward representing a query
 * @count: the number of requested results, which must be greater than zero
 *
 * Sets the number of requested results to @count.
 **/
void
geocode_forward_set_answer_count (GeocodeForward *forward,
				  guint           count)
{
	GValue *count_value;

	g_return_if_fail (GEOCODE_IS_FORWARD (forward));
	g_return_if_fail (count > 0);

	forward->priv->answer_count = count;

	/* Note: This key name is not defined in the Telepathy specification or
	 * in XEP-0080; it is custom, but standard within Geocode. */
	count_value = g_new0 (GValue, 1);
	g_value_init (count_value, G_TYPE_UINT);
	g_value_set_uint (count_value, count);
	g_hash_table_insert (forward->priv->ht, g_strdup ("limit"),
	                     count_value);
}

/**
 * geocode_forward_set_search_area:
 * @forward: a #GeocodeForward representing a query
 * @box: a bounding box to limit the search area.
 *
 * Sets the area to limit searches within.
 **/
void
geocode_forward_set_search_area (GeocodeForward     *forward,
				 GeocodeBoundingBox *bbox)
{
	GValue *area_value;
	char *area;
	char top[G_ASCII_DTOSTR_BUF_SIZE];
	char left[G_ASCII_DTOSTR_BUF_SIZE];
	char bottom[G_ASCII_DTOSTR_BUF_SIZE];
	char right[G_ASCII_DTOSTR_BUF_SIZE];

	g_return_if_fail (GEOCODE_IS_FORWARD (forward));

	forward->priv->search_area = bbox;

	/* need to convert with g_ascii_dtostr to be locale safe */
	g_ascii_dtostr (top, G_ASCII_DTOSTR_BUF_SIZE,
	                geocode_bounding_box_get_top (bbox));

	g_ascii_dtostr (bottom, G_ASCII_DTOSTR_BUF_SIZE,
	                geocode_bounding_box_get_bottom (bbox));

	g_ascii_dtostr (left, G_ASCII_DTOSTR_BUF_SIZE,
	                geocode_bounding_box_get_left (bbox));

	g_ascii_dtostr (right, G_ASCII_DTOSTR_BUF_SIZE,
	                geocode_bounding_box_get_right (bbox));

	/* Note: This key name is not defined in the Telepathy specification or
	 * in XEP-0080; it is custom, but standard within Geocode. */
	area = g_strdup_printf ("%s,%s,%s,%s", left, top, right, bottom);
	area_value = g_new0 (GValue, 1);
	g_value_init (area_value, G_TYPE_STRING);
	g_value_take_string (area_value, area);
	g_hash_table_insert (forward->priv->ht, g_strdup ("viewbox"),
	                     area_value);
}

/**
 * geocode_forward_set_bounded:
 * @forward: a #GeocodeForward representing a query
 * @bounded: #TRUE to restrict results to only items contained within the
 * #GeocodeForward:search-area bounding box.
 *
 * Set the #GeocodeForward:bounded property that regulates whether the
 * #GeocodeForward:search-area property acts restricting or not.
 **/
void
geocode_forward_set_bounded (GeocodeForward *forward,
			     gboolean        bounded)
{
	GValue *bounded_value;

	g_return_if_fail (GEOCODE_IS_FORWARD (forward));

	forward->priv->bounded = bounded;

	/* Note: This key name is not defined in the Telepathy specification or
	 * in XEP-0080; it is custom, but standard within Geocode. */
	bounded_value = g_new0 (GValue, 1);
	g_value_init (bounded_value, G_TYPE_STRING);
	g_value_set_boolean (bounded_value, bounded);
	g_hash_table_insert (forward->priv->ht, g_strdup ("bounded"),
	                     bounded_value);
}

/**
 * geocode_forward_get_answer_count:
 * @forward: a #GeocodeForward representing a query
 *
 * Gets the number of requested results for searches.
 **/
guint
geocode_forward_get_answer_count (GeocodeForward *forward)
{
	g_return_val_if_fail (GEOCODE_IS_FORWARD (forward), 0);

	return forward->priv->answer_count;
}

/**
 * geocode_forward_get_search_area:
 * @forward: a #GeocodeForward representing a query
 *
 * Gets the area to limit searches within.
 *
 * Returns: (transfer none) (nullable): the search area, or %NULL if none is set
 **/
GeocodeBoundingBox *
geocode_forward_get_search_area (GeocodeForward *forward)
{
	g_return_val_if_fail (GEOCODE_IS_FORWARD (forward), NULL);

	return forward->priv->search_area;
}

/**
 * geocode_forward_get_bounded:
 * @forward: a #GeocodeForward representing a query
 *
 * Gets the #GeocodeForward:bounded property that regulates whether the
 * #GeocodeForward:search-area property acts restricting or not.
 **/
gboolean
geocode_forward_get_bounded (GeocodeForward *forward)
{
	g_return_val_if_fail (GEOCODE_IS_FORWARD (forward), FALSE);

	return forward->priv->bounded;
}

/**
 * geocode_forward_set_backend:
 * @forward: a #GeocodeForward representing a query
 * @backend: (nullable) (transfer none): a #GeocodeBackend, or %NULL to use the
 *    default one.
 *
 * Specifies the backend to use in the forward geocoding operation.
 *
 * If none is given, the default GNOME Nominatim server is used.
 *
 * Since: 3.23.1
 */
void
geocode_forward_set_backend (GeocodeForward *forward,
                             GeocodeBackend *backend)
{
	g_return_if_fail (GEOCODE_IS_FORWARD (forward));
	g_return_if_fail (backend == NULL || GEOCODE_IS_BACKEND (backend));

	g_set_object (&forward->priv->backend, backend);
}
