/*
 * cogctl.c
 * Copyright (C) 2021 Igalia S.L.
 * Copyright (C) 2018 Adrian Perez <aperez@igalia.com>
 *
 * Distributed under terms of the MIT license.
 */

#define COG_INSIDE_COG__ 1

#include "../core/cog-utils.h"
#include "cog-config.h"

#include <gio/gio.h>
#include <stdlib.h>
#include <string.h>

#ifndef COG_DEFAULT_APPID
#define COG_DEFAULT_APPID    "com.igalia.Cog"
#endif

#define GTK_ACTIONS_ACTIVATE "org.gtk.Actions", "Activate"
#define FDO_DBUS_PEER_PING   "org.freedesktop.DBus.Peer", "Ping"


static struct {
    char    *appid;
    char    *objpath;
    gboolean system_bus;
} s_options = {
    .appid = COG_DEFAULT_APPID,
};


static GOptionEntry s_cli_options[] = {
    { "appid", 'A', 0, G_OPTION_ARG_STRING, &s_options.appid,
        "Application identifier of the Cog instance to control",
        "ID" },
    { "object-path", 'o', 0, G_OPTION_ARG_STRING, &s_options.objpath,
        "Object path implementing the org.gtk.Actions interface",
        "PATH" },
    { "system", 'y', 0, G_OPTION_ARG_NONE, &s_options.system_bus,
        "Use the system bus instead of the session bus",
        NULL },
    { NULL, }
};


static gboolean
call_method (const char *iface,
             const char *method,
             GVariant   *params,
             GError    **error)
{
    const GBusType bus_type =
        s_options.system_bus ? G_BUS_TYPE_SYSTEM : G_BUS_TYPE_SESSION;
    g_autoptr(GDBusConnection) conn = g_bus_get_sync (bus_type, NULL, error);
    if (!error)
        return FALSE;

    g_autoptr(GVariant) result =
        g_dbus_connection_call_sync (conn,
                                     s_options.appid,
                                     s_options.objpath,
                                     iface,
                                     method,
                                     params,
                                     NULL,
                                     G_DBUS_CALL_FLAGS_NO_AUTO_START,
                                     -1,
                                     NULL,
                                     error);
    return !!result;
}


struct cmd {
    const char *name;
    const char *desc;
    int (*handler) (const char *name, const void *data, int argc, char **argv);
    const void *data;
};

static const struct cmd* cmd_find_by_name (const char *name);


static void
cmd_check_simple_help (const char *name, int needed_argc, int *argc, char ***argv)
{
    const char *space = strchr (name, ' ');
    if (!space) space = strchr (name, '\0');

    g_autofree char *cmd_name = g_strndup (name, space - name);

    g_autoptr(GOptionContext) option_context = g_option_context_new (name);
    g_option_context_set_description (option_context,
                                      cmd_find_by_name (cmd_name)->desc);
    g_option_context_add_main_entries (option_context,
                                       &((GOptionEntry) { NULL, }),
                                       NULL);

    g_autoptr(GError) error = NULL;
    if (!g_option_context_parse (option_context, argc, argv, &error) || *argc > (1 + needed_argc)) {
        g_printerr ("%s: %s\n", name, error ? error->message : "No arguments expected");
        exit (EXIT_FAILURE);
    }
}


static int
cmd_generic_alias (const char *name,
                   const void *data,
                   int         argc,
                   char      **argv)
{
    const struct cmd *cmd = cmd_find_by_name ((const char*) data);
    return (*cmd->handler) (cmd->name, cmd->data, argc, argv);
}

static int
cmd_generic_no_args (const char               *name,
                     G_GNUC_UNUSED const void *data,
                     int                       argc,
                     char                    **argv)
{
    cmd_check_simple_help (name, 0, &argc, &argv);

    GVariant *params = g_variant_new ("(sava{sv})", name, NULL, NULL);

    g_autoptr(GError) error = NULL;
    if (!call_method (GTK_ACTIONS_ACTIVATE, params, &error)) {
        g_printerr ("%s\n", error->message);
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

static int
cmd_appid (const char               *name,
           G_GNUC_UNUSED const void *data,
           int                       argc,
           char                    **argv)
{
    cmd_check_simple_help (name, 0, &argc, &argv);
    g_print ("%s\n", s_options.appid);
    return EXIT_SUCCESS;
}


static int
cmd_objpath (const char               *name,
             G_GNUC_UNUSED const void *data,
             int                       argc,
             char                    **argv)
{
    cmd_check_simple_help (name, 0, &argc, &argv);
    g_print ("%s\n", s_options.objpath);
    return EXIT_SUCCESS;
}


static int
cmd_open (const char               *name,
          G_GNUC_UNUSED const void *data,
          int                       argc,
          char                    **argv)
{
    cmd_check_simple_help ("open URL", 1, &argc, &argv);

    g_autoptr(GError) error = NULL;
    g_autofree char *utf8_uri =
        cog_uri_guess_from_user_input (argv[1], TRUE, &error);
    if (!utf8_uri) {
        g_printerr ("%s\n", error->message);
        return EXIT_FAILURE;
    }

    g_autoptr(GVariantBuilder) param_uri =
        g_variant_builder_new (G_VARIANT_TYPE ("av"));
    g_variant_builder_add (param_uri, "v", g_variant_new_string (utf8_uri));
    GVariant *params = g_variant_new ("(sava{sv})", "open", param_uri, NULL);

    if (!call_method (GTK_ACTIONS_ACTIVATE, params, &error)) {
        g_printerr ("%s\n", error->message);
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}


static int
cmd_ping (const char               *name,
          G_GNUC_UNUSED const void *data,
          int                       argc,
          char                    **argv)
{
    cmd_check_simple_help (name, 0, &argc, &argv);
    return call_method (FDO_DBUS_PEER_PING, NULL, NULL)
        ? EXIT_SUCCESS
        : EXIT_FAILURE;
}


static int
cmd_help (const char *name,
          const void *data,
          int         argc,
          char      **argv)
{
    g_autoptr(GOptionContext) option_context =
        g_option_context_new ("help [command]");
    g_option_context_add_main_entries (option_context,
                                       &((GOptionEntry) { NULL, }),
                                       NULL);

    g_autoptr(GError) error = NULL;
    if (!g_option_context_parse (option_context, &argc, &argv, &error)) {
        g_printerr ("%s: %s\n", argv[0], error->message);
        return EXIT_FAILURE;
    }

    if (argc == 1) {
        g_print ("Available commands:\n");
        const struct cmd *cmds = data;
        for (unsigned i = 0; cmds[i].name; i++) {
            if (cmds[i].desc) {
                g_print ("  %-10s %s\n", cmds[i].name, cmds[i].desc);
            }
        }
    } else if (argc == 2) {
        const struct cmd *cmd = cmd_find_by_name (argv[1]);
        if (!cmd) {
            g_printerr ("Unrecognized command: %s\n", argv[1]);
            return EXIT_FAILURE;
        }

        if (cmd->handler == cmd_generic_alias) {
            g_print ("'%s' is aliased to '%s'\n", argv[1],  (const char*) cmd->data);
        } else {
            char *cmd_argv[] = { (char*) cmd->name, "--help" };
            return (*cmd->handler) (cmd->name, cmd->data, G_N_ELEMENTS (cmd_argv), cmd_argv);
        }
    } else {
        g_printerr ("Invalid command line ussage\n");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}


static const struct cmd*
cmd_find_by_name (const char *name)
{
    static const struct cmd cmdlist[] = {
        {
            .name = "appid",
            .desc = "Display application ID being remotely controlled",
            .handler = cmd_appid,
        },
        {
            .name = "objpath",
            .desc = "Display the D-Bus object path being used",
            .handler = cmd_objpath,
        },
        {
            .name = "help",
            .desc = "Obtain help about commands",
            .data = cmdlist,
            .handler = cmd_help,
        },
        {
            .name = "open",
            .desc = "Open a URL",
            .handler = cmd_open,
        },
        {
            .name = "previous",
            .desc = "Navigate backward in the page view history",
            .handler = cmd_generic_no_args,
        },
        {
            .name = "prev",
            .data = "previous",
            .handler = cmd_generic_alias,
        },
        {
            .name = "next",
            .desc = "Navigate forward in the page view history",
            .handler = cmd_generic_no_args,
        },
        {
            .name = "ping",
            .desc = "Check whether Cog is running",
            .handler = cmd_ping,
        },
        {
            .name = "quit",
            .desc = "Exit the application",
            .handler = cmd_generic_no_args,
        },
        {
            .name = "reload",
            .desc = "Reload the current page",
            .handler = cmd_generic_no_args,
        },
        {
            .name = NULL,
        },
    };

    for (unsigned i = 0; cmdlist[i].name; i++)
        if (strcmp (cmdlist[i].name, name) == 0)
            return &cmdlist[i];

    return NULL;
}


static const char s_extended_help_text[] =
    "For a list of commands use:\n"
    "  cogctl help\n"
    "\n"
    "Help on each command can be obtained with one of:\n"
    "  cogctl help <command>\n"
    "  cogctl <command> --help\n";


int
main (int argc, char **argv)
{
    g_autoptr(GOptionContext) option_context =
        g_option_context_new ("<subcommand> [SUBCOMMAND-OPTION?]");
    g_option_context_set_description (option_context, s_extended_help_text);
    g_option_context_add_main_entries (option_context, s_cli_options, NULL);
    g_option_context_set_strict_posix (option_context, TRUE);

    g_autoptr(GError) error = NULL;
    if (!g_option_context_parse (option_context, &argc, &argv, &error)) {
        g_printerr ("Command line error: %s\n", error->message);
        return EXIT_FAILURE;
    }

    if (!g_application_id_is_valid (s_options.appid)) {
        g_printerr ("Invalid application ID: %s\n", s_options.appid);
        return EXIT_FAILURE;
    }

    if (!s_options.objpath) {
        s_options.objpath = cog_appid_to_dbus_object_path (s_options.appid);
    }
    if (!g_variant_is_object_path (s_options.objpath)) {
        g_printerr ("Invalid D-Bus object path: %s\n", s_options.objpath);
        return EXIT_FAILURE;
    }

    if (argc < 2) {
        g_printerr ("Missing subcommand\n");
        return EXIT_FAILURE;
    }

    const struct cmd *cmd = cmd_find_by_name (argv[1]);
    if (!cmd) {
        g_printerr ("Unrecognized command: %s\n", argv[1]);
        return EXIT_FAILURE;
    }

    return (*cmd->handler) (cmd->name, cmd->data, argc - 1, argv + 1);
}
