/*
 * Copyright © 2013 Canonical Limited
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Ryan Lortie <desrt@desrt.ca>
 */

#include "config.h"

#include <gio/gdesktopappinfo.h>

#include <glib/gi18n.h>
#include <gio/gio.h>

#include <string.h>
#include <locale.h>

struct help_topic
{
  const gchar *command;
  const gchar *summary;
  const gchar *description;
  const gchar *synopsis;
};

struct help_substvar
{
  const gchar *var;
  const gchar *description;
};

static const struct help_topic topics[] = {
  { "help",         N_("Print help"),
                    N_("Print help"),
                    N_("[COMMAND]")
  },
  { "version",      N_("Print version"),
                    N_("Print version information and exit"),
                    NULL
  },
  { "list-apps",    N_("List applications"),
                    N_("List the installed D-Bus activatable applications (by .desktop files)"),
                    NULL
  },
  { "launch",       N_("Launch an application"),
                    N_("Launch the application (with optional files to open)"),
                    N_("APPID [FILE…]")
  },
  { "action",       N_("Activate an action"),
                    N_("Invoke an action on the application"),
                    N_("APPID ACTION [PARAMETER]")
  },
  { "list-actions", N_("List available actions"),
                    N_("List static actions for an application (from .desktop file)"),
                    N_("APPID")
  }
};

static const struct help_substvar substvars[] = {
  { N_("COMMAND"),   N_("The command to print detailed help for")                             },
  { N_("APPID"),     N_("Application identifier in D-Bus format (eg: org.example.viewer)")    },
  { N_("FILE"),      N_("Optional relative or absolute filenames, or URIs to open")           },
  { N_("ACTION"),    N_("The action name to invoke")                                          },
  { N_("PARAMETER"), N_("Optional parameter to the action invocation, in GVariant format")    }
};

static int
app_help (gboolean     requested,
          const gchar *command)
{
  const struct help_topic *topic = NULL;
  GString *string;

  string = g_string_new (NULL);

  if (command)
    {
      gsize i;

      for (i = 0; i < G_N_ELEMENTS (topics); i++)
        if (g_str_equal (topics[i].command, command))
          topic = &topics[i];

      if (!topic)
        {
          g_string_printf (string, _("Unknown command %s\n\n"), command);
          requested = FALSE;
        }
    }

  g_string_append (string, _("Usage:\n"));

  if (topic)
    {
      guint maxwidth;
      gsize i;

      g_string_append_printf (string, "\n  %s %s %s\n\n", "gapplication",
                              topic->command, topic->synopsis ? _(topic->synopsis) : "");
      g_string_append_printf (string, "%s\n\n", _(topic->description));

      if (topic->synopsis)
        {
          g_string_append (string, _("Arguments:\n"));

          maxwidth = 0;
          for (i = 0; i < G_N_ELEMENTS (substvars); i++)
            if (strstr (topic->synopsis, substvars[i].var))
              maxwidth = MAX(maxwidth, strlen (_(substvars[i].var)));

          for (i = 0; i < G_N_ELEMENTS (substvars); i++)
            if (strstr (topic->synopsis, substvars[i].var))
              g_string_append_printf (string, "  %-*.*s   %s\n", maxwidth, maxwidth,
                                      _(substvars[i].var), _(substvars[i].description));
          g_string_append (string, "\n");
        }
    }
  else
    {
      guint maxwidth;
      gsize i;

      g_string_append_printf (string, "\n  %s %s %s\n\n", "gapplication", _("COMMAND"), _("[ARGS…]"));
      g_string_append_printf (string, _("Commands:\n"));

      maxwidth = 0;
      for (i = 0; i < G_N_ELEMENTS (topics); i++)
        maxwidth = MAX(maxwidth, strlen (topics[i].command));

      for (i = 0; i < G_N_ELEMENTS (topics); i++)
        g_string_append_printf (string, "  %-*.*s   %s\n", maxwidth, maxwidth,
                                topics[i].command, _(topics[i].summary));

      g_string_append (string, "\n");
      /* Translators: do not translate 'help', but please translate 'COMMAND'. */
      g_string_append_printf (string, _("Use “%s help COMMAND” to get detailed help.\n\n"), "gapplication");
    }

  if (requested)
    g_print ("%s", string->str);
  else
    g_printerr ("%s\n", string->str);

  g_string_free (string, TRUE);

  return requested ? 0 : 1;
}

static gboolean
app_check_name (gchar       **args,
                const gchar  *command)
{
  if (args[0] == NULL)
    {
      g_printerr (_("%s command requires an application id to directly follow\n\n"), command);
      return FALSE;
    }

  if (!g_dbus_is_name (args[0]))
    {
      g_printerr (_("invalid application id: “%s”\n"), args[0]);
      return FALSE;
    }

  return TRUE;
}

static int
app_no_args (const gchar *command)
{
  /* Translators: %s is replaced with a command name like 'list-actions' */
  g_printerr (_("“%s” takes no arguments\n\n"), command);
  return app_help (FALSE, command);
}

static int
app_version (gchar **args)
{
  if (g_strv_length (args))
    return app_no_args ("version");

  g_print (PACKAGE_VERSION "\n");
  return 0;
}

static int
app_list (gchar **args)
{
  GList *apps;

  if (g_strv_length (args))
    return app_no_args ("list");

  apps = g_app_info_get_all ();

  while (apps)
    {
      GDesktopAppInfo *info = apps->data;

      if (G_IS_DESKTOP_APP_INFO (info))
        if (g_desktop_app_info_get_boolean (info, "DBusActivatable"))
          {
            const gchar *filename;

            filename = g_app_info_get_id (G_APP_INFO (info));
            if (g_str_has_suffix (filename, ".desktop"))
              {
                gchar *id;

                id = g_strndup (filename, strlen (filename) - 8);
                g_print ("%s\n", id);
                g_free (id);
              }
          }

      apps = g_list_delete_link (apps, apps);
      g_object_unref (info);
    }

  return 0;
}

static gchar *
app_path_for_id (const gchar *app_id)
{
  gchar *path;
  gint i;

  path = g_strconcat ("/", app_id, NULL);
  for (i = 0; path[i]; i++)
    {
      if (path[i] == '.')
        path[i] = '/';
      if (path[i] == '-')
        path[i] = '_';
    }

  return path;
}

static int
app_call (const gchar *app_id,
          const gchar *method_name,
          GVariant    *parameters)
{
  GDBusConnection *session;
  GError *error = NULL;
  gchar *object_path;
  GVariant *result;


  session = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
  if (!session)
    {
      g_variant_unref (g_variant_ref_sink (parameters));
      g_printerr (_("unable to connect to D-Bus: %s\n"), error->message);
      g_error_free (error);
      return 1;
    }

  object_path = app_path_for_id (app_id);

  result = g_dbus_connection_call_sync (session, app_id, object_path, "org.freedesktop.Application",
                                        method_name, parameters, G_VARIANT_TYPE_UNIT,
                                        G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);

  g_free (object_path);

  if (result)
    {
      g_variant_unref (result);
      return 0;
    }
  else
    {
      g_printerr (_("error sending %s message to application: %s\n"), method_name, error->message);
      g_error_free (error);
      return 1;
    }
}

static GVariant *
app_get_platform_data (void)
{
  GVariantBuilder builder;
  const gchar *startup_id;

  g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT);

  if ((startup_id = g_getenv ("DESKTOP_STARTUP_ID")))
    g_variant_builder_add (&builder, "{sv}", "desktop-startup-id", g_variant_new_string (startup_id));

  if ((startup_id = g_getenv ("XDG_ACTIVATION_TOKEN")))
    g_variant_builder_add (&builder, "{sv}", "activation-token", g_variant_new_string (startup_id));

  return g_variant_builder_end (&builder);
}

static int
app_action (gchar **args)
{
  GVariantBuilder params;
  const gchar *name;

  if (!app_check_name (args, "action"))
    return 1;

  if (args[1] == NULL)
    {
      g_printerr (_("action name must be given after application id\n"));
      return 1;
    }

  name = args[1];

  if (!g_action_name_is_valid (name))
    {
      g_printerr (_("invalid action name: “%s”\n"
                    "action names must consist of only alphanumerics, “-” and “.”\n"), name);
      return 1;
    }

  g_variant_builder_init (&params, G_VARIANT_TYPE ("av"));

  if (args[2])
    {
      GError *error = NULL;
      GVariant *parameter;

      parameter = g_variant_parse (NULL, args[2], NULL, NULL, &error);

      if (!parameter)
        {
          gchar *context;

          context = g_variant_parse_error_print_context (error, args[2]);
          g_printerr (_("error parsing action parameter: %s\n"), context);
          g_variant_builder_clear (&params);
          g_error_free (error);
          g_free (context);
          return 1;
        }

      g_variant_builder_add (&params, "v", parameter);
      g_variant_unref (parameter);

      if (args[3])
        {
          g_printerr (_("actions accept a maximum of one parameter\n"));
          g_variant_builder_clear (&params);
          return 1;
        }
    }

  return app_call (args[0], "ActivateAction", g_variant_new ("(sav@a{sv})", name, &params, app_get_platform_data ()));
}

static int
app_activate (const gchar *app_id)
{
  return app_call (app_id, "Activate", g_variant_new ("(@a{sv})", app_get_platform_data ()));
}

static int
app_launch (gchar **args)
{
  GVariantBuilder files;
  gint i;

  if (!app_check_name (args, "launch"))
    return 1;

  if (args[1] == NULL)
    return app_activate (args[0]);

  g_variant_builder_init (&files, G_VARIANT_TYPE_STRING_ARRAY);

  for (i = 1; args[i]; i++)
    {
      GFile *file;

      /* "This operation never fails" */
      file = g_file_new_for_commandline_arg (args[i]);
      g_variant_builder_add_value (&files, g_variant_new_take_string (g_file_get_uri (file)));
      g_object_unref (file);
    }

  return app_call (args[0], "Open", g_variant_new ("(as@a{sv})", &files, app_get_platform_data ()));
}

static int
app_list_actions (gchar **args)
{
  const gchar * const *actions;
  GDesktopAppInfo *app_info;
  gchar *filename;
  gint i;

  if (!app_check_name (args, "list-actions"))
    return 1;

  if (args[1])
    {
      g_printerr (_("list-actions command takes only the application id"));
      app_help (FALSE, "list-actions");
    }

  filename = g_strconcat (args[0], ".desktop", NULL);
  app_info = g_desktop_app_info_new (filename);
  g_free (filename);

  if (app_info == NULL)
    {
      g_printerr (_("unable to find desktop file for application %s\n"), args[0]);
      return 1;
    }

  actions = g_desktop_app_info_list_actions (app_info);

  for (i = 0; actions[i]; i++)
    g_print ("%s\n", actions[i]);

  g_object_unref (app_info);

  return 0;
}

int
main (int argc, char **argv)
{
  setlocale (LC_ALL, "");
  textdomain (GETTEXT_PACKAGE);
  bindtextdomain (GETTEXT_PACKAGE, GLIB_LOCALE_DIR);
#ifdef HAVE_BIND_TEXTDOMAIN_CODESET
  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
#endif

  if (argc < 2)
    return app_help (TRUE, NULL);

  if (g_str_equal (argv[1], "help"))
    return app_help (TRUE, argv[2]);

  if (g_str_equal (argv[1], "version"))
    return app_version (argv + 2);

  if (g_str_equal (argv[1], "list-apps"))
    return app_list (argv + 2);

  if (g_str_equal (argv[1], "launch"))
    return app_launch (argv + 2);

  if (g_str_equal (argv[1], "action"))
    return app_action (argv + 2);

  if (g_str_equal (argv[1], "list-actions"))
    return app_list_actions (argv + 2);

  g_printerr (_("unrecognised command: %s\n\n"), argv[1]);

  return app_help (FALSE, NULL);
}
