/* extension.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 3 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 .
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
/* Based on https://stackoverflow.com/questions/33001192/how-to-send-a-string-to-a-gnome-shell-extension */
// https://github.com/bananenfisch/RecentItems/blob/master/extension.js
// https://github.com/julio641742/gnome-shell-extension-reference/blob/master/tutorials/POPUPMENU-EXTENSION.md
// https://gjs-docs.gnome.org/st10~1.0_api/st.widget
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/master/js/ui/panelMenu.js
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/master/js/ui/popupMenu.js
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/master/js/ui/panel.js
const St = imports.gi.St;
const Gio = imports.gi.Gio;
const Lang = imports.lang;
const GLib = imports.gi.GLib
const Main = imports.ui.main;
const Mainloop = imports.mainloop;
const Clutter = imports.gi.Clutter;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const GObject = imports.gi.GObject;
const Pixbuf = imports.gi.GdkPixbuf;
const Cogl = imports.gi.Cogl;
function hash_get(hash, key, default_value) {
    if (hash.hasOwnProperty(key))
	return hash[key];
    return default_value;
}
function log(message) {
    global.log('[GenericMontior]', message);
}
var MyPopupMenuItem = GObject.registerClass({
  GTypeName: "MyPopupMenuItem"
},
class MyPopupMenuItem extends PopupMenu.PopupBaseMenuItem {
    _init(widgets, params) {
	super._init(params);
	this.box = new St.BoxLayout({ style_class: 'popup-combobox-item' });
	this.box.set_vertical(true);
	for (let widgetIndex in widgets)
	    this.box.add(widgets[widgetIndex]);
	
	this.add_child(this.box);
    }
});
var MonitorWidget = GObject.registerClass({
  GTypeName: "MonitorWidget"
},
class MonitorWidget extends PanelMenu.Button {
    _init(item, group, dbus, position) {	
	super._init(0.0);
	
        this.name = item['name'];
        this.group = group;
	this.fullname = this.name + '@' + this.group;
	this.dbus = dbus;
	
        if (item.hasOwnProperty('icon'))
	{
	    if (typeof(item['icon']) === "string")
		this.icon = this._createIconOld(item);
	    else
		this.icon = this._createIcon(item['icon']);
	    if (this.icon !== null) {
		this._connectWidgetSignals(this.icon);
		this.add_child(this.icon);
	    }
	}
	else
	    this.icon = null;
	
        if (item.hasOwnProperty('text'))
	{
	    if (typeof(item['text']) === "string")
		this.widget = this._createTextOld(item);
	    else
		this.widget = this._createText(item['text']);
	    if (this.widget !== null) {
		this._connectWidgetSignals(this.widget);
		this.add_child(this.widget);
	    }
	}
	else
	    this.widget = null;
	
        if (item.hasOwnProperty('popup'))
            this._createPopup(item['popup']);
	this.onClick = hash_get(item, 'on-click', '');
	this.onEnter = hash_get(item, 'on-enter', '');
	this.onLeave = hash_get(item, 'on-leave', '');
        let box = hash_get(item, 'box', 'center');
	if (box === 'right' && position == -1)
	    position = 0;
	
	this.connect('enter-event', this._onEnter.bind(this));
	this.connect('leave-event', this._onLeave.bind(this));
	this.connect('style-changed', this._onStyleChanged.bind(this));
	// Disable click event at PanelMenu.button level
	this.setSensitive(false);
	
	this.nbClicks = 0;
	this.button = -1;
	this.nbEnter = 0;
	Main.panel.addToStatusArea(this.fullname, this, position, box);
    }
    _onStyleChanged(actor) {
	// Force these values to avoid big spaces between each widgets
        this._minHPadding = 1;
        this._natHPadding = 1;
    }
    _connectWidgetSignals(widget) {
	widget.connect('enter-event', this._onEnter.bind(this));
	widget.connect('leave-event', this._onLeave.bind(this));
	widget.set_reactive(true);
	widget.connect('button-release-event', Lang.bind(this, this._clicked));
    }
    
    _createPopup(item) {
        if (!item.hasOwnProperty('items')) {
	    return null;
	}
	let widgets = [];
	for (let itemIndex in item['items']) {
	    let widget = null;
	    let widgetDict = item['items'][itemIndex];
	    if (widgetDict.hasOwnProperty('text'))
		widget = this._createText(widgetDict['text']);
	    else if (widgetDict.hasOwnProperty('picture'))
		widget = this._createPicture(widgetDict['picture']);
	    if (widget !== null)
		widgets.push(widget);
	}
	if (widgets.length > 0) {
	    this.menuItem = new MyPopupMenuItem(widgets, {});	
            this.menu.addMenuItem(this.menuItem);
	    this.menu.setSensitive(false);
	    return this.menuItem;
	}
	return null;
    }
    
    _createTextOld(item) {
	var item_values = {};
	item_values = {'text':item['text']};
        if (item.hasOwnProperty('style'))
	    item_values['style'] = item['style'];
	return this._createText(item_values);
    }
    
    _createText(item) {
        if (!item.hasOwnProperty('text')) {
	    log("Text must have a \'text\' value");
	    return null;
	}
	let style = hash_get(item, 'style', '');
	this.textProperties = item;
	if (item['text'] === '') {
            return null;
        } else {
            let widget = new St.Button({ label: item['text'] });
            widget.set_style(style);
	    return widget;
	}
    }
    _createIconOld(item) {
	var item_values = {};
	item_values = {'path':item['icon']};
        if (item.hasOwnProperty('iconStyle'))
	    item_values['style'] = item['iconStyle'];
	return this._createIcon(item_values);
    }
    _createIcon(item) {
        if (!item.hasOwnProperty('path')) {
	    log("Icon must have a \'path\' value");
	    return null;
	}
	let style = hash_get(item, 'style', '');
	this.iconProperties = item;
	if (item['path'] === '') {
            return null;
        } else {
            let gicon = Gio.icon_new_for_string(item['path']);
            gicon = new St.Icon({ gicon });
            gicon.set_style(style);
	    return gicon;
        }
    }
    _createPicture(item) {
        if (!item.hasOwnProperty('path')) {
	    log("Picture must have a \'path\' value");
	    return null;
	}
	let width = hash_get(item, 'width', -1);
	let height = hash_get(item, 'height', -1);
	if (typeof(width) === "string")
	    width = parseInt(width, 10);
	if (typeof(heigth) === "string")
	    heigth = parseInt(heigth, 10);
	let img = new Clutter.Image();
	let initial_pixbuf = Pixbuf.Pixbuf.new_from_file(item['path']);
	img.set_data(initial_pixbuf.get_pixels(),
			   initial_pixbuf.get_has_alpha() ? Cogl.PixelFormat.RGBA_8888
                           : Cogl.PixelFormat.RGB_888,
			   initial_pixbuf.get_width(),
			   initial_pixbuf.get_height(),
			   initial_pixbuf.get_rowstride());
	let picture = new Clutter.Actor();
	picture.set_content(img);
	picture.set_size((width != -1)?width:initial_pixbuf.get_width(),
		      (height != -1)?height:initial_pixbuf.get_height());
	return picture;
    }
    _manageEventAction(action) {
	if (action === 'open-popup')
	    this.menu.open(true);
	else if (action === 'close-popup')
	    this.menu.close();
	else if (action == 'delete')
	    this.dbus.deleteItem(this, this.group);
	else
	    return Clutter.EVENT_PROPAGATE;
	return Clutter.EVENT_STOP;
    }
    
    _manageLeaveEvent() {
	if (this.nbEnter <= 0) {
	    this._manageEventAction(this.onLeave);
	    this.nbEnter = 0;
	}
    }
    
    _doClickCallback() {
        let right = '';
        let nbClicks = '';
        if (this.button == 3)
            right = 'Right';
        if (this.nbClicks > 1)
            nbClicks = 'Dbl';
        let signalName = 'on' + right + nbClicks + 'Click';
        this.dbus.emit_signal(signalName, this.fullname);
	this.nbClicks = 0;
	this.button = -1;
    }
    _clicked(actor, event) {
	if (this.onClick === 'signal') {
	    if (event.get_button() == this.button) {
		this.nbClicks++;
	    } else {
		this.button = event.get_button();
		this.nbClicks = 1;
		Mainloop.timeout_add(this.dbus.ClutterSettings['double-click-time'],
				     Lang.bind(this, this._doClickCallback));
	    }
	    return Clutter.EVENT_STOP;
	} else if (this.onClick === 'open-popup') {
	    this.menu.toggle();
	} else
	    return this._manageEventAction(this.onClick);
    }
    
    _onEnter() {
	if (this.onEnter === 'signal')
	    this.dbus.emit_signal('onEnter', this.fullname);
	else {
	    this.nbEnter++;
	    return this._manageEventAction(this.onEnter);
	}
	return Clutter.EVENT_STOP;
    }
    _onLeave() {
	if (this.onLeave === 'signal')
	    this.dbus.emit_signal('onLeave', this.fullname);
	else {
	    this.nbEnter--;
	    Mainloop.timeout_add(this.dbus.ClutterSettings['double-click-time'],
				 Lang.bind(this, this._manageLeaveEvent));
	}
	return Clutter.EVENT_STOP;
    }
    update(item) {
        let prevWidget = this.widget;
        let prevIcon = this.icon;
        if (item.hasOwnProperty('text'))
	{
	    let text = '';
	    let style = '';
	    if (typeof(item['text']) === "string") {
		text = hash_get(item, 'text', '');
		style = hash_get(item, 'style', '');
	    } else {
		let textValues = item['text'];
		text = hash_get(textValues, 'text', '');
		style = hash_get(textValues, 'style', '');
	    }
	    
            if (text !== '') {
		if (!this.widget) {
		    if (typeof(item['text']) === "string")
			this.widget = this._createTextOld(item);
		    else
			this.widget = this._createText(item['text']);
                    this.insert_child_above(this.widget, this.icon);
		} else {
                    this.widget.label = text;
		}
	    }
            if (style !== '' && this.widget) {
		this.widget.set_style(style);
            }
        }
        if (item.hasOwnProperty('icon'))
	{
	    let icon = '';
	    let style = '';
	    if (typeof(item['icon']) === "string") {
		icon = hash_get(item, 'icon', '');
		style = hash_get(item, 'iconStyle', '');
	    } else {
		let iconValues = item['icon'];
		icon = hash_get(iconValues, 'path', '');
		style = hash_get(textValues, 'style', '');
	    }
	    
            if (icon !== '') {
		if (typeof(item['icon']) === "string")
		    this.icon = this._createIconOld(item);
		else
		    this.icon = this._createIcon(item['icon']);
	    }
	    
            if (prevIcon) {
                this.insert_child_above(this.icon, prevIcon);
		this.remove_child(prevIcon);
		//delete prevIcon;
            } else
                this.insert_child_before(this.icon, prevWidget);
            if (style !== '' && this.icon) {
		this.icon.set_style(style);
            }
        }
        if (item.hasOwnProperty('popup'))
	{
	    if (this.menuItem) {
		this.menu.removeAll();
		//delete this.menuItem;
	    }
	    this._createPopup(item['popup']);
	}
	this.onClick = hash_get(item, 'on-click', this.onClick);
	this.onEnter = hash_get(item, 'on-enter', this.onEnter);
	this.onLeave = hash_get(item, 'on-leave', this.onLeave);
    }
});
// From https://github.com/ubuntu/gnome-shell-extension-appindicator/blob/master/interfaces.js
// loads a xml file into an in-memory string
function loadInterfaceXml(filename) {
    let extension = imports.misc.extensionUtils.getCurrentExtension();
    let interfacesDir = extension.dir.get_child('.');
    let file = interfacesDir.get_child(filename);
    let [result, contents] = imports.gi.GLib.file_get_contents(file.get_path());
    if (result) {
        // HACK: The "" + trick is important as hell because file_get_contents returns
        // an object (WTF?) but Gio.makeProxyWrapper requires `typeof() == "string"`
        // Otherwise, it will try to check `instanceof XML` and fail miserably because there
        // is no `XML` on very recent SpiderMonkey releases (or, if SpiderMonkey is old enough,
        // will spit out a TypeError soon).
        if (contents instanceof Uint8Array)
            contents = imports.byteArray.toString(contents);
        let res = `${contents}`;
        return res;
    } else {
        throw new Error(`Generic monitor: Could not load file: ${filename}`);
    }
}
class GenericMonitorDBUS {
    constructor() {
        this.monitor_groups = {};
        this.actor_clicked = {};
        this.ClutterSettings = Clutter.Settings.get_default();
        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(loadInterfaceXml('dbus.xml'), this);
        this._dbusImpl.export(Gio.DBus.session, '/com/soutade/GenericMonitor');
        this._dbusImpl.emit_signal('onActivate', null);
    }
    emit_signal(name, value) {
        this._dbusImpl.emit_signal(name, GLib.Variant.new('(s)',[value]));
    }
    _checkParameters(parameters) {
        if (!parameters.hasOwnProperty('group')) {
	    log('No group defined');
	    return false;
	}
	if (!parameters.hasOwnProperty('items')) {
	    log('No items defined');
	    return false;
	}
        for (let itemIndex in parameters['items']) {
            let item = parameters['items'][itemIndex];
            if (!item.hasOwnProperty('name')) {
		log('No name defined for item');
		return false;
	    }
        }
	return true;
    }
    _getItemFromGroup(group, name) {
        for (let groupItemIndex in group) {
            let groupItem = group[groupItemIndex];
            if (groupItem.name === name)
                return groupItem;
        }
        return null;
    }
    _removeFromArray(array, value) {
        for(let i=0; i= childrens.length)
			position = -1;
		}
                monitorWidget = new MonitorWidget(item, groupName, this, position);
                group.push(monitorWidget);
            } else {
                monitorWidget.update(item);
            }
        }
    }
    deleteItem(item, groupName) {
        let group = this.monitor_groups[groupName];
        group = this._removeFromArray(group, item);
	item.destroy();
        if (group.length === 0)
            delete this.monitor_groups[groupName];
        else
            this.monitor_groups = group;
    }
    
    deleteItems(str) {
        let parameters = JSON.parse(str);
        if (!parameters.hasOwnProperty('items')) {
	    log('No items defined');
	    return false;
	}
        for (let itemIndex in parameters['items']) {
            let itemName = parameters['items'][itemIndex];
            let fullName = itemName.split('@');
            if (fullName.length !== 2) {
		log(`Invalid name ${itemName}`);
		return false;
	    }
            itemName = fullName[0];
            let groupName = fullName[1];
            if (!this.monitor_groups.hasOwnProperty(groupName))
                continue;
            let group = this.monitor_groups[groupName];
            let item = this._getItemFromGroup(group, itemName);
            if (item !== null) {
                this.deleteItem(item, groupName);
            }
        }
    }
    deleteGroups(str) {
        let parameters = JSON.parse(str);
        if (!parameters.hasOwnProperty('groups')) {
	    log('No groups defined');
	    return false;
	}
        let groupsToDelete = [];
        for (let groupIndex in parameters['groups']) {
            let groupName = parameters['groups'][groupIndex];
            if (!this.monitor_groups.hasOwnProperty(groupName))
                continue;
            let group = this.monitor_groups[groupName];
            for (let itemIndex in group)
		group[itemIndex].destroy();
            groupsToDelete.push(groupName);
        }
        for (let groupDeleteIndex in groupsToDelete) {
            let groupName = groupsToDelete[groupDeleteIndex];
            delete this.monitor_groups[groupName];
        }
    }
    destructor() {
        this._dbusImpl.emit_signal('onDeactivate', null);
        for (let groupIndex in this.monitor_groups) {
            let group = this.monitor_groups[groupIndex];
            for (let itemIndex in group)
		group[itemIndex].destroy();
        }
        this.monitor_groups = {};
        this._dbusImpl.unexport();
    }
}
class Extension {
    enable() {
        this.textDBusService = new GenericMonitorDBUS();
    }
    disable() {
        this.textDBusService.destructor();
        delete this.textDBusService;
    }
}
let extension = new Extension();
function init() {
}
function enable() {
    extension.enable();
}
function disable() {
    extension.disable();
}