/*
 * This file is part of budgie-desktop
 *
 * Copyright © 2017-2019 Budgie Desktop Developers
 *
 * 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 2 of the License, or
 * (at your option) any later version.
 */

namespace Budgie {

[Compact]
public class CommandInfo
{
    public string title;
    public string description;
    public string command;
}

public class AutostartItem : GLib.Object
{
    public string id;
    public string filename;
    public string title;
    public string description;
    public string command;
    public string executable;
    public GLib.Icon? icon = null;

    construct {
        id = "";
        filename = "";
        title = "";
        description = "";
        command = "";
        executable = "";
    }

    public AutostartItem.from_app_info(GLib.DesktopAppInfo info)
    {
        this.id = info.get_id() ?? "";
        this.filename = info.get_filename() ?? "";
        this.title = info.get_display_name() ?? "";
        this.description = info.get_description() ?? "";
        this.executable = info.get_executable() ?? "";
        this.icon = info.get_icon();
    }

    public AutostartItem.from_command_info(CommandInfo info)
    {
        this.title = info.title;
        this.description = info.description;
        this.command = info.command;
    }

    public string? make_autostart() {
        DirUtils.create_with_parents(GLib.Path.build_path("/", AutostartPage.AUTOSTART_PATH), 00755);
        if (filename != "") {
            string destination_path = GLib.Path.build_path("/", AutostartPage.AUTOSTART_PATH, GLib.Filename.display_basename(filename));
            GLib.File destination = GLib.File.new_for_path(destination_path);
            GLib.File file = GLib.File.new_for_path(filename);
            try {
                file.copy(destination, GLib.FileCopyFlags.NONE);
                this.filename = destination_path;
            } catch (GLib.Error e) {
                warning(e.message);
                return null;
            }
        } else if (command != "") {
            string destination_path = GLib.Path.build_path("/", AutostartPage.AUTOSTART_PATH, @"$title.desktop");
            GLib.File file = GLib.File.new_for_path(destination_path);
            try {
                GLib.FileOutputStream file_stream = file.create(GLib.FileCreateFlags.NONE);
                if (file.query_exists()) {
                    GLib.DataOutputStream data_stream = new GLib.DataOutputStream(file_stream);
                    data_stream.put_string(@"[Desktop Entry]\nType=Application\nName=$title\nDescription=$description\nExec=$command\n");
                    this.filename = file.get_path();
                    GLib.DesktopAppInfo? info = new GLib.DesktopAppInfo.from_filename(file.get_path());
                    if (info == null) {
                        this.delete();
                        return null;
                    }
                    this.id = info.get_id();
                } else {
                    return null;
                }
            } catch (GLib.Error e) {
                warning(e.message);
                return null;
            }
        }

        return this.filename;
    }

    public void delete() {
        GLib.File file = GLib.File.new_for_path(filename);
        try {
            file.delete();
        } catch (GLib.Error e) {
            warning(e.message);
        }
        AutostartPage.autostart_files.remove(this.id);
    }
}

public class AutostartItemWidget : Gtk.ListBoxRow
{
    public AutostartItem autostart_item;
    public bool running;

    public AutostartItemWidget(AutostartItem item, bool show_delete = true, bool running = false)
    {
        Object(can_focus: false,
               focus_on_click: false);

        this.autostart_item = item;
        this.running = running;

        Gtk.Box main_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 5);
        this.add(main_box);
        main_box.margin = 6;

        Gtk.Image icon;
        if (item.icon != null) {
            icon = new Gtk.Image.from_gicon(item.icon, Gtk.IconSize.INVALID);
        } else {
            icon = new Gtk.Image.from_icon_name("image-missing", Gtk.IconSize.INVALID);
        }
        icon.pixel_size = 48;
        main_box.pack_start(icon, false, false, 0);
        icon.valign = Gtk.Align.CENTER;

        Gtk.Box text_box = new Gtk.Box(Gtk.Orientation.VERTICAL, 5);
        main_box.pack_start(text_box, false, false, 0);
        text_box.valign = Gtk.Align.CENTER;

        string title = item.title;
        title = (title != "") ? GLib.Markup.escape_text(title) : "<i>" + _("Untitled") + "</i>";
        Gtk.Label title_label = new Gtk.Label(@"<big>$title</big>");
        text_box.add(title_label);
        title_label.set_use_markup(true);
        title_label.halign = Gtk.Align.START;

        string description = item.description;
        description = (description != "") ? GLib.Markup.escape_text(description) : "<i>" + _("No description") + "</i>";
        Gtk.Label desc_label = new Gtk.Label(description);
        text_box.add(desc_label);
        desc_label.set_max_width_chars(35);
        desc_label.set_tooltip_text(description);
        desc_label.set_ellipsize(Pango.EllipsizeMode.END);
        desc_label.set_use_markup(true);
        desc_label.get_style_context().add_class("dim-label");
        desc_label.halign = Gtk.Align.START;

        if (show_delete) {
            Gtk.Button remove_button = new Gtk.Button.from_icon_name("list-remove-symbolic", Gtk.IconSize.MENU);
            main_box.pack_end(remove_button, false, false, 0);
            remove_button.get_style_context().add_class(Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION);
            remove_button.get_style_context().add_class("round-button");
            remove_button.valign = Gtk.Align.CENTER;
            remove_button.clicked.connect(() => {
                autostart_item.delete();
                this.destroy();
            });
        }

        if (running) {
            Gtk.Label running_label = new Gtk.Label(_("running"));
            main_box.pack_end(running_label, false, false, 0);
            running_label.get_style_context().add_class("dim-label");
            running_label.valign = Gtk.Align.CENTER;
        }

        this.show_all();
    }
}

public class AppChooser : Gtk.Dialog
{
    private Gtk.ListBox app_listbox;
    private Gtk.Widget button_ok;
    private Gtk.SearchEntry search_entry;
    private GLib.GenericSet<string> running_processes;

    private AutostartItem? selected_item = null;

    public AppChooser(Gtk.Window parent)
    {
        Object(use_header_bar: 1,
               modal: true,
               title: _("Applications"),
               transient_for: parent);

        running_processes = new GLib.GenericSet<string>(str_hash, str_equal);

        get_running_processes();

        Gtk.Box content_area = get_content_area() as Gtk.Box;

        this.add_button(_("Cancel"), Gtk.ResponseType.CANCEL);
        button_ok = this.add_button(_("Add"), Gtk.ResponseType.ACCEPT);
        button_ok.set_sensitive(false);
        button_ok.get_style_context().add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION);

        Gtk.HeaderBar header_bar = this.get_header_bar() as Gtk.HeaderBar;
        Gtk.ToggleButton search_button = new Gtk.ToggleButton();
        search_button.image = new Gtk.Image.from_icon_name("system-search-symbolic", Gtk.IconSize.MENU);
        header_bar.pack_end(search_button);

        Gtk.SearchBar search_bar = new Gtk.SearchBar();
        content_area.pack_start(search_bar, false, false, 0);
        search_bar.show_all();
        search_button.bind_property("active", search_bar, "search-mode-enabled");
        search_bar.bind_property("search-mode-enabled", search_button, "active");

        this.key_press_event.connect(search_bar.handle_event);

        search_entry = new Gtk.SearchEntry();
        search_bar.add(search_entry);

        search_entry.changed.connect(() => {
            app_listbox.invalidate_filter();
            app_listbox.invalidate_sort();
        });

        Gtk.ScrolledWindow scroll = new Gtk.ScrolledWindow(null, null);
        content_area.pack_start(scroll, true, true, 0);
        scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);

        app_listbox = new Gtk.ListBox();
        scroll.add(app_listbox);
        app_listbox.set_filter_func(search_filter);
        app_listbox.set_sort_func(sort_function);
        app_listbox.set_activate_on_single_click(false);
        app_listbox.row_selected.connect(row_selected);
        app_listbox.row_activated.connect(row_activated);

        content_area.show_all();

        set_default_size(400, 450);
        set_app_list(GLib.AppInfo.get_all());
    }

    private bool search_filter(Gtk.ListBoxRow row)
    {
        AutostartItem item = (row as AutostartItemWidget).autostart_item;
        if (AutostartPage.autostart_files.contains(item.id)) {
            return false;
        }
        string search_text = search_entry.get_text().down();
        string title_text = item.title.down();
        string desc_text = item.description.down();
        return (search_text in title_text || search_text in desc_text);
    }

    /* Credit to gnome-tweak-tool for inspiration */
    private int sort_function(Gtk.ListBoxRow row1, Gtk.ListBoxRow row2) {
        AutostartItemWidget item1 = row1 as AutostartItemWidget;
        AutostartItemWidget item2 = row2 as AutostartItemWidget;

        if (item1.running && !item2.running) {
            return -1;
        } else if (!item1.running && item2.running) {
            return 1;
        }

        return GLib.strcmp(item1.autostart_item.title, item2.autostart_item.title);
    }

    public new AutostartItem? run()
    {
        Gtk.ResponseType resp = (Gtk.ResponseType)base.run();
        switch (resp) {
            case Gtk.ResponseType.ACCEPT:
                return this.selected_item;
            case Gtk.ResponseType.CANCEL:
            default:
                return null;
        }
    }

    private void row_selected(Gtk.ListBoxRow? row)
    {
        if (row == null) {
            this.selected_item = null;
            this.button_ok.set_sensitive(false);
            return;
        }

        this.button_ok.set_sensitive(true);

        this.selected_item = (row as AutostartItemWidget).autostart_item;
    }

    private void row_activated(Gtk.ListBoxRow? row)
    {
        this.row_selected(row);

        if (this.selected_item != null) {
            this.response(Gtk.ResponseType.ACCEPT);
        }
    }

    private void set_app_list(GLib.List<GLib.AppInfo> app_list)
    {
        foreach (var child in app_listbox.get_children()) {
            child.destroy();
        }

        foreach (var app in app_list) {
            if (app.should_show()) {
                GLib.DesktopAppInfo? info = new GLib.DesktopAppInfo(app.get_id());
                if (info != null) {
                    AutostartItem item = new AutostartItem.from_app_info(info);
                    bool running = running_processes.contains(item.executable);
                    app_listbox.add(new AutostartItemWidget(item, false, running));
                }
            }
        }
        this.show_all();
    }

    private void get_running_processes()
    {
        string ls_stdout;
        string ls_stderr;
        int ls_status;

        try {
            string username = GLib.Environment.get_user_name();
            // Credit to gnome-tweak-tool for inspiration
            GLib.Process.spawn_command_line_sync(@"ps -e -w -w -U $username -o cmd",
                out ls_stdout,
                out ls_stderr,
                out ls_status);

            if (ls_status != 0) {
                return;
            }

            string[] commands = ls_stdout.split("\n");
            foreach (string line in commands) {
                string command = line.split(" ")[0];
                if (command != null && command != "") {
                    if (!command.has_prefix("[")) {
                        running_processes.add(command);
                    }
                }
            }
        } catch (GLib.SpawnError e) {
            warning(e.message);
        }
    }
}

public class CommandDialog : Gtk.Dialog
{
    Gtk.Widget button_ok;
    Gtk.Label exists_label;
    Gtk.Entry title_entry;
    Gtk.Entry desc_entry;
    Gtk.Entry command_entry;

    public CommandDialog(Gtk.Window parent)
    {
        Object(use_header_bar: 1,
               modal: true,
               title: _("Command"),
               transient_for: parent);

        Gtk.Box content_area = get_content_area() as Gtk.Box;

        this.add_button(_("Cancel"), Gtk.ResponseType.CANCEL);
        button_ok = this.add_button(_("Add"), Gtk.ResponseType.ACCEPT);
        button_ok.set_sensitive(false);
        button_ok.get_style_context().add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION);

        Gtk.Grid grid = new Gtk.Grid();
        grid.set_column_spacing(20);
        grid.set_row_spacing(10);
        grid.halign = Gtk.Align.CENTER;
        grid.valign = Gtk.Align.CENTER;

        exists_label = new Gtk.Label(_("An entry with that title already exists"));
        grid.attach(exists_label, 0, 0, 2, 1);

        Gtk.Label label = new Gtk.Label(_("Title"));
        label.halign = Gtk.Align.START;
        grid.attach(label, 0, 1, 1, 1);
        title_entry = new Gtk.Entry();
        title_entry.set_placeholder_text(_("Required"));
        grid.attach(title_entry, 1, 1, 1, 1);

        label = new Gtk.Label(_("Description"));
        label.halign = Gtk.Align.START;
        grid.attach(label, 0, 2, 1, 1);
        desc_entry = new Gtk.Entry();
        desc_entry.set_placeholder_text(_("Optional"));
        grid.attach(desc_entry, 1, 2, 1, 1);

        label = new Gtk.Label(_("Command"));
        label.halign = Gtk.Align.START;
        grid.attach(label, 0, 3, 1, 1);
        command_entry = new Gtk.Entry();
        command_entry.set_placeholder_text(_("Required"));
        grid.attach(command_entry, 1, 3, 1, 1);

        title_entry.changed.connect(check_inputs);
        command_entry.changed.connect(check_inputs);

        content_area.pack_start(grid, true, true, 0);
        content_area.show_all();

        exists_label.hide();

        set_default_size(300, 200);
    }

    private void check_inputs() {
        string title_text = title_entry.get_text();
        string command_text = command_entry.get_text();
        bool exists = false;
        foreach (string title in AutostartPage.autostart_files.get_values()) {
            if (title.down() == title_text.down()) {
                exists = true;
                break;
            }
        }
        exists_label.set_visible(exists);
        button_ok.set_sensitive(title_text != "" && command_text != "" && !exists);
    }

    public new CommandInfo? run()
    {
        Gtk.ResponseType resp = (Gtk.ResponseType)base.run();
        switch (resp) {
            case Gtk.ResponseType.ACCEPT:
                CommandInfo info = new CommandInfo();
                info.title = title_entry.get_text();
                info.description = desc_entry.get_text();
                info.command = command_entry.get_text();
                return info;
            case Gtk.ResponseType.CANCEL:
            default:
                return null;
        }
    }
}

/**
 * AutostartPage allows users to control autostart apps
 */
public class AutostartPage : Budgie.SettingsPage {
    private Gtk.ListBox listbox_autostart;
    public static GLib.HashTable<string, string> autostart_files;
    public static string AUTOSTART_PATH;

    public AutostartPage()
    {
        Object(group: SETTINGS_GROUP_SESSION,
               content_id: "autostart",
               title: _("Autostart"),
               display_weight: 0,
               icon_name: "preferences-other",
               halign: Gtk.Align.FILL);

        AUTOSTART_PATH = GLib.Path.build_path("/", GLib.Environment.get_user_config_dir(), "autostart");

        autostart_files = new GLib.HashTable<string, string>(str_hash, str_equal);

        Gtk.Frame frame = new Gtk.Frame(null);
        this.pack_start(frame, true, true, 0);

        Gtk.Box frame_box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
        frame.add(frame_box);

        Gtk.Overlay overlay = new Gtk.Overlay();
        frame_box.pack_start(overlay, false, false, 0);

        Gtk.Label add_label = new Gtk.Label(_("Autostart apps"));
        overlay.add(add_label);
        add_label.margin = 15;
        add_label.get_style_context().add_class("dim-label");

        Gtk.MenuButton add_button = new Gtk.MenuButton();
        add_button.image = new Gtk.Image.from_icon_name("list-add-symbolic", Gtk.IconSize.MENU);
        add_button.popup = create_menu();
        overlay.add_overlay(add_button);
        add_button.margin_end = 10;
        add_button.get_style_context().add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION);
        add_button.get_child().get_style_context().add_class("round-button");
        add_button.halign = Gtk.Align.END;
        add_button.valign = Gtk.Align.CENTER;

        Gtk.Separator separator = new Gtk.Separator(Gtk.Orientation.HORIZONTAL);
        frame_box.pack_start(separator, false, false, 0);

        listbox_autostart = new Gtk.ListBox();
        frame_box.pack_start(listbox_autostart, true, true, 0);
        listbox_autostart.set_selection_mode(Gtk.SelectionMode.NONE);

        /* No sense in iterating it if it doesn't exist .. */
        if (!FileUtils.test(AUTOSTART_PATH, FileTest.EXISTS|FileTest.IS_DIR)) {
            return;
        }

        list_directory.begin(AUTOSTART_PATH, (obj, res) => {
            string[] files = list_directory.end(res);
            foreach (string file in files) {
                GLib.DesktopAppInfo? info = new GLib.DesktopAppInfo.from_filename(file);
                if (info != null) {
                    AutostartItem item = new AutostartItem.from_app_info(info);
                    add_item(item);
                    autostart_files.set(item.id, item.title);
                }
            }
        });
    }

    private Gtk.Menu create_menu()
    {
        Gtk.Menu menu = new Gtk.Menu();
        Gtk.MenuItem item_new_app = new Gtk.MenuItem.with_label("Add application");
        menu.add(item_new_app);
        Gtk.MenuItem item_new_command = new Gtk.MenuItem.with_label("Add command");
        menu.add(item_new_command);

        item_new_app.activate.connect(on_add_app);
        item_new_command.activate.connect(on_add_command);

        menu.show_all();

        return menu;
    }

    private void add_item(AutostartItem item) {
        listbox_autostart.add(new AutostartItemWidget(item));
    }

    private async string[] list_directory(string directory)
    {
        string[] filelist = {};
        GLib.File dir = GLib.File.new_for_path(directory);

        try {
            var e = yield dir.enumerate_children_async(GLib.FileAttribute.STANDARD_NAME, 0, GLib.Priority.DEFAULT);
            while (true) {
                var files = yield e.next_files_async(10, GLib.Priority.DEFAULT);
                if (files == null) {
                    break;
                }
                foreach (GLib.FileInfo info in files) {
                    if (info.get_name().has_suffix(".desktop")) {
                        string path = GLib.Path.build_path("/", directory, info.get_name());
                        filelist += path;
                    }
                }
            }
        } catch (GLib.Error e) {
            warning("Error: list_directory failed: %s\n", e.message);
        }

        return filelist;
    }

    void on_add_app()
    {
        var dlg = new AppChooser(this.get_toplevel() as Gtk.Window);
        AutostartItem? item = dlg.run();
        dlg.destroy();

        if (item == null) {
            return;
        }

        if (autostart_files.contains(item.id)) {
            return;
        }

        string? autostart_path = item.make_autostart();

        if (autostart_path == null) {
            return;
        }

        autostart_files.set(item.id, item.title);
        add_item(item);
    }

    void on_add_command()
    {
        var dlg = new CommandDialog(this.get_toplevel() as Gtk.Window);
        CommandInfo? command_info = dlg.run();
        dlg.destroy();

        if (command_info == null) {
            return;
        }

        AutostartItem item = new AutostartItem.from_command_info(command_info);
        string? autostart_path = item.make_autostart();

        if (autostart_path == null) {
            return;
        }

        autostart_files.set(item.id, item.title);
        add_item(item);
    }
}

}
