/* systemMenu.js
 *
 * 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.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

/* exported SystemMenuButton */

const Gettext = imports.gettext;

const { Clutter, Gio, GLib, GObject, Gvc, Shell, St } = imports.gi;

const ExtensionUtils = imports.misc.extensionUtils;
const LoginManager = imports.misc.loginManager;
const Util = imports.misc.util;

const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;

const { InputStreamSlider } = imports.ui.status.volume;

const Me = ExtensionUtils.getCurrentExtension();

const _ = Gettext.domain(Me.metadata['gettext-domain']).gettext;

const BASIC_SETTINGS = [
    'ca.desrt.dconf-editor.desktop',    // dconf-editor
    'org.gnome.Extensions.desktop',     // gnome-shell-extension-prefs
    'org.gnome.Software.desktop',       // gnome-software
    'org.gnome.tweaks.desktop',         // gnome-tweaks
    'synaptic.desktop',                 // synaptic
    'system-config-printer.desktop',    // system-config-printer
    'users.desktop',                    // gnome-system-tools
]

const GNOME_TERMINAL = 'org.gnome.Terminal.desktop';

const KEY_ALWAYS_SHOW_AUDIO_INPUT = 'always-show-audio-input';
const KEY_DISABLE_RESTART_BUTTONS = 'disable-restart-buttons';
const KEY_SHOW_HIBERNATE = 'show-hibernate';
const KEY_SHOW_HYBRID_SLEEP = 'show-hybrid-sleep';
const KEY_SHOW_SYSTEMMENU = 'show-systemmenu';

const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen';

const SYSTEMMENU_DEFAULT = 0;
const SYSTEMMENU_ICON = 1;

const PopupPanelManagerSettingsMenuItem = GObject.registerClass(
class PopupPanelManagerSettingsMenuItem extends PopupMenu.PopupBaseMenuItem {
    _init(uuid) {
        super._init();
        this._icon = new St.Icon({
            style_class: 'popup-menu-icon',
            icon_name: 'preferences-other-symbolic',
            x_align: Clutter.ActorAlign.END,
        });
        this.add_child(this._icon);
        this.label = new St.Label({
            text: _('Settings Panel Manager'),
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this.label);
        this.label_actor = this.label;
        this.connect('activate', () => {
            Util.spawn([ 'gnome-extensions', 'prefs', uuid ]);
            Main.overview.hide();
        });
    }
});

const PopupTerminalMenuItem = GObject.registerClass(
class PopupTerminalMenuItem extends PopupMenu.PopupBaseMenuItem {
    _init() {
        super._init();
        this._icon = new St.Icon({
            style_class: 'popup-menu-icon',
            x_align: Clutter.ActorAlign.END,
        });
        this.add_child(this._icon);
        this.label = new St.Label({
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this.label);
        this.label_actor = this.label;
        this._appSystem = Shell.AppSystem.get_default();
        this._appSystemChangedId = this._appSystem.connect('installed-changed', this._onInstalledChanged.bind(this));
        this._onInstalledChanged();
    }

    _onDestroy() {
        this._appSystem.disconnect(this._appSystemChangedId);
        super._onDestroy();
    }

    _onInstalledChanged() {
        this._app = this._appSystem.lookup_app(GNOME_TERMINAL);
        this._icon.gicon = this._app?.get_icon() ?? null;
        this.label.set_text(this._app?.get_name() ?? '');
        this.visible = this._app != null;
    }

    activate(event) {
        this._app.activate();
        Main.overview.hide();
        super.activate(event);
    }
});

class PopupHibernationMenuSection extends PopupMenu.PopupMenuSection {
    constructor(settings) {
        super();
        this._settings = settings;
        this._settingsChangedId = [
            this._settings.connect('changed::%s'.format(KEY_SHOW_HYBRID_SLEEP), this._updateHibernation.bind(this)),
            this._settings.connect('changed::%s'.format(KEY_SHOW_HIBERNATE), this._updateHibernation.bind(this)),
        ];
        this._loginManager = LoginManager.getLoginManager();
        this._loginScreenSettings = new Gio.Settings({ schema_id: LOGIN_SCREEN_SCHEMA });
        this._itemHybridSleep = new PopupMenu.PopupMenuItem(_('Hybrid Sleep'));
        this._itemHybridSleep.connect('activate', () => {
            this._onHibernation('HybridSleep');
        });
        this.addMenuItem(this._itemHybridSleep);
        this._itemHibernate = new PopupMenu.PopupMenuItem(_('Hibernate'));
        this._itemHibernate.connect('activate', () => {
            this._onHibernation('Hibernate');
        });
        this.addMenuItem(this._itemHibernate);
        this._openStateChangedId = Main.panel.statusArea.aggregateMenu.menu.connect('open-state-changed', (menu, open) => {
            if (!open) return;
            this._updateHibernation();
        });
    }

    _canHibernation(value, asyncCallback) {
        if (this._loginManager._proxy) {
            this._loginManager._proxy.call(value, null, Gio.DBusCallFlags.NONE, -1, null, (proxy, asyncResult) => {
                try {
                    let result = proxy.call_finish(asyncResult).deep_unpack();
                    asyncCallback(!['no', 'na'].includes(result[0]));
                } catch (e) {
                    asyncCallback(false);
                }
            });
        } else {
            Mainloop.idle_add(() => {
                asyncCallback(false);
                return false;
            });
        }
    }

    _onHibernation(value) {
        if (this._loginManager._proxy) {
            this._loginManager._proxy.call(value, GLib.Variant.new('(b)', [true]), Gio.DBusCallFlags.NONE, -1, null, null);
        } else {
            this._loginManager.emit('prepare-for-sleep', true);
            this._loginManager.emit('prepare-for-sleep', false);
        }
        Main.overview.hide();
    }

    _updateHibernation() {
        let disableRestartButtons = this._loginScreenSettings.get_boolean(KEY_DISABLE_RESTART_BUTTONS);
        let disabled = Main.sessionMode.isLocked || (Main.sessionMode.isGreeter && disableRestartButtons);
        this._canHibernation('CanHibernate', (result) => {
            this._itemHibernate.visible = result && !disabled && this._settings.get_boolean(KEY_SHOW_HIBERNATE);
        });
        this._canHibernation('CanHybridSleep', (result) => {
            this._itemHybridSleep.visible = result && !disabled && this._settings.get_boolean(KEY_SHOW_HYBRID_SLEEP);
        });
        this.visible = this._itemHibernate.visible || this._itemHybridSleep.visible;
    }

    destroy() {
        Main.panel.statusArea.aggregateMenu.menu.disconnect(this._openStateChangedId);
        this._settingsChangedId.forEach(id => {
            this._settings.disconnect(id);
        });
        super.destroy();
    }
}

const PopupBasicSettingsSubMenuMenuItem = GObject.registerClass(
class PopupBasicSettingsSubMenuMenuItem extends PopupMenu.PopupSubMenuMenuItem {
    _init() {
        super._init(_('Basic Settings'), true);
        this.icon.icon_name = 'preferences-system-symbolic';
        this._appSystem = Shell.AppSystem.get_default();
        this._appSystemChangedId = this._appSystem.connect('installed-changed', this._reloadBasicSettings.bind(this));
        this._reloadBasicSettings();
    }

    _reloadBasicSettings() {
        this.menu.removeAll();
        let appList = [];
        BASIC_SETTINGS.forEach(desktopFile => {
            let app = this._appSystem.lookup_app(desktopFile);
            if (app) appList.push(app);
        }, this);
        appList.sort((a, b) => {
            return a.compare_by_name(b);
        });
        appList.forEach(app => {
            let item = new PopupMenu.PopupMenuItem(app.get_name());
            item.connect('activate', () => {
                app.activate();
                Main.overview.hide();
            });
            this.menu.addMenuItem(item);
        }, this);
        this.visible = this.menu.numMenuItems > 0;
    }

    destroy() {
        this._appSystem.disconnect(this._appSystemChangedId);
        super.destroy();
    }
});

const PopupVolumeSubMenuMenuItem = GObject.registerClass(
class PopupVolumeSubMenuMenuItem extends PopupMenu.PopupSubMenuMenuItem {
    _init(deviceType) {
        super._init('...', true);
        this._deviceSection = new PopupMenu.PopupMenuSection();
        this.menu.addMenuItem(this._deviceSection);
        this.menu.addSettingsAction(_('Sound Settings'), 'gnome-sound-panel.desktop');
        this._controlChangedId = [
            Main.panel.statusArea.aggregateMenu._volume._control.connect('active-%s-update'.format(deviceType), (c, id) => {
                this._setActiveDevice(id);
            }),
            Main.panel.statusArea.aggregateMenu._volume._control.connect('default-sink-changed', (c, id) => {
                this._setActiveDevice(id);
            }),
            Main.panel.statusArea.aggregateMenu._volume._control.connect('default-source-changed', (c, id) => {
                this._setActiveDevice(id);
            }),
            Main.panel.statusArea.aggregateMenu._volume._control.connect('state-changed', this._updateDevices.bind(this)),
            Main.panel.statusArea.aggregateMenu._volume._control.connect('%s-added'.format(deviceType), this._updateDevices.bind(this)),
            Main.panel.statusArea.aggregateMenu._volume._control.connect('%s-removed'.format(deviceType), this._updateDevices.bind(this)),
        ];
        this._updateDevices();
    }

    _addDevice(id) {
        let uidevice = this.getDeviceById(id);
        if (!uidevice || !uidevice.port_available) return null;
        let deviceInfo = new Object();
        deviceInfo.uidevice = uidevice;
        deviceInfo.text = uidevice.description;
        if (uidevice.origin !== '') deviceInfo.text += ' - '+uidevice.origin;
        deviceInfo.item = this._deviceSection.addAction(deviceInfo.text, () => {
            this.changeDevice(uidevice);
        });
        this._devices[id] = deviceInfo;
        return uidevice;
    }

    _setActiveDevice(id) {
        let deviceInfo = this._devices[id];
        if (deviceInfo && deviceInfo !== this._activeDevice) {
            if (this._activeDevice) this._activeDevice.item.setOrnament(PopupMenu.Ornament.NONE);
            this._activeDevice = deviceInfo;
            deviceInfo.item.setOrnament(PopupMenu.Ornament.CHECK);
            this.label.text = deviceInfo.text;
            let icon = deviceInfo.uidevice.get_icon_name();
            if (icon === null || icon.trim() === '') icon = this.getDefaultIcon();
            this.icon.icon_name = icon;
        }
    }

    _updateDevices() {
        this._devices = {};
        this._deviceSection.removeAll();
        if (Main.panel.statusArea.aggregateMenu._volume._control.get_state() === Gvc.MixerControlState.READY) {
            let defaultStream = this.getDefaultStream();
            let numId = new Gvc.MixerUIDevice().get_id();
            for (let id = 0; id <= numId; id++) {
                let uidevice = this._addDevice(id);
                if (uidevice) {
                    let stream = Main.panel.statusArea.aggregateMenu._volume._control.get_stream_from_device(uidevice);
                    if (stream) {
                        let stream_port = stream.get_port();
                        if (stream_port && stream === defaultStream && stream_port.port === uidevice.get_port()) this._setActiveDevice(id);
                    }
                }
            }
        }
        this.setVisibility(true);
    }

    destroy() {
        this._controlChangedId.forEach(id => {
            Main.panel.statusArea.aggregateMenu._volume._control.disconnect(id);
        });
        super.destroy();
    }

    setVisibility(visible) {
        this.visible = this._deviceSection.numMenuItems > 1 && visible;
    }
});

const PopupVolumeInputSubMenuMenuItem = GObject.registerClass(
class PopupVolumeInputSubMenuMenuItem extends PopupVolumeSubMenuMenuItem {
    _init() {
        super._init('input');
    }

    changeDevice(uidevice) {
        Main.panel.statusArea.aggregateMenu._volume._control.change_input(uidevice);
    }

    getDefaultIcon() {
        return 'audio-input-microphone';
    }

    getDefaultStream() {
        return Main.panel.statusArea.aggregateMenu._volume._control.get_default_source();
    }

    getDeviceById(id) {
        return Main.panel.statusArea.aggregateMenu._volume._control.lookup_input_id(id);
    }
});

const PopupVolumeOutputSubMenuMenuItem = GObject.registerClass(
class PopupVolumeOutputSubMenuMenuItem extends PopupVolumeSubMenuMenuItem {
    _init() {
        super._init('output');
    }

    changeDevice(uidevice) {
        Main.panel.statusArea.aggregateMenu._volume._control.change_output(uidevice);
    }

    getDefaultIcon() {
        return 'audio-card';
    }

    getDefaultStream() {
        return Main.panel.statusArea.aggregateMenu._volume._control.get_default_sink();
    }

    getDeviceById(id) {
        return Main.panel.statusArea.aggregateMenu._volume._control.lookup_output_id(id);
    }
});

class VolumeIndicator {
    constructor(settings) {
        this._itemVolumeOutput = new PopupVolumeOutputSubMenuMenuItem();
        Main.panel.statusArea.aggregateMenu._volume._volumeMenu.addMenuItem(this._itemVolumeOutput, 1);
        this._itemVolumeInput = new PopupVolumeInputSubMenuMenuItem();
        Main.panel.statusArea.aggregateMenu._volume._volumeMenu.addMenuItem(this._itemVolumeInput, 3);
        this._settings = settings;
        this._settingsChangedId = this._settings.connect('changed::%s'.format(KEY_ALWAYS_SHOW_AUDIO_INPUT), this._onChangedAlwaysShowAudioInput.bind(this));
        this._onChangedAlwaysShowAudioInput();
    }

    _maybeShowInput() {
        Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input._maybeShowInput.call(Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input);
    }

    _onChangedAlwaysShowAudioInput() {
        this._setOverride(this._settings.get_boolean(KEY_ALWAYS_SHOW_AUDIO_INPUT));
        this._maybeShowInput();
        let showInput = Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input._showInput;
        this._itemVolumeInput.setVisibility(showInput);
    }

    _setOverride(isOverride) {
        if (isOverride) {
            Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input._shouldBeVisible = function() {
                return this._stream !== null;
            };
            Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input._maybeShowInput = function() {
                this._showInput = true;
                this._updateVisibility();
            };
        } else {
            Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input._maybeShowInput = InputStreamSlider.prototype._maybeShowInput;
            Main.panel.statusArea.aggregateMenu._volume._volumeMenu._input._shouldBeVisible = InputStreamSlider.prototype._shouldBeVisible;
        }
    }

    destroy() {
        this._settings.disconnect(this._settingsChangedId);
        this._setOverride(false);
        this._maybeShowInput();
        this._itemVolumeInput.destroy();
        this._itemVolumeOutput.destroy();
    }
}

class SystemIndicator {
    constructor(uuid, settings) {
        this._itemTerminal = new PopupTerminalMenuItem();
        Main.panel.statusArea.aggregateMenu._system.menu.addMenuItem(this._itemTerminal, 2);
        this._itemSeparator = new PopupMenu.PopupSeparatorMenuItem();
        Main.panel.statusArea.aggregateMenu._system.menu.addMenuItem(this._itemSeparator, 2);
        this._itemBasicSettings = new PopupBasicSettingsSubMenuMenuItem();
        Main.panel.statusArea.aggregateMenu._system.menu.addMenuItem(this._itemBasicSettings, 2);
        this._itemPanelManagerSettings = new PopupPanelManagerSettingsMenuItem(uuid);
        Main.panel.statusArea.aggregateMenu._system.menu.addMenuItem(this._itemPanelManagerSettings, 2);
        this._itemHibernation = new PopupHibernationMenuSection(settings);
        Main.panel.statusArea.aggregateMenu._system._sessionSubMenu.menu.addMenuItem(this._itemHibernation, 1);
    }

    destroy() {
        this._itemHibernation.destroy();
        this._itemPanelManagerSettings.destroy();
        this._itemBasicSettings.destroy();
        this._itemSeparator.destroy();
        this._itemTerminal.destroy();
    }
}

var SystemMenuButton = class {
    constructor(uuid, settings) {
        Main.panel.statusArea.aggregateMenu.remove_child(Main.panel.statusArea.aggregateMenu._indicators);
        this._hbox = new St.BoxLayout();
        this._hbox.add_child(Main.panel.statusArea.aggregateMenu._indicators);
        this._indicators = new St.BoxLayout({ style_class: 'panel-status-indicators-box' });
        this._indicators.add_child(new St.Icon({
            style_class: 'system-status-icon',
            icon_name: 'system-shutdown-symbolic',
        }));
        this._indicators.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM));
        this._hbox.add_child(this._indicators);
        Main.panel.statusArea.aggregateMenu.add_child(this._hbox);
        this._volume = new VolumeIndicator(settings);
        this._system = new SystemIndicator(uuid, settings);
        this._settings = settings;
        this._settingsChangedId = this._settings.connect('changed::%s'.format(KEY_SHOW_SYSTEMMENU), this._onChangedShowSystemMenu.bind(this));
        this._onChangedShowSystemMenu();
    }

    _onChangedShowSystemMenu() {
        let showSystemMenu = this._settings.get_enum(KEY_SHOW_SYSTEMMENU);
        Main.panel.statusArea.aggregateMenu._indicators.visible = showSystemMenu === SYSTEMMENU_DEFAULT;
        this._indicators.visible = showSystemMenu === SYSTEMMENU_ICON;
    }

    destroy() {
        this._settings.disconnect(this._settingsChangedId);
        this._system.destroy();
        this._volume.destroy();
        this._hbox.remove_child(Main.panel.statusArea.aggregateMenu._indicators);
        Main.panel.statusArea.aggregateMenu.remove_child(this._hbox);
        Main.panel.statusArea.aggregateMenu.add_child(Main.panel.statusArea.aggregateMenu._indicators);
        Main.panel.statusArea.aggregateMenu._indicators.show();
        this._hbox.destroy_all_children();
        this._hbox.destroy();
    }
};
