// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported Indicator */

const {Gio, GLib, GnomeBluetooth, GObject} = imports.gi;

const {QuickToggle, SystemIndicator} = imports.ui.quickSettings;

const {loadInterfaceXML} = imports.misc.fileUtils;

const {AdapterState} = GnomeBluetooth;

const BUS_NAME = 'org.gnome.SettingsDaemon.Rfkill';
const OBJECT_PATH = '/org/gnome/SettingsDaemon/Rfkill';

const RfkillManagerInterface = loadInterfaceXML('org.gnome.SettingsDaemon.Rfkill');
const rfkillManagerInfo = Gio.DBusInterfaceInfo.new_for_xml(RfkillManagerInterface);

const HAD_BLUETOOTH_DEVICES_SETUP = 'had-bluetooth-devices-setup';

const BtClient = GObject.registerClass({
    Properties: {
        'available': GObject.ParamSpec.boolean('available', '', '',
            GObject.ParamFlags.READABLE,
            false),
        'active': GObject.ParamSpec.boolean('active', '', '',
            GObject.ParamFlags.READABLE,
            false),
        'adapter-state': GObject.ParamSpec.enum('adapter-state', '', '',
            GObject.ParamFlags.READABLE,
            AdapterState, AdapterState.ABSENT),
    },
    Signals: {
        'devices-changed': {},
    },
}, class BtClient extends GObject.Object {
    _init() {
        super._init();

        this._hadSetupDevices = global.settings.get_boolean(HAD_BLUETOOTH_DEVICES_SETUP);

        this._client = new GnomeBluetooth.Client();
        this._client.connect('notify::default-adapter-powered', () => {
            this.notify('active');
            this.notify('available');
        });
        this._client.connect('notify::default-adapter-state',
            () => this.notify('adapter-state'));
        this._client.connect('notify::default-adapter', () => {
            const newAdapter = this._client.default_adapter ?? null;

            this._adapter = newAdapter;
            this._deviceNotifyConnected.clear();
            this.emit('devices-changed');

            this.notify('active');
            this.notify('available');
        });

        this._proxy = new Gio.DBusProxy({
            g_connection: Gio.DBus.session,
            g_name: BUS_NAME,
            g_object_path: OBJECT_PATH,
            g_interface_name: rfkillManagerInfo.name,
            g_interface_info: rfkillManagerInfo,
        });
        this._proxy.connect('g-properties-changed', (p, properties) => {
            if ('BluetoothHardwareAirplaneMode' in properties.unpack())
                this.notify('available');
        });
        this._proxy.init_async(GLib.PRIORITY_DEFAULT, null)
            .catch(e => console.error(e.message));

        this._adapter = null;

        this._deviceNotifyConnected = new Set();

        const deviceStore = this._client.get_devices();
        for (let i = 0; i < deviceStore.get_n_items(); i++)
            this._connectDeviceNotify(deviceStore.get_item(i));

        this._client.connect('device-removed', (c, path) => {
            this._syncHadSetupDevices();
            this._deviceNotifyConnected.delete(path);
            this.emit('devices-changed');
        });
        this._client.connect('device-added', (c, device) => {
            this._syncHadSetupDevices();
            this._connectDeviceNotify(device);
            this.emit('devices-changed');
        });
    }

    get available() {
        // If there were set up devices, assume there is an adapter
        // that can be powered on as long as we're not hard blocked
        return this._hadSetupDevices
            ? !this._proxy.BluetoothHardwareAirplaneMode
            : this.active;
    }

    get active() {
        return this._client.default_adapter_powered;
    }

    get adapter_state() {
        return this._client.default_adapter_state;
    }

    toggleActive() {
        this._proxy.BluetoothAirplaneMode = this.active;
        if (!this._client.default_adapter_powered)
            this._client.default_adapter_powered = true;
    }

    *getDevices() {
        const deviceStore = this._client.get_devices();

        for (let i = 0; i < deviceStore.get_n_items(); i++) {
            const device = deviceStore.get_item(i);

            if (device.paired || device.trusted)
                yield device;
        }
    }

    _queueDevicesChanged() {
        if (this._devicesChangedId)
            return;
        this._devicesChangedId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
            this._syncHadSetupDevices();
            delete this._devicesChangedId;
            this.emit('devices-changed');
            return GLib.SOURCE_REMOVE;
        });
    }

    _syncHadSetupDevices() {
        const {defaultAdapter} = this._client;
        if (!defaultAdapter || !this._adapter)
            return; // ignore changes while powering up/down

        const [firstDevice] = this.getDevices();
        const hadSetupDevices = !!firstDevice;

        if (this._hadSetupDevices === hadSetupDevices)
            return;

        this._hadSetupDevices = hadSetupDevices;
        global.settings.set_boolean(
            HAD_BLUETOOTH_DEVICES_SETUP, this._hadSetupDevices);
    }

    _connectDeviceNotify(device) {
        const path = device.get_object_path();

        if (this._deviceNotifyConnected.has(path))
            return;

        device.connect('notify::alias', () => this._queueDevicesChanged());
        device.connect('notify::paired', () => this._queueDevicesChanged());
        device.connect('notify::trusted', () => this._queueDevicesChanged());
        device.connect('notify::connected', () => this._queueDevicesChanged());

        this._deviceNotifyConnected.add(path);
    }
});

const BluetoothToggle = GObject.registerClass(
class BluetoothToggle extends QuickToggle {
    _init(client) {
        super._init({label: _('Bluetooth')});

        this._client = client;

        this._client.bind_property('available',
            this, 'visible',
            GObject.BindingFlags.SYNC_CREATE);
        this._client.bind_property('active',
            this, 'checked',
            GObject.BindingFlags.SYNC_CREATE);
        this._client.bind_property_full('adapter-state',
            this, 'icon-name',
            GObject.BindingFlags.SYNC_CREATE,
            (bind, source) => [true, this._getIconNameFromState(source)],
            null);

        this.connect('clicked', () => this._client.toggleActive());
    }

    _getIconNameFromState(state) {
        switch (state) {
        case AdapterState.ON:
            return 'bluetooth-active-symbolic';
        case AdapterState.OFF:
        case AdapterState.ABSENT:
            return 'bluetooth-disabled-symbolic';
        case AdapterState.TURNING_ON:
        case AdapterState.TURNING_OFF:
            return 'bluetooth-acquiring-symbolic';
        default:
            console.warn(`Unexpected state ${
                GObject.enum_to_string(AdapterState, state)}`);
            return '';
        }
    }
});

var Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    _init() {
        super._init();

        this._client = new BtClient();
        this._client.connect('devices-changed', () => this._sync());

        this._indicator = this._addIndicator();
        this._indicator.icon_name = 'bluetooth-active-symbolic';

        this.quickSettingsItems.push(new BluetoothToggle(this._client));

        this._sync();
    }

    _sync() {
        const devices = [...this._client.getDevices()];
        const connectedDevices = devices.filter(dev => dev.connected);
        const nConnectedDevices = connectedDevices.length;

        this._indicator.visible = nConnectedDevices > 0;
    }
});
