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

const { Gdk, Gio, GLib, GObject, Gtk, Shew } = imports.gi;

const ExtensionUtils = imports.misc.extensionUtils;

const { loadInterfaceXML } = imports.misc.fileUtils;
const { ServiceImplementation } = imports.dbusService;

const ExtensionsIface = loadInterfaceXML('org.gnome.Shell.Extensions');
const ExtensionsProxy = Gio.DBusProxy.makeProxyWrapper(ExtensionsIface);

var ExtensionsService = class extends ServiceImplementation {
    constructor() {
        super(ExtensionsIface, '/org/gnome/Shell/Extensions');

        this._proxy = new ExtensionsProxy(Gio.DBus.session,
            'org.gnome.Shell', '/org/gnome/Shell');

        this._proxy.connectSignal('ExtensionStateChanged',
            (proxy, sender, params) => {
                this._dbusImpl.emit_signal('ExtensionStateChanged',
                    new GLib.Variant('(sa{sv})', params));
            });

        this._proxy.connect('g-properties-changed', () => {
            this._dbusImpl.emit_property_changed('UserExtensionsEnabled',
                new GLib.Variant('b', this._proxy.UserExtensionsEnabled));
        });
    }

    get ShellVersion() {
        return this._proxy.ShellVersion;
    }

    get UserExtensionsEnabled() {
        return this._proxy.UserExtensionsEnabled;
    }

    set UserExtensionsEnabled(enable) {
        this._proxy.UserExtensionsEnabled = enable;
    }

    ListExtensionsAsync(params, invocation) {
        this._proxy.ListExtensionsRemote(...params, (res, error) => {
            if (this._handleError(invocation, error))
                return;

            invocation.return_value(new GLib.Variant('(a{sa{sv}})', res));
        });
    }

    GetExtensionInfoAsync(params, invocation) {
        this._proxy.GetExtensionInfoRemote(...params, (res, error) => {
            if (this._handleError(invocation, error))
                return;

            invocation.return_value(new GLib.Variant('(a{sv})', res));
        });
    }

    GetExtensionErrorsAsync(params, invocation) {
        this._proxy.GetExtensionErrorsRemote(...params, (res, error) => {
            if (this._handleError(invocation, error))
                return;

            invocation.return_value(new GLib.Variant('(as)', res));
        });
    }

    InstallRemoteExtensionAsync(params, invocation) {
        this._proxy.InstallRemoteExtensionRemote(...params, (res, error) => {
            if (this._handleError(invocation, error))
                return;

            invocation.return_value(new GLib.Variant('(s)', res));
        });
    }

    UninstallExtensionAsync(params, invocation) {
        this._proxy.UninstallExtensionRemote(...params, (res, error) => {
            if (this._handleError(invocation, error))
                return;

            invocation.return_value(new GLib.Variant('(b)', res));
        });
    }

    EnableExtensionAsync(params, invocation) {
        this._proxy.EnableExtensionRemote(...params, (res, error) => {
            if (this._handleError(invocation, error))
                return;

            invocation.return_value(new GLib.Variant('(b)', res));
        });
    }

    DisableExtensionAsync(params, invocation) {
        this._proxy.DisableExtensionRemote(...params, (res, error) => {
            if (this._handleError(invocation, error))
                return;

            invocation.return_value(new GLib.Variant('(b)', res));
        });
    }

    LaunchExtensionPrefsAsync([uuid], invocation) {
        this.OpenExtensionPrefsAsync([uuid, '', {}], invocation);
    }

    OpenExtensionPrefsAsync(params, invocation) {
        const [uuid, parentWindow, options] = params;

        this._proxy.GetExtensionInfoRemote(uuid, (res, error) => {
            if (this._handleError(invocation, error))
                return;

            const [serialized] = res;
            const extension = ExtensionUtils.deserializeExtension(serialized);

            const window = new ExtensionPrefsDialog(extension);
            window.connect('realize', () => {
                let externalWindow = null;

                if (parentWindow)
                    externalWindow = Shew.ExternalWindow.new_from_handle(parentWindow);

                if (externalWindow)
                    externalWindow.set_parent_of(window.get_surface());
            });

            if (options.modal)
                window.modal = options.modal.get_boolean();

            window.connect('destroy', () => this.release());
            this.hold();

            window.show();

            invocation.return_value(null);
        });
    }

    CheckForUpdatesAsync(params, invocation) {
        this._proxy.CheckForUpdatesRemote(...params, (res, error) => {
            if (this._handleError(invocation, error))
                return;

            invocation.return_value(null);
        });
    }
};

var ExtensionPrefsDialog = GObject.registerClass({
    GTypeName: 'ExtensionPrefsDialog',
    Template: 'resource:///org/gnome/Shell/Extensions/ui/extension-prefs-dialog.ui',
    InternalChildren: [
        'stack',
        'expander',
        'expanderArrow',
        'revealer',
        'errorView',
    ],
}, class ExtensionPrefsDialog extends Gtk.Window {
    _init(extension) {
        super._init({
            title: extension.metadata.name,
        });

        this._uuid = extension.uuid;
        this._url = extension.metadata.url || '';

        this._actionGroup = new Gio.SimpleActionGroup();
        this.insert_action_group('win', this._actionGroup);

        this._initActions();
        this._addCustomStylesheet();

        this._gesture = new Gtk.GestureClick({
            button: 0,
            exclusive: true,
        });
        this._expander.add_controller(this._gesture);

        this._gesture.connect('released', (gesture, nPress) => {
            if (nPress === 1)
                this._revealer.reveal_child = !this._revealer.reveal_child;
        });

        this._revealer.connect('notify::reveal-child', () => {
            this._expanderArrow.icon_name = this._revealer.reveal_child
                ? 'pan-down-symbolic'
                : 'pan-end-symbolic';
            this._syncExpandedStyle();
        });
        this._revealer.connect('notify::child-revealed',
            () => this._syncExpandedStyle());

        try {
            ExtensionUtils.installImporter(extension);

            // give extension prefs access to their own extension object
            ExtensionUtils.getCurrentExtension = () => extension;

            const prefsModule = extension.imports.prefs;
            prefsModule.init(extension.metadata);

            const widget = prefsModule.buildPrefsWidget();
            this._stack.add_named(widget, 'prefs');
            this._stack.visible_child = widget;
        } catch (e) {
            this._setError(e);
        }
    }

    _syncExpandedStyle() {
        if (this._revealer.reveal_child)
            this._expander.add_css_class('expanded');
        else if (!this._revealer.child_revealed)
            this._expander.remove_css_class('expanded');
    }

    _setError(exc) {
        this._errorView.buffer.text = `${exc}\n\nStack trace:\n`;
        // Indent stack trace.
        this._errorView.buffer.text +=
            exc.stack.split('\n').map(line => `  ${line}`).join('\n');

        // markdown for pasting in gitlab issues
        let lines = [
            `The settings of extension ${this._uuid} had an error:`,
            '```',
            `${exc}`,
            '```',
            '',
            'Stack trace:',
            '```',
            exc.stack.replace(/\n$/, ''), // stack without trailing newline
            '```',
            '',
        ];
        this._errorMarkdown = lines.join('\n');
        this._actionGroup.lookup('copy-error').enabled = true;
    }

    _initActions() {
        let action;

        action = new Gio.SimpleAction({
            name: 'copy-error',
            enabled: false,
        });
        action.connect('activate', () => {
            const clipboard = Gtk.Clipboard.get_default(this.get_display());
            clipboard.set_text(this._errorMarkdown, -1);
        });
        this._actionGroup.add_action(action);

        action = new Gio.SimpleAction({
            name: 'show-url',
            enabled: this._url !== '',
        });
        action.connect('activate', () => {
            Gio.AppInfo.launch_default_for_uri(this._url,
                this.get_display().get_app_launch_context());
        });
        this._actionGroup.add_action(action);
    }

    _addCustomStylesheet() {
        let provider = new Gtk.CssProvider();
        let uri = 'resource:///org/gnome/Shell/Extensions/css/application.css';
        try {
            provider.load_from_file(Gio.File.new_for_uri(uri));
        } catch (e) {
            logError(e, 'Failed to add application style');
        }
        Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(),
            provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
    }
});
