/*
 * This file is part of budgie-desktop
 *
 * Copyright © 2015-2020 Budgie Desktop Developers
 *
 * 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.
 */

public class IconTasklist : Budgie.Plugin, Peas.ExtensionBase {
	public Budgie.Applet get_panel_widget(string uuid) {
		return new IconTasklistApplet(uuid);
	}
}

[GtkTemplate (ui="/com/solus-project/icon-tasklist/settings.ui")]
public class IconTasklistSettings : Gtk.Grid {
	[GtkChild]
	private Gtk.Switch? switch_grouping;

	[GtkChild]
	private Gtk.Switch? switch_restrict;

	[GtkChild]
	private Gtk.Switch? switch_lock_icons;

	[GtkChild]
	private Gtk.Switch? switch_only_pinned;

	[GtkChild]
	private Gtk.Switch? show_all_on_click;

	[GtkChild]
	private Gtk.Switch? switch_middle_click_create_new_instance;

	[GtkChild]
	private Gtk.Switch? switch_require_double_click_to_launch_new_instance;

	private Settings? settings;

	public IconTasklistSettings(Settings? settings) {
		this.settings = settings;
		settings.bind("grouping", switch_grouping, "active", SettingsBindFlags.DEFAULT);
		settings.bind("restrict-to-workspace", switch_restrict, "active", SettingsBindFlags.DEFAULT);
		settings.bind("lock-icons", switch_lock_icons, "active", SettingsBindFlags.DEFAULT);
		settings.bind("only-pinned", switch_only_pinned, "active", SettingsBindFlags.DEFAULT);
		settings.bind("show-all-windows-on-click", show_all_on_click, "active", SettingsBindFlags.DEFAULT);
		settings.bind("middle-click-launch-new-instance", switch_middle_click_create_new_instance, "active", SettingsBindFlags.DEFAULT);
		settings.bind("require-double-click-to-launch", switch_require_double_click_to_launch_new_instance, "active", SettingsBindFlags.DEFAULT);
	}
}

public class IconTasklistApplet : Budgie.Applet {
	private Budgie.Abomination? abomination = null;
	private Wnck.Screen? wnck_screen = null;
	private Settings? settings = null;
	private HashTable<string,IconButton> buttons;
	private HashTable<string,string> id_map;
	private Gtk.Box? main_layout = null;
	private bool grouping = true;
	private bool restrict_to_workspace = false;
	private bool only_show_pinned = false;

	/* Applet support */
	private DesktopHelper? desktop_helper = null;
	private Budgie.AppSystem? app_system = null;
	private unowned Budgie.PopoverManager? manager = null;

	public string uuid { public set; public get; }

	public override Gtk.Widget? get_settings_ui() {
		return new IconTasklistSettings(this.get_applet_settings(uuid));
	}

	public override bool supports_settings() {
		return true;
	}

	public IconTasklistApplet(string uuid) {
		Object(uuid: uuid);

		/* Get our settings working first */
		settings_schema = "com.solus-project.icon-tasklist";
		settings_prefix = "/com/solus-project/budgie-panel/instance/icon-tasklist";
		settings = this.get_applet_settings(uuid);

		/* Somewhere to store the window mappings */
		buttons = new HashTable<string,IconButton>(str_hash, str_equal);
		id_map = new HashTable<string,string>(str_hash, str_equal);
		main_layout = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);

		/* Initial bootstrap of helpers */
		this.desktop_helper = new DesktopHelper(this.settings, this.main_layout);
		wnck_screen = Wnck.Screen.get_default();
		abomination = new Budgie.Abomination();
		app_system = new Budgie.AppSystem();

		/* Now hook up settings */
		settings.changed.connect(on_settings_changed);

		this.add(main_layout);

		Gtk.drag_dest_set(main_layout, Gtk.DestDefaults.ALL, DesktopHelper.targets, Gdk.DragAction.COPY);
		main_layout.drag_data_received.connect(on_drag_data_received);

		app_system.app_launched.connect((desktop_file) => {
			DesktopAppInfo? info = new DesktopAppInfo.from_filename(desktop_file);
			if (info == null) {
				return;
			}
			if (buttons.contains(info.get_id())) {
				IconButton button = buttons[info.get_id()];
				if (!button.icon.waiting) {
					button.icon.waiting = true;
					button.icon.animate_wait();
				}
			}
		});

		on_settings_changed("grouping");
		on_settings_changed("restrict-to-workspace");
		on_settings_changed("lock-icons");
		on_settings_changed("only-pinned");

		Timeout.add(1000, () => {
			connect_wnck_signals();
			on_active_window_changed(null);
			return false;
		});

		this.get_style_context().add_class("icon-tasklist");
		this.show_all();
	}

	private void startup() {
		string[] pinned = settings.get_strv("pinned-launchers");

		foreach (string launcher in pinned) {
			DesktopAppInfo? info = new DesktopAppInfo(launcher);
			if (info == null) {
				continue;
			}
			IconButton button = new IconButton(this.abomination, this.app_system, this.settings, this.desktop_helper, this.manager, info, true);
			button.update();
			ButtonWrapper wrapper = new ButtonWrapper(button);
			wrapper.orient = this.get_orientation();

			buttons.insert(launcher, button);

			button.became_empty.connect(() => {
				if (!button.pinned) {
					buttons.remove(launcher);
					wrapper.gracefully_die();
				}
			});

			main_layout.add(wrapper);
			this.show_all();
			wrapper.set_reveal_child(true);
		}
	}

	private void connect_wnck_signals() {
		wnck_screen.active_window_changed.connect_after(on_active_window_changed);
		wnck_screen.active_workspace_changed.connect_after(update_buttons);

		this.abomination.added_group.connect(on_class_group_opened);
		this.abomination.removed_group.connect(on_class_group_closed);

		this.abomination.added_app.connect((group, app) => {
			on_window_opened(app);
		});

		this.abomination.removed_app.connect((group, app) => {
			on_window_closed(app.window);
		});
	}

	private void rebuild_items() {
		foreach (Gtk.Widget widget in this.main_layout.get_children()) {
			widget.destroy();
		}

		buttons.remove_all();

		startup();

		abomination.running_apps_id.foreach((id, app) => { // For each running app
			if (grouping) {
				on_class_group_opened(app.group);
			} else {
				on_window_opened(app);
			}
		});
	}

	private void on_settings_changed(string key) {
		switch (key) {
			case "grouping":
				this.grouping = settings.get_boolean(key);
				Idle.add(() => {
					rebuild_items();
					return false;
				});
				break;
			case "lock-icons":
				this.desktop_helper.lock_icons = settings.get_boolean(key);
				break;
			case "restrict-to-workspace":
				this.restrict_to_workspace = settings.get_boolean(key);
				break;
			case "only-pinned":
				this.only_show_pinned = settings.get_boolean(key);
				break;
		}
		if (key != "grouping") {
			update_buttons();
		}
	}

	private void update_buttons() {
		buttons.foreach((id, button) => {
			bool visible = true;

			if (this.restrict_to_workspace) { // Only show apps on this workspace
				var workspace = this.wnck_screen.get_active_workspace();
				if (workspace == null) {
					return;
				}

				visible = button.has_window_on_workspace(workspace); // Set if the button is pinned and on workspace
			}

			if (this.only_show_pinned) {
				visible = button.is_pinned();
			}

			visible = visible || button.is_pinned();

			((ButtonWrapper) button.get_parent()).orient = this.get_orientation();
			((Gtk.Revealer) button.get_parent()).set_reveal_child(visible);
			button.update();
		});
	}

	private void on_drag_data_received(Gtk.Widget widget, Gdk.DragContext context, int x, int y, Gtk.SelectionData selection_data, uint item, uint time) {
		if (item != 0) {
			message("Invalid target type");
			return;
		}

		// id of app that is currently being dragged
		var app_id = (string)selection_data.get_data();
		ButtonWrapper? original_button = null;

		if (app_id.has_prefix("x-nautilus-desktop")) { // Someone is trying to drag a desktop icon to an application tasklist for some reason
			return;
		}

		if (app_id.has_prefix("file://")) {
			app_id = app_id.split("://")[1];
			app_id = app_id.strip();

			DesktopAppInfo? info = new DesktopAppInfo.from_filename(app_id);
			if (info == null) {
				return;
			}

			app_id = info.get_filename();

			if (buttons.contains(app_id)) {
				original_button = (buttons[app_id].get_parent() as ButtonWrapper);
			} else {
				IconButton button = new IconButton(this.abomination, this.app_system, this.settings, this.desktop_helper, this.manager, info, true);
				button.update();

				buttons.set(app_id, button);
				original_button = new ButtonWrapper(button);
				original_button.orient = this.get_orientation();

				button.became_empty.connect(() => {
					if (!button.pinned) {
						buttons.remove(app_id);
						original_button.gracefully_die();
					}
				});
				main_layout.pack_start(original_button, false, false, 0);
			}
		} else { // Doesn't start with file://
			unowned IconButton? button = null;

			if (buttons.contains(app_id)) { // If buttons contains this app_id
				button = buttons.get(app_id);
			} else if (id_map.contains(app_id)) { // id_map contains the app
				button = buttons.get(id_map.get(app_id));
			}

			if (button != null) {
				original_button = button.get_parent() as ButtonWrapper;
			}
		}

		if (original_button == null) {
			return;
		}

		// Iterate through launchers
		foreach (Gtk.Widget widget1 in main_layout.get_children()) {
			ButtonWrapper current_button = (widget1 as ButtonWrapper);

			Gtk.Allocation alloc;

			current_button.get_allocation(out alloc);

			if ((get_orientation() == Gtk.Orientation.HORIZONTAL && x <= (alloc.x + (alloc.width / 2))) ||
				(get_orientation() == Gtk.Orientation.VERTICAL && y <= (alloc.y + (alloc.height / 2)))) {
				int new_position, old_position;
				main_layout.child_get(original_button, "position", out old_position, null);
				main_layout.child_get(current_button, "position", out new_position, null);

				if (new_position == old_position) {
					break;
				}

				if (new_position == old_position + 1) {
					break;
				}

				if (new_position > old_position) {
					new_position = new_position - 1;
				}

				main_layout.reorder_child(original_button, new_position);
				break;
			}

			if ((get_orientation() == Gtk.Orientation.HORIZONTAL && x <= (alloc.x + alloc.width)) ||
				(get_orientation() == Gtk.Orientation.VERTICAL && y <= (alloc.y + alloc.height))) {
				int new_position, old_position;
				main_layout.child_get(original_button, "position", out old_position, null);
				main_layout.child_get(current_button, "position", out new_position, null);

				if (new_position == old_position) {
					break;
				}

				if (new_position == old_position - 1) {
					break;
				}

				if (new_position < old_position) {
					new_position = new_position + 1;
				}

				main_layout.reorder_child(original_button, new_position);
				break;
			}
		}
		original_button.set_transition_type(Gtk.RevealerTransitionType.NONE);
		original_button.set_reveal_child(true);

		this.desktop_helper.update_pinned();

		Gtk.drag_finish(context, true, true, time);
	}

	private void on_class_group_opened(string group_name) {
		if (!grouping) {
			return;
		}

		Array<Budgie.AbominationRunningApp> running_apps = abomination.running_apps.get(group_name);

		if (running_apps == null) {
			return;
		}

		Budgie.AbominationRunningApp first_app = running_apps.index(0);

		if (first_app == null) {
			return;
		}

		if ((first_app.window != null) && (first_app.window.get_state() == Wnck.WindowState.SKIP_TASKLIST)) {
			return;
		}

		DesktopAppInfo? app_info = first_app.app;

		string app_id = (app_info == null) ? "%s".printf(group_name) : app_info.get_id();
		app_id = app_id.strip();

		id_map.insert(group_name, app_id);

		if (buttons.contains(group_name)) {
			buttons.get(app_id).set_class_group(first_app.group_object);
			buttons.get(app_id).update();
			return;
		}

		IconButton button = new IconButton.from_group(this.abomination, this.app_system, this.settings, this.desktop_helper, this.manager, first_app.group_object, app_info);
		ButtonWrapper wrapper = new ButtonWrapper(button);
		wrapper.orient = this.get_orientation();

		button.became_empty.connect(() => {
			if (!button.pinned) {
				if (wrapper != null) {
					wrapper.gracefully_die();
				}

				id_map.remove(group_name);
				buttons.remove(app_id);
			}
		});

		buttons.insert(app_id, button);

		main_layout.add(wrapper);
		this.show_all();
		((Gtk.Revealer) wrapper).set_reveal_child(false); // Default to not showing
		this.update_buttons(); // Ensure we update the buttons at launch, determines immediately whether to show
	}

	private void on_class_group_closed(string class_name) {
		if (!grouping) {
			return;
		}

		string? app_id = id_map.get(class_name);
		app_id = (app_id == null) ? "%s".printf(class_name) : app_id;

		IconButton? button = buttons.get(app_id);

		if (button == null) {
			return;
		}

		if (button.is_pinned()) {
			button.set_class_group(null);
			button.update();
			return;
		}

		ButtonWrapper wrapper = (ButtonWrapper)button.get_parent();

		if (wrapper != null) {
			main_layout.remove(wrapper);
			wrapper.gracefully_die();
		}

		id_map.remove(class_name);
		buttons.remove(app_id);
	}

	/**
	 * on_window_opened handles when we open a new app / window
	 */
	private void on_window_opened(Budgie.AbominationRunningApp app) {
		id_map.insert("%lu".printf(app.id), "%s|%lu".printf(app.group, app.id));

		IconButton? button = buttons.get(app.group);
		if (button == null) {
			button = buttons.get("%s|%lu".printf(app.group, app.id));
		}

		if (button != null && button.is_empty()) {
			if (!grouping) {
				button.set_wnck_window(app.window);
			} else {
				button.set_class_group(app.group_object);
			}
			button.update();
			return;
		}

		if (grouping) {
			if (button == null) {
				on_class_group_opened(app.group);
			}
			return;
		}

		button = new IconButton.from_window(this.abomination, this.app_system, this.settings, this.desktop_helper, this.manager, app.window, app.app, false);
		ButtonWrapper wrapper = new ButtonWrapper(button);
		wrapper.orient = this.get_orientation();

		buttons.insert("%s|%lu".printf(app.group, app.id), button);

		button.became_empty.connect(() => {
			if (!button.pinned) {
				buttons.remove("%s|%lu".printf(app.group, app.id));
				wrapper.gracefully_die();
			}
		});

		main_layout.add(wrapper);
		this.show_all();
		wrapper.set_reveal_child(false);
		this.update_buttons(); // Ensure we update the buttons at launch, determines immediately whether to show
	}

	private void on_window_closed(Wnck.Window window) {
		if (window.is_skip_pager() || window.is_skip_tasklist()) {
			return;
		}

		ulong win_id = window.get_xid();
		string? app_id = id_map.get("%lu".printf(win_id));
		app_id = (app_id == null) ? "%lu".printf(win_id) : app_id;
		IconButton? button = buttons.get(app_id);
		if (button != null) {
			button.set_wnck_window(null);
			button.update();
		} else {
			app_id = app_id.split("|")[0];
			button = buttons.get(app_id);
		}

		if (grouping) {
			return;
		}

		if (button.is_pinned()) {
			button.set_wnck_window(null);
			button.update();
			return;
		}
	}

	private void on_active_window_changed(Wnck.Window? previous_window) {
		foreach (IconButton button in buttons.get_values()) {
			if (button.has_window(this.desktop_helper.get_active_window())) {
				button.last_active_window = this.desktop_helper.get_active_window();
				button.attention(false);
			}
			button.update();
		}
	}


	void set_icons_size() {
		Wnck.set_default_icon_size(this.desktop_helper.icon_size);

		Idle.add(() => {
			buttons.foreach((id, button) => {
				button.update_icon();
			});
			return false;
		});

		queue_resize();
		queue_draw();
	}

	/**
	 * Our panel has moved somewhere, stash the positions
	 */
	public override void panel_position_changed(Budgie.PanelPosition position) {
		this.desktop_helper.panel_position = position;
		this.desktop_helper.orientation = this.get_orientation();
		main_layout.set_orientation(this.desktop_helper.orientation);

		set_icons_size();
	}

	/**
	 * Our panel has changed size, record the new icon sizes
	 */
	public override void panel_size_changed(int panel, int icon, int small_icon) {
		this.desktop_helper.icon_size = small_icon;
		this.desktop_helper.panel_size = panel;
		set_icons_size();
	}

	public override void update_popovers(Budgie.PopoverManager? manager) {
		this.manager = manager;
	}

	/**
	 * Return our orientation in relation to the panel position
	 */
	private Gtk.Orientation get_orientation() {
		switch (this.desktop_helper.panel_position) {
			case Budgie.PanelPosition.TOP:
			case Budgie.PanelPosition.BOTTOM:
				return Gtk.Orientation.HORIZONTAL;
			default:
				return Gtk.Orientation.VERTICAL;
		}
	}
}

[ModuleInit]
public void peas_register_types(TypeModule module) {
	// boilerplate - all modules need this
	var objmodule = module as Peas.ObjectModule;
	objmodule.register_extension_type(typeof(Budgie.Plugin), typeof(IconTasklist));
}
