/* vim: set et ts=8 sw=8: */
/*
 * Copyright 2014 Red Hat, Inc.
 *
 * Geoclue 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.
 *
 * Geoclue 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 Geoclue; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Authors: Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
 */

#include <glib.h>
#include <config.h>
#include "gclue-location-source.h"

#if GCLUE_USE_COMPASS
#include "gclue-compass.h"
#include "gclue-config.h"
#endif

/**
 * SECTION:gclue-location-source
 * @short_description: GeoIP client
 * @include: gclue-glib/gclue-location-source.h
 *
 * The interface all geolocation sources must implement.
 **/

static GClueLocationSourceStartResult
start_source (GClueLocationSource *source);
static GClueLocationSourceStopResult
stop_source (GClueLocationSource *source);

struct _GClueLocationSourcePrivate
{
        GClueLocation *location;

        guint active_counter;
        GClueMinUINT *time_threshold;

        GClueAccuracyLevel avail_accuracy_level;

        gboolean compute_movement;
        gboolean scramble_location;
        gboolean priority_source;

#if GCLUE_USE_COMPASS
        GClueCompass *compass;
#endif

        guint heading_changed_id;
};

G_DEFINE_ABSTRACT_TYPE_WITH_CODE (GClueLocationSource,
                                  gclue_location_source,
                                  G_TYPE_OBJECT,
                                  G_ADD_PRIVATE (GClueLocationSource))

enum
{
        PROP_0,
        PROP_LOCATION,
        PROP_ACTIVE,
        PROP_TIME_THRESHOLD,
        PROP_AVAILABLE_ACCURACY_LEVEL,
        PROP_COMPUTE_MOVEMENT,
        PROP_SCRAMBLE_LOCATION,
        PROP_PRIORITY_SOURCE,
        LAST_PROP
};

static GParamSpec *gParamSpecs[LAST_PROP];

#if GCLUE_USE_COMPASS
static gboolean
set_heading_from_compass (GClueLocationSource *source,
                          GClueLocation       *location)
{
        GClueLocationSourcePrivate *priv = source->priv;
        gdouble heading, curr_heading;

        if (priv->compass == NULL)
                return FALSE;

        heading = gclue_compass_get_heading (priv->compass);
        curr_heading = gclue_location_get_heading (location);

        if (heading == GCLUE_LOCATION_HEADING_UNKNOWN  ||
            heading == curr_heading)
                return FALSE;

        g_debug ("%s got new heading from compass: %f",
                 G_OBJECT_TYPE_NAME (source), heading);
        /* We trust heading from compass more than any other source so we always
         * override existing heading
         */
        gclue_location_set_heading (location, heading);

        return TRUE;
}

static void
on_compass_heading_changed (GObject    *gobject,
                            GParamSpec *pspec,
                            gpointer    user_data)
{
        GClueLocationSource* source = GCLUE_LOCATION_SOURCE (user_data);

        if (source->priv->location == NULL)
                return;

        if (set_heading_from_compass (source, source->priv->location))
                g_object_notify (G_OBJECT (source), "location");
}
#endif /* GCLUE_USE_COMPASS */

static void
gclue_location_source_get_property (GObject    *object,
                                    guint       prop_id,
                                    GValue     *value,
                                    GParamSpec *pspec)
{
        GClueLocationSource *source = GCLUE_LOCATION_SOURCE (object);

        switch (prop_id) {
        case PROP_LOCATION:
                g_value_set_object (value, source->priv->location);
                break;

        case PROP_ACTIVE:
                g_value_set_boolean (value,
                                     gclue_location_source_get_active (source));
                break;

        case PROP_TIME_THRESHOLD:
                g_value_set_object (value, source->priv->time_threshold);
                break;

        case PROP_AVAILABLE_ACCURACY_LEVEL:
                g_value_set_enum (value, source->priv->avail_accuracy_level);
                break;

        case PROP_COMPUTE_MOVEMENT:
                g_value_set_boolean (value, source->priv->compute_movement);
                break;

        case PROP_SCRAMBLE_LOCATION:
                g_value_set_boolean (value, source->priv->scramble_location);
                break;

        case PROP_PRIORITY_SOURCE:
                g_value_set_boolean (value, source->priv->priority_source);
                break;

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

static void
gclue_location_source_set_property (GObject      *object,
                                    guint         prop_id,
                                    const GValue *value,
                                    GParamSpec   *pspec)
{
        GClueLocationSource *source = GCLUE_LOCATION_SOURCE (object);

        switch (prop_id) {
        case PROP_LOCATION:
        {
                GClueLocation *location = g_value_get_object (value);

                gclue_location_source_set_location (source, location);
                break;
        }

        case PROP_AVAILABLE_ACCURACY_LEVEL:
                source->priv->avail_accuracy_level = g_value_get_enum (value);
                break;

        case PROP_COMPUTE_MOVEMENT:
                source->priv->compute_movement = g_value_get_boolean (value);
                break;

        case PROP_SCRAMBLE_LOCATION:
                source->priv->scramble_location = g_value_get_boolean (value);
                break;

        case PROP_PRIORITY_SOURCE:
                source->priv->priority_source = g_value_get_boolean (value);
                break;

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

static void
gclue_location_source_finalize (GObject *object)
{
        GClueLocationSourcePrivate *priv = GCLUE_LOCATION_SOURCE (object)->priv;

        gclue_location_source_stop (GCLUE_LOCATION_SOURCE (object));
        g_clear_object (&priv->location);
        g_clear_object (&priv->time_threshold);

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

static void
gclue_location_source_class_init (GClueLocationSourceClass *klass)
{
        GObjectClass *object_class;

        klass->start = start_source;
        klass->stop = stop_source;

        object_class = G_OBJECT_CLASS (klass);
        object_class->get_property = gclue_location_source_get_property;
        object_class->set_property = gclue_location_source_set_property;
        object_class->finalize = gclue_location_source_finalize;

        gParamSpecs[PROP_LOCATION] = g_param_spec_object ("location",
                                                          "Location",
                                                          "Location",
                                                          GCLUE_TYPE_LOCATION,
                                                          G_PARAM_READWRITE);
        g_object_class_install_property (object_class,
                                         PROP_LOCATION,
                                         gParamSpecs[PROP_LOCATION]);

        gParamSpecs[PROP_ACTIVE] = g_param_spec_boolean ("active",
                                                         "Active",
                                                         "Active",
                                                         FALSE,
                                                         G_PARAM_READABLE);
        g_object_class_install_property (object_class,
                                         PROP_ACTIVE,
                                         gParamSpecs[PROP_ACTIVE]);

        gParamSpecs[PROP_TIME_THRESHOLD] =
                g_param_spec_object ("time-threshold",
                                     "TimeThreshold",
                                     "TimeThreshold",
                                     GCLUE_TYPE_MIN_UINT,
                                     G_PARAM_READABLE);
        g_object_class_install_property (object_class,
                                         PROP_TIME_THRESHOLD,
                                         gParamSpecs[PROP_TIME_THRESHOLD]);

        gParamSpecs[PROP_AVAILABLE_ACCURACY_LEVEL] =
                g_param_spec_enum ("available-accuracy-level",
                                   "AvailableAccuracyLevel",
                                   "Available accuracy level",
                                   GCLUE_TYPE_ACCURACY_LEVEL,
                                   0,
                                   G_PARAM_READWRITE);
        g_object_class_install_property (object_class,
                                         PROP_AVAILABLE_ACCURACY_LEVEL,
                                         gParamSpecs[PROP_AVAILABLE_ACCURACY_LEVEL]);

        gParamSpecs[PROP_COMPUTE_MOVEMENT] =
                g_param_spec_boolean ("compute-movement",
                                      "ComputeMovement",
                                      "Whether or not, speed and heading should "
                                      "be automatically computed (or fetched "
                                      "from hardware) and set on new locations.",
                                      TRUE,
                                      G_PARAM_READWRITE);
        g_object_class_install_property (object_class,
                                         PROP_COMPUTE_MOVEMENT,
                                         gParamSpecs[PROP_COMPUTE_MOVEMENT]);

        gParamSpecs[PROP_SCRAMBLE_LOCATION] =
                g_param_spec_boolean ("scramble-location",
                                      "ScrambleLocation",
                                      "Enable location scrambling",
                                      FALSE,
                                      G_PARAM_READWRITE |
                                      G_PARAM_CONSTRUCT_ONLY);
        g_object_class_install_property (object_class,
                                         PROP_SCRAMBLE_LOCATION,
                                         gParamSpecs[PROP_SCRAMBLE_LOCATION]);

        gParamSpecs[PROP_PRIORITY_SOURCE] =
                g_param_spec_boolean ("priority-source",
                                      "PrioritySource",
                                      "Whether source is priority or not",
                                      FALSE,
                                      G_PARAM_READWRITE |
                                      G_PARAM_CONSTRUCT_ONLY);
        g_object_class_install_property (object_class,
                                         PROP_PRIORITY_SOURCE,
                                         gParamSpecs[PROP_PRIORITY_SOURCE]);

}

static void
gclue_location_source_init (GClueLocationSource *source)
{
        source->priv = gclue_location_source_get_instance_private (source);
        source->priv->compute_movement = TRUE;
        source->priv->time_threshold = gclue_min_uint_new ();
        source->priv->priority_source = FALSE;
}

static GClueLocationSourceStartResult
start_source (GClueLocationSource *source)
{
        source->priv->active_counter++;
        if (source->priv->active_counter > 1) {
                g_debug ("%s already active, not starting.",
                         G_OBJECT_TYPE_NAME (source));
                return GCLUE_LOCATION_SOURCE_START_RESULT_ALREADY_STARTED;
        }

#if GCLUE_USE_COMPASS
        if (source->priv->compute_movement) {
                GClueConfig *config = gclue_config_get_singleton ();

                if (gclue_config_get_enable_compass (config)) {
                        source->priv->compass = gclue_compass_get_singleton ();
                        source->priv->heading_changed_id = g_signal_connect
                                (G_OBJECT (source->priv->compass),
                                 "notify::heading",
                                 G_CALLBACK (on_compass_heading_changed),
                                 source);
                } else {
                        source->priv->compass = NULL;
                        g_debug ("Compass is disabled in config");
                }
        }
#endif

        g_object_notify (G_OBJECT (source), "active");
        g_debug ("%s now active", G_OBJECT_TYPE_NAME (source));
        return GCLUE_LOCATION_SOURCE_START_RESULT_OK;
}

static GClueLocationSourceStopResult
stop_source (GClueLocationSource *source)
{
        if (source->priv->active_counter == 0) {
                g_debug ("%s already inactive, not stopping.",
                         G_OBJECT_TYPE_NAME (source));
                return GCLUE_LOCATION_SOURCE_STOP_RESULT_ALREADY_STOPPED;
        }

        source->priv->active_counter--;
        if (source->priv->active_counter > 0) {
                g_debug ("%s still in use, not stopping.",
                         G_OBJECT_TYPE_NAME (source));
                return GCLUE_LOCATION_SOURCE_STOP_RESULT_STILL_USED;
        }

#if GCLUE_USE_COMPASS
        if (source->priv->compass) {
                g_signal_handler_disconnect (source->priv->compass,
                                             source->priv->heading_changed_id);
                g_clear_object (&source->priv->compass);
        }
#endif

        g_object_notify (G_OBJECT (source), "active");
        g_debug ("%s now inactive", G_OBJECT_TYPE_NAME (source));

        return GCLUE_LOCATION_SOURCE_STOP_RESULT_OK;
}

/**
 * gclue_location_source_start:
 * @source: a #GClueLocationSource
 *
 * Start searching for location and keep an eye on location changes.
 **/
void
gclue_location_source_start (GClueLocationSource *source)
{
        g_return_if_fail (GCLUE_IS_LOCATION_SOURCE (source));

        GCLUE_LOCATION_SOURCE_GET_CLASS (source)->start (source);
}

/**
 * gclue_location_source_stop:
 * @source: a #GClueLocationSource
 *
 * Stop searching for location and no need to keep an eye on location changes
 * anymore.
 **/
void
gclue_location_source_stop (GClueLocationSource *source)
{
        g_return_if_fail (GCLUE_IS_LOCATION_SOURCE (source));

        GCLUE_LOCATION_SOURCE_GET_CLASS (source)->stop (source);
}

/**
 * gclue_location_source_get_location:
 * @source: a #GClueLocationSource
 *
 * Returns: (transfer none): The location, or NULL if unknown.
 **/
GClueLocation *
gclue_location_source_get_location (GClueLocationSource *source)
{
        g_return_val_if_fail (GCLUE_IS_LOCATION_SOURCE (source), NULL);

        return source->priv->location;
}

/* 1 km in latitude is always .00899928005759539236 degrees */
#define LATITUDE_IN_KM .00899928005759539236

/**
 * gclue_location_source_set_location:
 * @source: a #GClueLocationSource
 *
 * Set the current location to @location. Its meant to be only used by
 * subclasses.
 **/
void
gclue_location_source_set_location (GClueLocationSource *source,
                                    GClueLocation       *location)
{
        GClueLocationSourcePrivate *priv = source->priv;
        GClueLocation *cur_location;
        gdouble speed, heading;

        cur_location = priv->location;
        priv->location = gclue_location_duplicate (location);

        if (priv->scramble_location) {
                gdouble latitude, distance, accuracy, scramble_range;

                latitude = gclue_location_get_latitude (priv->location);
                accuracy = gclue_location_get_accuracy (priv->location);

                scramble_range = GCLUE_LOCATION_ACCURACY_NEIGHBORHOOD;
                if (accuracy >= scramble_range) {
                        /* If the location source is already pretty inaccurate
                         * do just a limited range scrambling to be sure.
                         */
                        scramble_range /= 3;
                }

                /* Randomization is needed to stop apps from calculationg the
                 * actual location.
                 */
                distance = g_random_double_range (0, scramble_range);
                distance /= 1000;

                if (g_random_boolean ())
                        latitude += distance * LATITUDE_IN_KM;
                else
                        latitude -= distance * LATITUDE_IN_KM;
                accuracy += scramble_range;

                g_object_set (G_OBJECT (priv->location),
                              "latitude", latitude,
                              "accuracy", accuracy,
                              NULL);
                g_debug ("%s location scrambled", G_OBJECT_TYPE_NAME (source));
        }

        speed = gclue_location_get_speed (location);
        if (speed == GCLUE_LOCATION_SPEED_UNKNOWN) {
                if (cur_location != NULL && priv->compute_movement) {
                        guint64 cur_timestamp, timestamp;

                        timestamp = gclue_location_get_timestamp (location);
                        cur_timestamp = gclue_location_get_timestamp
                                        (cur_location);

                        if (timestamp != cur_timestamp)
                                gclue_location_set_speed_from_prev_location
                                        (priv->location, cur_location);
                }
        } else {
                gclue_location_set_speed (priv->location, speed);
        }

#if GCLUE_USE_COMPASS
        set_heading_from_compass (source, location);
#endif
        heading = gclue_location_get_heading (location);
        if (heading == GCLUE_LOCATION_HEADING_UNKNOWN) {
                if (cur_location != NULL && priv->compute_movement)
                        gclue_location_set_heading_from_prev_location
                                (priv->location, cur_location);
        } else {
                gclue_location_set_heading (priv->location, heading);
        }

        g_object_notify (G_OBJECT (source), "location");
        g_clear_object (&cur_location);
}

/**
 * gclue_location_source_get_active:
 * @source: a #GClueLocationSource
 *
 * Returns: TRUE if source is active, FALSE otherwise.
 **/
gboolean
gclue_location_source_get_active (GClueLocationSource *source)
{
        g_return_val_if_fail (GCLUE_IS_LOCATION_SOURCE (source), FALSE);

        return (source->priv->active_counter > 0);
}

/**
 * gclue_location_source_get_priority_source:
 * @source: a #GClueLocationSource
 *
 * Returns: Returns: TRUE if source (e.g. GPS) has priority over other
 * sources, FALSE otherwise.
 **/
gboolean
gclue_location_source_get_priority_source (GClueLocationSource *source)
{
        g_return_val_if_fail (GCLUE_IS_LOCATION_SOURCE (source), FALSE);

        return source->priv->priority_source;
}

/**
 * gclue_location_source_get_available_accuracy_level:
 * @source: a #GClueLocationSource
 *
 * Returns: The currently available accuracy level.
 **/
GClueAccuracyLevel
gclue_location_source_get_available_accuracy_level (GClueLocationSource *source)
{
        g_return_val_if_fail (GCLUE_IS_LOCATION_SOURCE (source), 0);

        return source->priv->avail_accuracy_level;
}

/**
 * gclue_location_source_get_compute_movement
 * @source: a #GClueLocationSource
 *
 * Returns: %TRUE if speed and heading will be automatically computed (or
 * fetched from hardware) and set on new locations, %FALSE otherwise.
 **/
gboolean
gclue_location_source_get_compute_movement (GClueLocationSource *source)
{
        g_return_val_if_fail (GCLUE_IS_LOCATION_SOURCE (source), FALSE);

        return source->priv->compute_movement;
}

/**
 * gclue_location_source_set_compute_movement
 * @source: a #GClueLocationSource
 * @compute: a #gboolean
 *
 * Use this to specify whether or not you want @source to automatically compute
 * (or fetch from hardware) and set speed and heading on new locations.
 **/
void
gclue_location_source_set_compute_movement (GClueLocationSource *source,
                                            gboolean             compute)
{
        g_return_if_fail (GCLUE_IS_LOCATION_SOURCE (source));

        source->priv->compute_movement = compute;
}

/**
 * gclue_location_source_get_time_threshold
 * @source: a #GClueLocationSource
 *
 * Returns: (transfer none): The current time-threshold object, or NULL if
 * @source is not a valid #GClueLocationSource instance.
 **/
GClueMinUINT *
gclue_location_source_get_time_threshold (GClueLocationSource *source)
{
        g_return_val_if_fail (GCLUE_IS_LOCATION_SOURCE (source), NULL);

        return source->priv->time_threshold;
}
