/* exported getDefault */
const { AccountsService, Clutter, Gdm, Gio, GLib, GObject, Meta } = imports.gi;

const GnomeSession = imports.misc.gnomeSession;
const LoginManager = imports.misc.loginManager;
const Main = imports.ui.main;

const { loadInterfaceXML } = imports.misc.fileUtils;

const LOCKDOWN_SCHEMA = 'org.gnome.desktop.lockdown';
const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen';
const DISABLE_USER_SWITCH_KEY = 'disable-user-switching';
const DISABLE_LOCK_SCREEN_KEY = 'disable-lock-screen';
const DISABLE_LOG_OUT_KEY = 'disable-log-out';
const DISABLE_RESTART_KEY = 'disable-restart-buttons';
const ALWAYS_SHOW_LOG_OUT_KEY = 'always-show-log-out';

const SENSOR_BUS_NAME = 'net.hadess.SensorProxy';
const SENSOR_OBJECT_PATH = '/net/hadess/SensorProxy';

const SensorProxyInterface = loadInterfaceXML('net.hadess.SensorProxy');

const POWER_OFF_ACTION_ID        = 'power-off';
const LOCK_SCREEN_ACTION_ID      = 'lock-screen';
const LOGOUT_ACTION_ID           = 'logout';
const SUSPEND_ACTION_ID          = 'suspend';
const SWITCH_USER_ACTION_ID      = 'switch-user';
const LOCK_ORIENTATION_ACTION_ID = 'lock-orientation';

const SensorProxy = Gio.DBusProxy.makeProxyWrapper(SensorProxyInterface);

let _singleton = null;

function getDefault() {
    if (_singleton == null)
        _singleton = new SystemActions();

    return _singleton;
}

const SystemActions = GObject.registerClass({
    Properties: {
        'can-power-off': GObject.ParamSpec.boolean('can-power-off',
                                                   'can-power-off',
                                                   'can-power-off',
                                                   GObject.ParamFlags.READABLE,
                                                   false),
        'can-suspend': GObject.ParamSpec.boolean('can-suspend',
                                                 'can-suspend',
                                                 'can-suspend',
                                                 GObject.ParamFlags.READABLE,
                                                 false),
        'can-lock-screen': GObject.ParamSpec.boolean('can-lock-screen',
                                                     'can-lock-screen',
                                                     'can-lock-screen',
                                                     GObject.ParamFlags.READABLE,
                                                     false),
        'can-switch-user': GObject.ParamSpec.boolean('can-switch-user',
                                                     'can-switch-user',
                                                     'can-switch-user',
                                                     GObject.ParamFlags.READABLE,
                                                     false),
        'can-logout': GObject.ParamSpec.boolean('can-logout',
                                                'can-logout',
                                                'can-logout',
                                                GObject.ParamFlags.READABLE,
                                                false),
        'can-lock-orientation': GObject.ParamSpec.boolean('can-lock-orientation',
                                                          'can-lock-orientation',
                                                          'can-lock-orientation',
                                                          GObject.ParamFlags.READABLE,
                                                          false),
        'orientation-lock-icon': GObject.ParamSpec.string('orientation-lock-icon',
                                                          'orientation-lock-icon',
                                                          'orientation-lock-icon',
                                                          GObject.ParamFlags.READWRITE,
                                                          null),
    },
}, class SystemActions extends GObject.Object {
    _init() {
        super._init();

        this._canHavePowerOff = true;
        this._canHaveSuspend = true;

        function tokenizeKeywords(keywords) {
            return keywords.split(';').map(keyword => GLib.str_tokenize_and_fold(keyword, null)).flat(2);
        }

        this._actions = new Map();
        this._actions.set(POWER_OFF_ACTION_ID, {
            // Translators: The name of the power-off action in search
            name: C_("search-result", "Power Off"),
            iconName: 'system-shutdown-symbolic',
            // Translators: A list of keywords that match the power-off action, separated by semicolons
            keywords: tokenizeKeywords(_('power off;shutdown;reboot;restart;halt;stop')),
            available: false,
        });
        this._actions.set(LOCK_SCREEN_ACTION_ID, {
            // Translators: The name of the lock screen action in search
            name: C_("search-result", "Lock Screen"),
            iconName: 'system-lock-screen-symbolic',
            // Translators: A list of keywords that match the lock screen action, separated by semicolons
            keywords: tokenizeKeywords(_('lock screen')),
            available: false,
        });
        this._actions.set(LOGOUT_ACTION_ID, {
            // Translators: The name of the logout action in search
            name: C_("search-result", "Log Out"),
            iconName: 'system-log-out-symbolic',
            // Translators: A list of keywords that match the logout action, separated by semicolons
            keywords: tokenizeKeywords(_('logout;log out;sign off')),
            available: false,
        });
        this._actions.set(SUSPEND_ACTION_ID, {
            // Translators: The name of the suspend action in search
            name: C_("search-result", "Suspend"),
            iconName: 'media-playback-pause-symbolic',
            // Translators: A list of keywords that match the suspend action, separated by semicolons
            keywords: tokenizeKeywords(_('suspend;sleep')),
            available: false,
        });
        this._actions.set(SWITCH_USER_ACTION_ID, {
            // Translators: The name of the switch user action in search
            name: C_("search-result", "Switch User"),
            iconName: 'system-switch-user-symbolic',
            // Translators: A list of keywords that match the switch user action, separated by semicolons
            keywords: tokenizeKeywords(_('switch user')),
            available: false,
        });
        this._actions.set(LOCK_ORIENTATION_ACTION_ID, {
            name: '',
            iconName: '',
            // Translators: A list of keywords that match the lock orientation action, separated by semicolons
            keywords: tokenizeKeywords(_('lock orientation;unlock orientation;screen;rotation')),
            available: false,
        });

        this._loginScreenSettings = new Gio.Settings({ schema_id: LOGIN_SCREEN_SCHEMA });
        this._lockdownSettings = new Gio.Settings({ schema_id: LOCKDOWN_SCHEMA });
        this._orientationSettings = new Gio.Settings({ schema_id: 'org.gnome.settings-daemon.peripherals.touchscreen' });

        this._session = new GnomeSession.SessionManager();
        this._loginManager = LoginManager.getLoginManager();
        this._monitorManager = Meta.MonitorManager.get();

        this._userManager = AccountsService.UserManager.get_default();

        this._userManager.connect('notify::is-loaded',
                                  () => this._updateMultiUser());
        this._userManager.connect('notify::has-multiple-users',
                                  () => this._updateMultiUser());
        this._userManager.connect('user-added',
                                  () => this._updateMultiUser());
        this._userManager.connect('user-removed',
                                  () => this._updateMultiUser());

        this._lockdownSettings.connect('changed::%s'.format(DISABLE_USER_SWITCH_KEY),
                                       () => this._updateSwitchUser());
        this._lockdownSettings.connect('changed::%s'.format(DISABLE_LOG_OUT_KEY),
                                       () => this._updateLogout());
        global.settings.connect('changed::%s'.format(ALWAYS_SHOW_LOG_OUT_KEY),
                                () => this._updateLogout());

        this._lockdownSettings.connect('changed::%s'.format(DISABLE_LOCK_SCREEN_KEY),
                                       () => this._updateLockScreen());

        this._lockdownSettings.connect('changed::%s'.format(DISABLE_LOG_OUT_KEY),
                                       () => this._updateHaveShutdown());

        this.forceUpdate();

        this._orientationSettings.connect('changed::orientation-lock', () => {
            this._updateOrientationLock();
            this._updateOrientationLockStatus();
        });
        Main.layoutManager.connect('monitors-changed',
                                   () => this._updateOrientationLock());
        this._sensorProxy = new SensorProxy(Gio.DBus.system,
            SENSOR_BUS_NAME,
            SENSOR_OBJECT_PATH,
            (proxy, error)  => {
                if (error)
                    log(error.message);
            },
            null,
            Gio.DBusProxyFlags.DO_NOT_AUTO_START);
        this._sensorProxy.connect('g-properties-changed', () => {
            this._updateOrientationLock();
        });
        this._sensorProxy.connect('notify::g-name-owner', () => {
            this._updateOrientationLock();
        });
        this._updateOrientationLock();
        this._updateOrientationLockStatus();

        Main.sessionMode.connect('updated', () => this._sessionUpdated());
        this._sessionUpdated();
    }

    // eslint-disable-next-line camelcase
    get can_power_off() {
        return this._actions.get(POWER_OFF_ACTION_ID).available;
    }

    // eslint-disable-next-line camelcase
    get can_suspend() {
        return this._actions.get(SUSPEND_ACTION_ID).available;
    }

    // eslint-disable-next-line camelcase
    get can_lock_screen() {
        return this._actions.get(LOCK_SCREEN_ACTION_ID).available;
    }

    // eslint-disable-next-line camelcase
    get can_switch_user() {
        return this._actions.get(SWITCH_USER_ACTION_ID).available;
    }

    // eslint-disable-next-line camelcase
    get can_logout() {
        return this._actions.get(LOGOUT_ACTION_ID).available;
    }

    // eslint-disable-next-line camelcase
    get can_lock_orientation() {
        return this._actions.get(LOCK_ORIENTATION_ACTION_ID).available;
    }

    // eslint-disable-next-line camelcase
    get orientation_lock_icon() {
        return this._actions.get(LOCK_ORIENTATION_ACTION_ID).iconName;
    }

    _updateOrientationLock() {
        let available = false;
        if (this._sensorProxy.g_name_owner) {
            available = this._sensorProxy.HasAccelerometer &&
                        this._monitorManager.get_is_builtin_display_on();
        }

        this._actions.get(LOCK_ORIENTATION_ACTION_ID).available = available;

        this.notify('can-lock-orientation');
    }

    _updateOrientationLockStatus() {
        let locked = this._orientationSettings.get_boolean('orientation-lock');
        let action = this._actions.get(LOCK_ORIENTATION_ACTION_ID);

        // Translators: The name of the lock orientation action in search
        // and in the system status menu
        let name = locked
            ? C_('search-result', 'Unlock Screen Rotation')
            : C_('search-result', 'Lock Screen Rotation');
        let iconName = locked
            ? 'rotation-locked-symbolic'
            : 'rotation-allowed-symbolic';

        action.name = name;
        action.iconName = iconName;

        this.notify('orientation-lock-icon');
    }

    _sessionUpdated() {
        this._updateLockScreen();
        this._updatePowerOff();
        this._updateSuspend();
        this._updateMultiUser();
    }

    forceUpdate() {
        // Whether those actions are available or not depends on both lockdown
        // settings and Polkit policy - we don't get change notifications for the
        // latter, so their value may be outdated; force an update now
        this._updateHaveShutdown();
        this._updateHaveSuspend();
    }

    getMatchingActions(terms) {
        // terms is a list of strings
        terms = terms.map(term => GLib.str_tokenize_and_fold(term, null)[0]);

        let results = [];

        for (let [key, { available, keywords }] of this._actions) {
            if (available && terms.every(t => keywords.some(k => k.startsWith(t))))
                results.push(key);
        }

        return results;
    }

    getName(id) {
        return this._actions.get(id).name;
    }

    getIconName(id) {
        return this._actions.get(id).iconName;
    }

    activateAction(id) {
        switch (id) {
        case POWER_OFF_ACTION_ID:
            this.activatePowerOff();
            break;
        case LOCK_SCREEN_ACTION_ID:
            this.activateLockScreen();
            break;
        case LOGOUT_ACTION_ID:
            this.activateLogout();
            break;
        case SUSPEND_ACTION_ID:
            this.activateSuspend();
            break;
        case SWITCH_USER_ACTION_ID:
            this.activateSwitchUser();
            break;
        case LOCK_ORIENTATION_ACTION_ID:
            this.activateLockOrientation();
            break;
        }
    }

    _updateLockScreen() {
        let showLock = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
        let allowLockScreen = !this._lockdownSettings.get_boolean(DISABLE_LOCK_SCREEN_KEY);
        this._actions.get(LOCK_SCREEN_ACTION_ID).available = showLock && allowLockScreen && LoginManager.canLock();
        this.notify('can-lock-screen');
    }

    _updateHaveShutdown() {
        this._session.CanShutdownRemote((result, error) => {
            if (error)
                return;

            this._canHavePowerOff = result[0];
            this._updatePowerOff();
        });
    }

    _updatePowerOff() {
        let disabled = Main.sessionMode.isLocked ||
                       (Main.sessionMode.isGreeter &&
                        this._loginScreenSettings.get_boolean(DISABLE_RESTART_KEY));
        this._actions.get(POWER_OFF_ACTION_ID).available = this._canHavePowerOff && !disabled;
        this.notify('can-power-off');
    }

    _updateHaveSuspend() {
        this._loginManager.canSuspend(
            (canSuspend, needsAuth) => {
                this._canHaveSuspend = canSuspend;
                this._suspendNeedsAuth = needsAuth;
                this._updateSuspend();
            });
    }

    _updateSuspend() {
        let disabled = (Main.sessionMode.isLocked &&
                        this._suspendNeedsAuth) ||
                       (Main.sessionMode.isGreeter &&
                        this._loginScreenSettings.get_boolean(DISABLE_RESTART_KEY));
        this._actions.get(SUSPEND_ACTION_ID).available = this._canHaveSuspend && !disabled;
        this.notify('can-suspend');
    }

    _updateMultiUser() {
        this._updateLogout();
        this._updateSwitchUser();
    }

    _updateSwitchUser() {
        let allowSwitch = !this._lockdownSettings.get_boolean(DISABLE_USER_SWITCH_KEY);
        let multiUser = this._userManager.can_switch() && this._userManager.has_multiple_users;
        let shouldShowInMode = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;

        let visible = allowSwitch && multiUser && shouldShowInMode;
        this._actions.get(SWITCH_USER_ACTION_ID).available = visible;
        this.notify('can-switch-user');

        return visible;
    }

    _updateLogout() {
        let user = this._userManager.get_user(GLib.get_user_name());

        let allowLogout = !this._lockdownSettings.get_boolean(DISABLE_LOG_OUT_KEY);
        let alwaysShow = global.settings.get_boolean(ALWAYS_SHOW_LOG_OUT_KEY);
        let systemAccount = user.system_account;
        let localAccount = user.local_account;
        let multiUser = this._userManager.has_multiple_users;
        let multiSession = Gdm.get_session_ids().length > 1;
        let shouldShowInMode = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;

        let visible = allowLogout && (alwaysShow || multiUser || multiSession || systemAccount || !localAccount) && shouldShowInMode;
        this._actions.get(LOGOUT_ACTION_ID).available = visible;
        this.notify('can-logout');

        return visible;
    }

    activateLockOrientation() {
        if (!this._actions.get(LOCK_ORIENTATION_ACTION_ID).available)
            throw new Error('The lock-orientation action is not available!');

        let locked = this._orientationSettings.get_boolean('orientation-lock');
        this._orientationSettings.set_boolean('orientation-lock', !locked);
    }

    activateLockScreen() {
        if (!this._actions.get(LOCK_SCREEN_ACTION_ID).available)
            throw new Error('The lock-screen action is not available!');

        Main.screenShield.lock(true);
    }

    activateSwitchUser() {
        if (!this._actions.get(SWITCH_USER_ACTION_ID).available)
            throw new Error('The switch-user action is not available!');

        if (Main.screenShield)
            Main.screenShield.lock(false);

        Clutter.threads_add_repaint_func_full(Clutter.RepaintFlags.POST_PAINT, () => {
            Gdm.goto_login_session_sync(null);
            return false;
        });
    }

    activateLogout() {
        if (!this._actions.get(LOGOUT_ACTION_ID).available)
            throw new Error('The logout action is not available!');

        Main.overview.hide();
        this._session.LogoutRemote(0);
    }

    activatePowerOff() {
        if (!this._actions.get(POWER_OFF_ACTION_ID).available)
            throw new Error('The power-off action is not available!');

        this._session.ShutdownRemote(0);
    }

    activateSuspend() {
        if (!this._actions.get(SUSPEND_ACTION_ID).available)
            throw new Error('The suspend action is not available!');

        this._loginManager.suspend();
    }
});