/* geoclue-stumbler-window.c
 *
 * Copyright 2013 Red Hat, Inc.
 * Copyright 2024 Christopher Talbot
 *
 * This program 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 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

// A lot of code shamelessly taken from https://gitlab.freedesktop.org/geoclue/geoclue/-/blob/master/demo/where-am-i.c?ref_type=heads

#include "config.h"

#include "geoclue-stumbler-window.h"
#include "geoclue-stumbler-maps-page.h"
#include "geoclue-stumbler-path-page.h"
#include "geoclue-stumbler-stats-page.h"
#include "geoclue-stumbler-settings.h"

#include <config.h>

#include <stdlib.h>
#include <locale.h>
#include <glib/gi18n.h>
#include <geoclue.h>
#include <shumate/shumate.h>

#define GEOCLUE_TIMEOUT_SECONDS 30
//From Geoclue gclue_web_source.c
#define SUBMISSION_ACCURACY_THRESHOLD 100

#define GEOCLUE_STUMBLER_SERVICE     "org.kop316.geoclue_stumbler_http"
#define GEOCLUE_STUMBLER_PATH        "/org/kop316/geoclue_stumbler_http"

#define GEOCLUE_STUMBLER_SERVICE_INTERFACE   GEOCLUE_STUMBLER_SERVICE ".Service"

struct _GeoclueStumblerWindow
{
    AdwApplicationWindow  parent_instance;
    AdwToastOverlay *toast_overlay;

    GtkLabel     *connected_label;
    GtkLabel     *submit_label;
    GtkLabel     *latitude_label;
    GtkLabel     *longitude_label;
    GtkLabel     *accuracy_label;
    GtkLabel     *altitude_label;
    AdwActionRow *altitude_row;
    GtkLabel     *speed_label;
    AdwActionRow *speed_row;
    GtkLabel     *heading_label;
    AdwActionRow *heading_row;
    GtkLabel     *source_label;
    AdwActionRow *source_row;
    GtkLabel     *timestamp_label;
    AdwActionRow *timestamp_row;

    GtkWidget    *maps_page_bin;
    GtkWidget    *path_page_bin;
    GtkWidget    *stats_page_bin;
    GtkButton    *upload_submission_button;

    GClueSimple *simple;

    guint         http_watch_id;
    guint         http_signal_id;
    GDBusConnection  *connection;
    GDBusProxy       *proxy;

    guint         upload_timeout_id;
};

G_DEFINE_FINAL_TYPE (GeoclueStumblerWindow, geoclue_stumbler_window, ADW_TYPE_APPLICATION_WINDOW)

static void
geoclue_stumbler_window_display_toast (GeoclueStumblerWindow *self,
                                       const char            *title)
{
  AdwToast *toast = adw_toast_new (title);
  adw_toast_overlay_add_toast (self->toast_overlay, toast);
  adw_toast_set_timeout (toast, 1);
}

static gboolean
on_location_timeout (gpointer user_data)
{
    AdwDialog *dialog;
    GeoclueStumblerWindow *self = user_data;

    if (self->simple)
        return G_SOURCE_REMOVE;

    dialog = adw_alert_dialog_new (_("Alert"),
                                   _("Cannot connect to Geoclue. Please quit this app and enable Geoclue."));

    adw_alert_dialog_add_responses (ADW_ALERT_DIALOG (dialog),
                                    "close",  _("_Close"),
                                    NULL);

    adw_dialog_present (dialog, GTK_WIDGET (self));

    return G_SOURCE_REMOVE;
}

static void
print_location (GeoclueStumblerWindow *self)
{
    GClueLocation *location;
    gdouble altitude, speed, heading;
    g_autofree char *latitude = NULL;
    g_autofree char *longitude = NULL;
    g_autofree char *accuracy = NULL;

    GVariant *timestamp;
    const char *desc;

    location = gclue_simple_get_location (self->simple);
    g_debug ("New location:");
    g_debug ("Latitude:    %f°", gclue_location_get_latitude (location));
    g_debug ("Longitude:   %f°", gclue_location_get_longitude (location));
    g_debug ("Accuracy:    %f meters", gclue_location_get_accuracy (location));

    latitude = g_strdup_printf("%f°", gclue_location_get_latitude (location));
    gtk_label_set_label (self->latitude_label, latitude);

    longitude = g_strdup_printf("%f°", gclue_location_get_longitude (location));
    gtk_label_set_label (self->longitude_label, longitude);

    if (gclue_location_get_accuracy (location) < 0.99)
        accuracy = g_strdup(_("Exact"));
    else
        accuracy = g_strdup_printf(_("%.0f m (%.0f ft)"),
                                   gclue_location_get_accuracy (location),
                                   gclue_location_get_accuracy (location)*METERS_TO_FEET);

    gtk_label_set_label (self->accuracy_label, accuracy);

    altitude = gclue_location_get_altitude (location);
    if (altitude != -G_MAXDOUBLE) {
        g_autofree char *alt_label = NULL;
        g_debug ("Altitude:    %.2f meters", altitude);
        alt_label = g_strdup_printf("%.1f m (%.0f ft)", altitude, altitude*METERS_TO_FEET);
        gtk_label_set_label (self->altitude_label, alt_label);
        gtk_widget_set_visible (GTK_WIDGET(self->altitude_row), true);
    } else {
        gtk_widget_set_visible (GTK_WIDGET(self->altitude_row), false);
        g_debug ("Altitude:   -G_MAXDOUBLE");
    }

    speed = gclue_location_get_speed (location);
    g_debug ("Speed:       %f meters/second", speed);
    if (speed > 0) {
        g_autofree char *speed_label_text = NULL;
        speed_label_text = g_strdup_printf(_("%.1f m/s (%.2f mph)"), speed, speed*METERS_TO_SECOND_TO_MPH);
        gtk_label_set_label (self->speed_label, speed_label_text);
        gtk_widget_set_visible (GTK_WIDGET(self->speed_row), true);
    } else
        gtk_widget_set_visible (GTK_WIDGET(self->speed_row), false);

    heading = gclue_location_get_heading (location);
    g_debug ("Heading:     %f°", heading);
    if (heading > 0.0001) {
        g_autofree char *heading_label_text = NULL;
        heading_label_text = g_strdup_printf("%.0f°", heading);
        gtk_label_set_label (self->heading_label, heading_label_text);
        gtk_widget_set_visible (GTK_WIDGET(self->heading_row), true);
    } else
        gtk_widget_set_visible (GTK_WIDGET(self->heading_row), false);

    desc = gclue_location_get_description (location);
    g_debug ("Description: %s", desc);
    if (desc != NULL && strlen (desc) > 0) {
        gtk_label_set_label (self->source_label, desc);
        gtk_widget_set_visible (GTK_WIDGET(self->source_row), true);
    } else
        gtk_widget_set_visible (GTK_WIDGET(self->source_row), false);

    timestamp = gclue_location_get_timestamp (location);
    if (timestamp) {
        g_autoptr(GDateTime) date_time = NULL;
        guint64 sec, usec;
        g_autofree gchar *str = NULL;

        g_variant_get (timestamp, "(tt)", &sec, &usec);

        /* Ignore usec, since it is not in the output format */
        date_time = g_date_time_new_from_unix_local ((gint64) sec);
        str = g_date_time_format (date_time, "%c");

        g_debug ("Timestamp:   %s", str);
        gtk_label_set_label (self->timestamp_label, str);
        gtk_widget_set_visible(GTK_WIDGET(self->timestamp_row), true);
    } else
        gtk_widget_set_visible(GTK_WIDGET(self->timestamp_row), false);

    if (desc != NULL && strstr(desc, "GPS") != NULL && gclue_location_get_accuracy (location) <= SUBMISSION_ACCURACY_THRESHOLD) {
        gtk_label_set_label (self->submit_label, _("Yes"));

        geoclue_stumbler_path_page_set_gps_lock (GEOCLUE_STUMBLER_PATH_PAGE (self->path_page_bin), TRUE);
        geoclue_stumbler_path_page_update_page (GEOCLUE_STUMBLER_PATH_PAGE (self->path_page_bin),
                                                gclue_location_get_latitude (location),
                                                gclue_location_get_longitude (location),
                                                gclue_location_get_speed (location),
                                                gclue_location_get_altitude (location));

        geoclue_stumbler_maps_page_update_marker (GEOCLUE_STUMBLER_MAPS_PAGE (self->maps_page_bin),
                                                  gclue_location_get_latitude (location),
                                                  gclue_location_get_longitude (location),
                                                  gclue_location_get_heading (location));

    } else {
        gtk_label_set_label (self->submit_label, _("No"));
        geoclue_stumbler_path_page_set_gps_lock (GEOCLUE_STUMBLER_PATH_PAGE (self->path_page_bin), FALSE);
        geoclue_stumbler_maps_page_update_marker_no_fix (GEOCLUE_STUMBLER_MAPS_PAGE (self->maps_page_bin),
                                                         gclue_location_get_latitude (location),
                                                         gclue_location_get_longitude (location),
                                                         gclue_location_get_accuracy (location));
    }

}

static void
on_simple_ready (GObject      *source_object,
                 GAsyncResult *res,
                 gpointer      user_data)
{
    GeoclueStumblerWindow *self = user_data;
    GError *error = NULL;

    self->simple = gclue_simple_new_with_thresholds_finish (res, &error);
    if (error != NULL) {
        g_critical ("Failed to connect to GeoClue2 service: %s", error->message);

        on_location_timeout (self);
    }

    gtk_label_set_label (self->connected_label, "Connected");
    print_location (self);

    g_signal_connect_swapped (self->simple,
                              "notify::location",
                              G_CALLBACK (print_location),
                              self);
}

static void
geoclue_stumbler_window_submission_updated (GeoclueStumblerWindow *self,
                                            GVariant        *parameters)
{
  g_autoptr (GVariant) variantstatus = NULL;
  const char *json_contents;

  g_variant_get (parameters, "(sv)", NULL, &variantstatus);
  json_contents = g_variant_get_string (variantstatus, NULL);

  geoclue_stumbler_stats_page_new_submission (GEOCLUE_STUMBLER_STATS_PAGE (self->stats_page_bin),
                                              json_contents,
                                              FALSE);
}

static gboolean
error_upload_timeout (gpointer user_data)
{
  GeoclueStumblerWindow *self = user_data;

  self->upload_timeout_id = 0;
  /* Since there was an error, we don't know how many subissions are left */
  g_debug ("Checking how many submissions are left now");
  geoclue_stumbler_stats_reset_submissions (GEOCLUE_STUMBLER_STATS_PAGE (self->stats_page_bin));
  return G_SOURCE_REMOVE;
}

static void
geoclue_stumbler_window_uploading_error (GeoclueStumblerWindow *self)
{
  if (self->upload_timeout_id == 0) {
    geoclue_stumbler_window_display_toast (self, _("Error Uploading Submissions!"));
  } else {
    g_source_remove (self->upload_timeout_id);
    self->upload_timeout_id = 0;
  }
    self->upload_timeout_id = g_timeout_add_seconds (3, error_upload_timeout, self);
}

static void
geoclue_stumbler_window_uploading_success (GeoclueStumblerWindow *self)
{
  GeoclueStumblerSettings *settings;

  settings = geoclue_stumbler_settings_get_default ();
  g_debug ("Upload Success");
  geoclue_stumbler_settings_add_total_number_of_submissions (settings, 1);
  geoclue_stumbler_stats_update_total_submission_label (GEOCLUE_STUMBLER_STATS_PAGE (self->stats_page_bin));
}

static void
http_proxy_signal_emitted_cb (GDBusConnection *connection,
                              const char      *sender_name,
                              const char      *object_path,
                              const char      *interface_name,
                              const char      *signal_name,
                              GVariant        *parameters,
                              gpointer         user_data)
{
  GeoclueStumblerWindow *self = user_data;

  g_assert (G_IS_DBUS_CONNECTION (connection));

  if (g_strcmp0 (signal_name, "SubmissionAdded") == 0 &&
      g_strcmp0 (interface_name, GEOCLUE_STUMBLER_SERVICE) == 0 &&
      g_strcmp0 (object_path, GEOCLUE_STUMBLER_PATH) == 0)
    geoclue_stumbler_window_submission_updated (self, parameters);
  else if (g_strcmp0 (signal_name, "UploadError") == 0 &&
      g_strcmp0 (interface_name, GEOCLUE_STUMBLER_SERVICE) == 0 &&
      g_strcmp0 (object_path, GEOCLUE_STUMBLER_PATH) == 0)
    geoclue_stumbler_window_uploading_error (self);
  else if (g_strcmp0 (signal_name, "UploadSuccess") == 0 &&
      g_strcmp0 (interface_name, GEOCLUE_STUMBLER_SERVICE) == 0 &&
      g_strcmp0 (object_path, GEOCLUE_STUMBLER_PATH) == 0)
    geoclue_stumbler_window_uploading_success (self);
  else
    g_warning ("Unknown Signal emitted");
}

static void
send_submissions_cb (GObject      *service,
                     GAsyncResult *res,
                     gpointer      user_data)
{
  GeoclueStumblerWindow *self = user_data;
  g_autoptr(GVariant) ret = NULL;
  g_autoptr(GError) error = NULL;

  ret = g_dbus_proxy_call_finish (self->proxy,
                                  res,
                                  &error);

  if (error != NULL) {
    g_warning ("Error in Proxy call: %s\n", error->message);
    return;
  }

  geoclue_stumbler_stats_reset_submissions (GEOCLUE_STUMBLER_STATS_PAGE (self->stats_page_bin));
  geoclue_stumbler_window_display_toast (self, _("Submitted All Submissions"));
}

static void
geoclue_stumbler_send_submission (GeoclueStumblerWindow *self)
{
  GeoclueStumblerSettings *settings;
  g_autofree char *submission_url = NULL;
  unsigned int queued_submissions = 0;

  settings = geoclue_stumbler_settings_get_default ();
  submission_url = geoclue_stumbler_settings_get_submission_url (settings);

  g_debug("Submission URL: %s", submission_url);

  if (g_strcmp0(submission_url, "https://example.com/v2/geosubmit") == 0 ||
      !submission_url || !*submission_url) {
    AdwDialog *dialog;

    dialog = adw_alert_dialog_new (_("Submission URL Not Set"),
                                   _("Please set this in the Preferences menu"));

    adw_alert_dialog_add_responses (ADW_ALERT_DIALOG (dialog),
                                    "close",  _("_Close"),
                                    NULL);

    adw_alert_dialog_set_default_response (ADW_ALERT_DIALOG (dialog), "close");
    adw_alert_dialog_set_close_response (ADW_ALERT_DIALOG (dialog), "close");
    adw_dialog_present (dialog, GTK_WIDGET (self));

    return;
  }

  queued_submissions = geoclue_stumbler_stats_get_queued_submission_count (GEOCLUE_STUMBLER_STATS_PAGE (self->stats_page_bin));

  if (queued_submissions == 0) {
    AdwDialog *dialog;

    dialog = adw_alert_dialog_new (_("No Submissions"),
                                   _("There are no submissions to upload"));

    adw_alert_dialog_add_responses (ADW_ALERT_DIALOG (dialog),
                                    "close",  _("_Close"),
                                    NULL);

    adw_alert_dialog_set_default_response (ADW_ALERT_DIALOG (dialog), "close");
    adw_alert_dialog_set_close_response (ADW_ALERT_DIALOG (dialog), "close");
    adw_dialog_present (dialog, GTK_WIDGET (self));

    return;
  }

  if (!self->proxy) {
    g_debug ("No Proxy to submit submission to");
    return;
  }
  g_dbus_proxy_call (self->proxy,
                     "SubmitSubmissions",
                     g_variant_new_parsed ("('URL', <%s>)", submission_url),
                     G_DBUS_CALL_FLAGS_NONE,
                     -1,
                     NULL,
                     (GAsyncReadyCallback)send_submissions_cb,
                     self);

  geoclue_stumbler_stats_page_new_upload (GEOCLUE_STUMBLER_STATS_PAGE (self->stats_page_bin));

}

static void
submission_button_clicked_cb (GeoclueStumblerWindow *self)
{
  g_debug ("Submitting Submissions");
  geoclue_stumbler_send_submission (self);
}

static void
get_proxy_cb (GObject      *simple,
              GAsyncResult *res,
              gpointer      user_data)
{
  GeoclueStumblerWindow *self = user_data;
  g_autoptr(GError) error = NULL;

  self->proxy = g_dbus_proxy_new_finish (res, &error);

  if (error != NULL) {
    g_warning ("Error in Proxy call: %s\n", error->message);
    return;
  }

  g_debug ("Have proxy");
  gtk_widget_set_sensitive (GTK_WIDGET (self->upload_submission_button), TRUE);
}

static void
dbus_appeared_cb (GDBusConnection *connection,
                  const char      *name,
                  const char      *name_owner,
                  gpointer         user_data)
{
  GeoclueStumblerWindow *self = user_data;
  g_assert (G_IS_DBUS_CONNECTION (connection));
  self->connection = connection;
  g_debug ("HTTP Proxy appeared");

  self->http_signal_id =
    g_dbus_connection_signal_subscribe (self->connection,
                                        GEOCLUE_STUMBLER_SERVICE,
                                        NULL, NULL, NULL, NULL,
                                        G_DBUS_SIGNAL_FLAGS_NONE,
                                        http_proxy_signal_emitted_cb,
                                        self,
                                        NULL);

  g_dbus_proxy_new (self->connection,
                    G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
                    NULL,
                    GEOCLUE_STUMBLER_SERVICE,
                    GEOCLUE_STUMBLER_PATH,
                    GEOCLUE_STUMBLER_SERVICE_INTERFACE,
                    NULL,
                    get_proxy_cb,
                    self);

}

static void
dbus_vanished_cb (GDBusConnection *connection,
                  const char      *name,
                  gpointer        user_data)
{
  GeoclueStumblerWindow *self = user_data;
  g_assert (G_IS_DBUS_CONNECTION (connection));
  g_debug ("HTTP Proxy vanished");

  if (GTK_IS_WIDGET (self->upload_submission_button))
    gtk_widget_set_sensitive (GTK_WIDGET (self->upload_submission_button), FALSE);

  g_clear_object (&self->proxy);
  if (self->http_signal_id && G_IS_DBUS_CONNECTION (self->connection)) {
    g_dbus_connection_signal_unsubscribe (self->connection, self->http_signal_id);
    self->http_signal_id = 0;
  }
}

static void
geoclue_stumbler_window_finalize (GObject *object)
{
  GeoclueStumblerWindow *self = (GeoclueStumblerWindow *)object;
  g_clear_object (&self->simple);

  if (G_IS_DBUS_CONNECTION (self->connection)) {
    dbus_vanished_cb (self->connection, GEOCLUE_STUMBLER_SERVICE, self);
    g_clear_object (&self->connection);
  }

  g_clear_handle_id (&self->http_watch_id, g_bus_unwatch_name);

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


static void
geoclue_stumbler_window_class_init (GeoclueStumblerWindowClass *klass)
{
    GObjectClass   *object_class = G_OBJECT_CLASS (klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

    gtk_widget_class_set_template_from_resource (widget_class, "/org/kop316/stumbler/geoclue-stumbler-window.ui");
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, connected_label);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, submit_label);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, latitude_label);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, longitude_label);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, accuracy_label);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, altitude_label);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, altitude_row);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, speed_label);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, speed_row);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, heading_label);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, heading_row);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, source_label);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, source_row);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, timestamp_label);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, timestamp_row);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, maps_page_bin);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, path_page_bin);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, stats_page_bin);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, upload_submission_button);
    gtk_widget_class_bind_template_child (widget_class, GeoclueStumblerWindow, toast_overlay);

    gtk_widget_class_bind_template_callback (widget_class, submission_button_clicked_cb);

    object_class->finalize = geoclue_stumbler_window_finalize;
}

static void
geoclue_stumbler_window_init (GeoclueStumblerWindow *self)
{
    GClueAccuracyLevel accuracy_level = GCLUE_ACCURACY_LEVEL_EXACT;
    gint time_threshold = 0;
    /*
    Shumate doesn't provide an init() function
    https://gitlab.gnome.org/GNOME/libshumate/-/issues/64
    */
    g_type_ensure (SHUMATE_TYPE_SIMPLE_MAP);
    g_type_ensure (GEOCLUE_STUMBLER_TYPE_MAPS_PAGE);
    g_type_ensure (GEOCLUE_STUMBLER_TYPE_PATH_PAGE);
    g_type_ensure (GEOCLUE_STUMBLER_TYPE_STATS_PAGE);

    gtk_widget_init_template (GTK_WIDGET (self));

    gclue_simple_new_with_thresholds ("geoclue-where-am-i",
                                      accuracy_level,
                                      time_threshold,
                                      0,
                                      NULL,
                                      on_simple_ready,
                                      self);

    g_timeout_add_seconds (GEOCLUE_TIMEOUT_SECONDS, on_location_timeout, self);

    self->simple = NULL;
    geoclue_stumbler_path_page_set_toast_overlay (GEOCLUE_STUMBLER_PATH_PAGE (self->path_page_bin),
                                                  self->toast_overlay);

    geoclue_stumbler_path_page_set_maps_page (GEOCLUE_STUMBLER_PATH_PAGE (self->path_page_bin),
                                              self->maps_page_bin);

    geoclue_stumbler_stats_page_set_maps_page (GEOCLUE_STUMBLER_STATS_PAGE (self->stats_page_bin),
                                               self->maps_page_bin);

    geoclue_stumbler_maps_page_set_path_page (GEOCLUE_STUMBLER_MAPS_PAGE (self->maps_page_bin),
                                              self->path_page_bin);

    self->http_watch_id = g_bus_watch_name (G_BUS_TYPE_SESSION,
                                            GEOCLUE_STUMBLER_SERVICE,
                                            G_BUS_NAME_WATCHER_FLAGS_AUTO_START,
                                            dbus_appeared_cb,
                                            dbus_vanished_cb,
                                            self, NULL);

    self->upload_timeout_id = 0;
}
