[DBus (name = "org.bluez.Agent1")]
public class Agent: Object
{
    // These actions and menus are exposed on their relevant paths by service.vala
    public GLib.Menu menu;
    public GLib.SimpleActionGroup actions;
    private GLib.SimpleAction pin_action;

    // These paths are set by service.vala
    public string menu_path;
    public string actions_path;

    private MainLoop loop;
    private Bluetooth bluetooth;
    private Notify.Notification? notification;
    private string passkey;

    public Agent (Bluetooth bluez)
    {
        // Menu
        menu = new GLib.Menu ();
        GLib.MenuItem item = new GLib.MenuItem ("", "notifications.pin");
        item.set_attribute_value ("x-canonical-type", new Variant.string ("com.canonical.snapdecision.textfield"));
        item.set_attribute_value ("x-echo-mode-password", new Variant.boolean (false));
        menu.append_item (item);

        // Actions
        actions = new GLib.SimpleActionGroup ();
        pin_action = new GLib.SimpleAction.stateful ("pin", null, new Variant.string (""));
        pin_action.change_state.connect ((value) => {
            this.passkey = value.get_string ();
        });
        actions.add_action (pin_action);

        loop = new MainLoop (null, false);
        bluetooth = bluez;
        Notify.init ("ayatana-indicator-bluetooth");
    }

    /* TODO: Add a better way to differentiate between rejected and cancelled errors, maybe with an enum */
    private bool sendNotification (string device_name, string body, bool need_input, bool have_actions)
    {
        bool accepted = !have_actions;

        string header = (_("Pair with %s?").printf (device_name));
        notification = new Notify.Notification (header, body, "bluetooth-active");
        notification.closed.connect (() => {
            accepted = false;
            notification = null;

            if (loop.is_running ()) {
                loop.quit ();
            }
        });

        bool is_lomiri = AyatanaCommon.utils_is_lomiri ();

        if (is_lomiri) {
            if (have_actions) {
                notification.set_hint ("x-lomiri-snap-decisions", true);
                notification.set_hint ("x-lomiri-private-affirmative-tint", "true");
            }

            if (need_input) {
                VariantBuilder actions_builder = new VariantBuilder (new VariantType ("a{sv}"));
                actions_builder.add ("{sv}", "notifications", new Variant.string (actions_path));

                VariantBuilder builder = new VariantBuilder (new VariantType ("a{sv}"));
                builder.add ("{sv}", "busName", new Variant.string ("org.ayatana.indicator.bluetooth"));
                builder.add ("{sv}", "menuPath", new Variant.string (menu_path));
                builder.add ("{sv}", "actions", actions_builder.end ());

                notification.set_hint ("x-lomiri-private-menu-model", builder.end ());
            }
        }

        if (have_actions) {
            notification.add_action("yes_id", (_("Yes")), (notif, action) => {
                loop.quit ();
                notification = null;
                accepted = true;
            });
            notification.add_action("no_id", (_("No")), (notif, action) => {
                loop.quit ();
                notification = null;
                accepted = false;
            });
        }

        if (!have_actions && !need_input) {
            // Display-only notification. Make sure we don't time out.
            notification.set_hint ("urgency", 2);
        }

        try {
            notification.show ();
        }
        catch (Error e) {
            warning ("Panic: Failed showing notification: %s", e.message);
        }

        if (have_actions) {
            loop.run ();
        }

        return accepted;
    }

    public void AuthorizeService (GLib.ObjectPath object, string uuid) throws RejectedError, GLib.DBusError, GLib.IOError
    {
        bool authorized = false;
        bool trusted = false;

        string header = (_("Allow %s to connect?").printf (bluetooth.get_device_name (object)));
        string body = (_("Allow the Bluetooth device to access a Bluetooth service?"));

        notification = new Notify.Notification (header, body, "bluetooth-active");
        notification.closed.connect (() => {
            authorized = false;
            notification = null;

            if (loop.is_running ()) {
                loop.quit ();
            }
        });

        if (AyatanaCommon.utils_is_lomiri ()) {
            notification.set_hint ("x-lomiri-snap-decisions", true);
        }

        notification.add_action ("trust_and_authorize", (_("Trust and authorize")), (notif, action) => {
            loop.quit ();
            notification = null;

            trusted = true;
            authorized = true;
        });

        notification.add_action ("authorize", (_("Authorize")), (notif, action) => {
            loop.quit ();
            notification = null;

            authorized = true;
        });

        notification.add_action ("reject", (_("Do not authorize")), (notif, action) => {
            loop.quit ();
            notification = null;
        });

        try {
            notification.show ();
        }
        catch (Error e) {
            warning ("Panic: Failed showing notification: %s", e.message);
        }

        loop.run ();

        // Once the loop quits, we can see if we want to set the 'Trusted' property in BlueZ
        if (trusted) {
            Device device = bluetooth.get_device (object);
            bluetooth.set_device_trusted (device.id, trusted);
        }

        if (!authorized) {
            throw new RejectedError.ERROR ("Rejected by user");
        }
    }

    public void RequestConfirmation (GLib.ObjectPath object, uint32 passkey) throws RejectedError, GLib.DBusError, GLib.IOError
    {
        string body = (_("Are you sure you want to pair with PIN %06u?").printf (passkey));
        bool confirmed = sendNotification (bluetooth.get_device_name (object), body, false, true);

        if (!confirmed) {
            throw new RejectedError.ERROR ("Rejected by user");
        }
    }

    public void RequestAuthorization (GLib.ObjectPath object) throws RejectedError, GLib.DBusError, GLib.IOError
    {
        bool authorized = sendNotification (bluetooth.get_device_name (object), (_("Are you sure you want to pair with this device?")), false, true);

        if (!authorized) {
            throw new RejectedError.ERROR ("Rejected by user");
        }
    }

    public string RequestPinCode (GLib.ObjectPath object) throws RejectedError, GLib.DBusError, GLib.IOError
    {
        bool accepted = sendNotification (bluetooth.get_device_name (object), (_("Enter PIN for this device")), true, true);

        if (!accepted) {
            throw new RejectedError.ERROR ("Rejected by user");
        }

        return passkey;
    }

    public void DisplayPinCode (GLib.ObjectPath object, string pincode) throws GLib.DBusError, GLib.IOError
    {
        string body = (_("Enter the PIN code %s on the other device").printf (pincode));
        sendNotification (bluetooth.get_device_name (object), body, false, false);
    }

    public uint32 RequestPasskey (GLib.ObjectPath object) throws RejectedError, GLib.DBusError, GLib.IOError
    {
        bool accepted = sendNotification (bluetooth.get_device_name (object), (_("Enter PIN for this device")), true, true);

        if (!accepted) {
            throw new RejectedError.ERROR ("Rejected by user");
        }

        return passkey.to_int ();
    }

    public void DisplayPasskey (GLib.ObjectPath object, uint32 passkey, uint16 entered) throws GLib.DBusError, GLib.IOError
    {
        string body = (_("Enter the PIN %06u on the other device").printf (passkey));
        sendNotification (bluetooth.get_device_name (object), body, false, false);
    }

    public void Cancel () throws GLib.DBusError, GLib.IOError
    {
        if (loop.is_running ()) {
            loop.quit ();
        }

        if (notification != null) {
            notification.close ();
            notification = null;
        }
    }

    public void Release () throws GLib.DBusError, GLib.IOError
    {
    }
}

[DBus (name = "org.bluez.Error.Cancelled")]
public errordomain CancelledError {
    ERROR
}

[DBus (name = "org.bluez.Error.Rejected")]
public errordomain RejectedError {
    ERROR
}
