/* vim:set et sts=4 sw=4:
 *
 * ibus - The Input Bus
 *
 * Copyright(c) 2013 Peng Huang <shawn.p.huang@gmail.com>
 * Copyright(c) 2015-2020 Takao Fujiwara <takao.fujiwara1@gmail.com>
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
 * USA
 */

private const string IBUS_SCHEMAS_GENERAL = "org.freedesktop.ibus.general";
private const string IBUS_SCHEMAS_GENERAL_HOTKEY =
        "org.freedesktop.ibus.general.hotkey";
private const string IBUS_SCHEMAS_PANEL = "org.freedesktop.ibus.panel";
private const string IBUS_SCHEMAS_PANEL_EMOJI =
        "org.freedesktop.ibus.panel.emoji";

bool name_only = false;
/* system() exists as a public API. */
bool is_system = false;
string cache_file = null;
string engine_id = null;

class EngineList {
    public IBus.EngineDesc[] data = {};
}

IBus.Bus? get_bus() {
    var bus = new IBus.Bus();
    if (!bus.is_connected ())
        return null;
    return bus;
}

int list_engine(string[] argv) {
    const OptionEntry[] options = {
        { "name-only", 0, 0, OptionArg.NONE, out name_only,
          N_("List engine name only"), null },
        { null }
    };

    var option = new OptionContext();
    option.add_main_entries(options, Config.GETTEXT_PACKAGE);

    try {
        option.parse(ref argv);
    } catch (OptionError e) {
        stderr.printf("%s\n", e.message);
        return Posix.EXIT_FAILURE;
    }

    var bus = get_bus();
    if (bus == null) {
        stderr.printf(_("Can't connect to IBus.\n"));
        return Posix.EXIT_FAILURE;
    }

    var engines = bus.list_engines();

    if (name_only) {
        foreach (var engine in engines) {
            print("%s\n", engine.get_name());
        }
        return Posix.EXIT_SUCCESS;
    }

    var map = new HashTable<string, EngineList>(GLib.str_hash, GLib.str_equal);

    foreach (var engine in engines) {
        var list = map.get(engine.get_language());
        if (list == null) {
            list = new EngineList();
            map.insert(engine.get_language(), list);
        }
        list.data += engine;
    }

    foreach (var language in map.get_keys()) {
        var list = map.get(language);
        print(_("language: %s\n"), IBus.get_language_name(language));
        foreach (var engine in list.data) {
            print("  %s - %s\n", engine.get_name(), engine.get_longname());
        }
    }

    return Posix.EXIT_SUCCESS;
}

private int exec_setxkbmap(IBus.EngineDesc engine) {
    string layout = engine.get_layout();
    string variant = engine.get_layout_variant();
    string option = engine.get_layout_option();
    string standard_error = null;
    int exit_status = 0;
    string[] args = { "setxkbmap" };

    if (layout != null && layout != "" && layout != "default") {
        args += "-layout";
        args += layout;
    }
    if (variant != null && variant != "" && variant != "default") {
        args += "-variant";
        args += variant;
    }
    if (option != null && option != "" && option != "default") {
        /*TODO: Need to get the session XKB options */
        args += "-option";
        args += "-option";
        args += option;
    }

    if (args.length == 1) {
        return Posix.EXIT_FAILURE;
    }

    try {
        if (!GLib.Process.spawn_sync(null, args, null,
                                     GLib.SpawnFlags.SEARCH_PATH,
                                     null, null,
                                     out standard_error,
                                     out exit_status)) {
            warning("Switch xkb layout to %s failed.",
                    engine.get_layout());
            return Posix.EXIT_FAILURE;
        }
    } catch (GLib.SpawnError e) {
        warning("Execute setxkbmap failed: %s", e.message);
        return Posix.EXIT_FAILURE;
    }

    if (exit_status != 0) {
        warning("Execute setxkbmap failed: %s", standard_error ?? "(null)");
        return Posix.EXIT_FAILURE;
    }

    return Posix.EXIT_SUCCESS;
}

int get_set_engine(string[] argv) {
    var bus = get_bus();
    string engine = null;
    if (argv.length > 1)
        engine = argv[1];

    if (engine == null) {
        var desc = bus.get_global_engine();
        if (desc == null) {
            stderr.printf(_("No engine is set.\n"));
            return Posix.EXIT_FAILURE;
        }
        print("%s\n", desc.get_name());
        return Posix.EXIT_SUCCESS;
    }

    if(!bus.set_global_engine(engine)) {
        stderr.printf(_("Set global engine failed.\n"));
        return Posix.EXIT_FAILURE;
    }
    var desc = bus.get_global_engine();
    if (desc == null) {
        stderr.printf(_("Get global engine failed.\n"));
        return Posix.EXIT_FAILURE;
    }

    var settings = new GLib.Settings(IBUS_SCHEMAS_GENERAL);
    if (!settings.get_boolean("use-system-keyboard-layout"))
        return exec_setxkbmap(desc);

    return Posix.EXIT_SUCCESS;
}

int message_watch(string[] argv) {
    return Posix.EXIT_SUCCESS;
}

int restart_daemon(string[] argv) {
    var bus = get_bus();
    if (bus == null) {
        stderr.printf(_("Can't connect to IBus.\n"));
        return Posix.EXIT_FAILURE;
    }
    bus.exit(true);
    return Posix.EXIT_SUCCESS;
}

int exit_daemon(string[] argv) {
    var bus = get_bus();
    if (bus == null) {
        stderr.printf(_("Can't connect to IBus.\n"));
        return Posix.EXIT_FAILURE;
    }
    bus.exit(false);
    return Posix.EXIT_SUCCESS;
}

int print_version(string[] argv) {
    print("IBus %s\n", Config.PACKAGE_VERSION);
    return Posix.EXIT_SUCCESS;
}

int read_cache (string[] argv) {
    const OptionEntry[] options = {
        { "system", 0, 0, OptionArg.NONE, out is_system,
          N_("Read the system registry cache."), null },
        { "file", 0, 0, OptionArg.STRING, out cache_file,
          N_("Read the registry cache FILE."), "FILE" },
        { null }
    };

    var option = new OptionContext();
    option.add_main_entries(options, Config.GETTEXT_PACKAGE);

    try {
        option.parse(ref argv);
    } catch (OptionError e) {
        stderr.printf("%s\n", e.message);
        return Posix.EXIT_FAILURE;
    }

    var registry = new IBus.Registry();

    if (cache_file != null) {
        if (!registry.load_cache_file(cache_file)) {
            stderr.printf(_("The registry cache is invalid.\n"));
            return Posix.EXIT_FAILURE;
        }
    } else {
        if (!registry.load_cache(!is_system)) {
            stderr.printf(_("The registry cache is invalid.\n"));
            return Posix.EXIT_FAILURE;
        }
    }

    var output = new GLib.StringBuilder();
    registry.output(output, 1);

    print ("%s\n", output.str);
    return Posix.EXIT_SUCCESS;
}

int write_cache (string[] argv) {
    const OptionEntry[] options = {
        { "system", 0, 0, OptionArg.NONE, out is_system,
          N_("Write the system registry cache."), null },
        { "file", 0, 0, OptionArg.STRING, out cache_file,
          N_("Write the registry cache FILE."),
          "FILE" },
        { null }
    };

    var option = new OptionContext();
    option.add_main_entries(options, Config.GETTEXT_PACKAGE);

    try {
        option.parse(ref argv);
    } catch (OptionError e) {
        stderr.printf("%s\n", e.message);
        return Posix.EXIT_FAILURE;
    }

    var registry = new IBus.Registry();
    registry.load();

    if (cache_file != null) {
        return registry.save_cache_file(cache_file) ?
                Posix.EXIT_SUCCESS : Posix.EXIT_FAILURE;
    }

    return registry.save_cache(!is_system) ?
            Posix.EXIT_SUCCESS : Posix.EXIT_FAILURE;
}

int print_address(string[] argv) {
    string address = IBus.get_address();
    print("%s\n", address != null ? address : "(null)");
    return Posix.EXIT_SUCCESS;
}

private int read_config_options(string[] argv) {
    const OptionEntry[] options = {
        { "engine-id", 0, 0, OptionArg.STRING, out engine_id,
          N_("Use engine schema paths instead of ibus core, " +
             "which can be comma-separated values."), "ENGINE_ID" },
        { null }
    };

    var option = new OptionContext();
    option.add_main_entries(options, Config.GETTEXT_PACKAGE);

    try {
        option.parse(ref argv);
    } catch (OptionError e) {
        stderr.printf("%s\n", e.message);
        return Posix.EXIT_FAILURE;
    }
    return Posix.EXIT_SUCCESS;
}

private GLib.SList<string> get_ibus_schemas() {
    string[] ids = {};
    if (engine_id != null) {
        ids = engine_id.split(",");
    }
    GLib.SList<string> ibus_schemas = new GLib.SList<string>();
    GLib.SettingsSchemaSource schema_source =
            GLib.SettingsSchemaSource.get_default();
    string[] list_schemas = {};
    schema_source.list_schemas(true, out list_schemas, null);
    foreach (string schema in list_schemas) {
        if (ids.length != 0) {
            foreach (unowned string id in ids) {
                if (id == schema ||
                    schema.has_prefix("org.freedesktop.ibus.engine." + id)) {
                    ibus_schemas.prepend(schema);
                    break;
                }
            }
        } else if (schema.has_prefix("org.freedesktop.ibus") &&
            !schema.has_prefix("org.freedesktop.ibus.engine")) {
            ibus_schemas.prepend(schema);
        }
    }
    if (ibus_schemas.length() == 0) {
        printerr("Not found schemas of \"org.freedesktop.ibus\"\n");
        return ibus_schemas;
    }
    ibus_schemas.sort(GLib.strcmp);

    return ibus_schemas;
}

int read_config(string[] argv) {
    if (read_config_options(argv) == Posix.EXIT_FAILURE)
        return Posix.EXIT_FAILURE;

    GLib.SList<string> ibus_schemas = get_ibus_schemas();
    if (ibus_schemas.length() == 0)
        return Posix.EXIT_FAILURE;

    GLib.SettingsSchemaSource schema_source =
            GLib.SettingsSchemaSource.get_default();
    var output = new GLib.StringBuilder();
    foreach (string schema in ibus_schemas) {
        GLib.SettingsSchema settings_schema = schema_source.lookup(schema,
                                                                   false);
        GLib.Settings settings = new GLib.Settings(schema);

        output.append_printf("SCHEMA: %s\n", schema);

        foreach (string key in settings_schema.list_keys()) {
            GLib.Variant variant = settings.get_value(key);
            output.append_printf("  %s: %s\n", key, variant.print(true));
        }
    }
    print("%s", output.str);

    return Posix.EXIT_SUCCESS;
}

int reset_config(string[] argv) {
    if (read_config_options(argv) == Posix.EXIT_FAILURE)
        return Posix.EXIT_FAILURE;

    GLib.SList<string> ibus_schemas = get_ibus_schemas();
    if (ibus_schemas.length() == 0)
        return Posix.EXIT_FAILURE;

    print("%s\n", _("Resetting…"));

    GLib.SettingsSchemaSource schema_source =
            GLib.SettingsSchemaSource.get_default();
    foreach (string schema in ibus_schemas) {
        GLib.SettingsSchema settings_schema = schema_source.lookup(schema,
                                                                   false);
        GLib.Settings settings = new GLib.Settings(schema);

        print("SCHEMA: %s\n", schema);

        foreach (string key in settings_schema.list_keys()) {
            print("  %s\n", key);
            settings.reset(key);
        }
    }

    GLib.Settings.sync();
    print("%s\n", _("Done"));

    return Posix.EXIT_SUCCESS;
}

#if EMOJI_DICT
int emoji_dialog(string[] argv) {
    string cmd = Config.LIBEXECDIR + "/ibus-ui-emojier";

    var file = File.new_for_path(cmd);
    if (!file.query_exists())
        cmd = "../ui/gtk3/ibus-ui-emojier";

    argv[0] = cmd;

    string[] env = Environ.get();

    try {
        // Non-blocking
        Process.spawn_async(null, argv, env,
                            SpawnFlags.SEARCH_PATH,
                            null, null);
    } catch (SpawnError e) {
        stderr.printf("%s\n", e.message);
        return Posix.EXIT_FAILURE;
    }

    return Posix.EXIT_SUCCESS;
}
#endif

int print_help(string[] argv) {
    print_usage(stdout);
    return Posix.EXIT_SUCCESS;
}

delegate int EntryFunc(string[] argv);

struct CommandEntry {
    unowned string name;
    unowned string description;
    unowned EntryFunc entry;
}

const CommandEntry commands[]  = {
    { "engine", N_("Set or get engine"), get_set_engine },
    { "exit", N_("Exit ibus-daemon"), exit_daemon },
    { "list-engine", N_("Show available engines"), list_engine },
    { "watch", N_("(Not implemented)"), message_watch },
    { "restart", N_("Restart ibus-daemon"), restart_daemon },
    { "version", N_("Show version"), print_version },
    { "read-cache", N_("Show the content of registry cache"), read_cache },
    { "write-cache", N_("Create registry cache"), write_cache },
    { "address", N_("Print the D-Bus address of ibus-daemon"), print_address },
    { "read-config", N_("Show the configuration values"), read_config },
    { "reset-config", N_("Reset the configuration values"), reset_config },
#if EMOJI_DICT
    { "emoji", N_("Save emoji on dialog to clipboard"), emoji_dialog },
#endif
    { "help", N_("Show this information"), print_help }
};

static string program_name;

void print_usage(FileStream stream) {
    stream.printf(_("Usage: %s COMMAND [OPTION...]\n\n"), program_name);
    stream.printf(_("Commands:\n"));
    for (int i = 0; i < commands.length; i++) {
        stream.printf("  %-12s    %s\n",
                      commands[i].name,
                      GLib.dgettext(null, commands[i].description));
    }
}

public int main(string[] argv) {
    GLib.Intl.setlocale(GLib.LocaleCategory.ALL, "");
    GLib.Intl.bindtextdomain(Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
    GLib.Intl.bind_textdomain_codeset(Config.GETTEXT_PACKAGE, "UTF-8");
    GLib.Intl.textdomain(Config.GETTEXT_PACKAGE);

    IBus.init();

    program_name = Path.get_basename(argv[0]);
    if (argv.length < 2) {
        print_usage(stderr);
        return Posix.EXIT_FAILURE;
    }

    string[] new_argv = argv[1:argv.length];
    new_argv[0] = "%s %s".printf(program_name, new_argv[0]);
    for (int i = 0; i < commands.length; i++) {
        if (commands[i].name == argv[1])
            return commands[i].entry(new_argv);
    }

    stderr.printf(_("%s is unknown command!\n"), argv[1]);
    print_usage(stderr);
    return Posix.EXIT_FAILURE;
}
