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

using LibUUID;

namespace Budgie {
	/**
	* The main panel area - i.e. the bit that's rendered
	*/
	public class MainPanel : Gtk.Box {
		public MainPanel() {
			Object(orientation: Gtk.Orientation.HORIZONTAL);
			get_style_context().add_class("budgie-panel");
			get_style_context().add_class(Gtk.STYLE_CLASS_BACKGROUND);
		}

		public void set_transparent(bool transparent) {
			if (transparent) {
				get_style_context().add_class("transparent");
			} else {
				get_style_context().remove_class("transparent");
			}
		}

		public void set_dock_mode(bool dock_mode) {
			if (dock_mode) {
				get_style_context().add_class("dock-mode");
			} else {
				get_style_context().remove_class("dock-mode");
			}
		}
	}

	/**
	* This is used to track panel animations, i.e. within the toplevel
	* itself to provide dock like behavior
	*/
	public enum PanelAnimation {
		NONE = 0,
		SHOW,
		HIDE
	}

	/**
	* The toplevel window for a panel
	*/
	public class Panel : Budgie.Toplevel {
		MainPanel layout;
		Gtk.Box main_layout;
		Gdk.Rectangle orig_scr;

		public Settings settings { construct set ; public get; }
		private unowned Budgie.PanelManager? manager;

		PopoverManager? popover_manager;

		Budgie.ShadowBlock shadow;

		HashTable<string,HashTable<string,string>> pending = null;
		HashTable<string,HashTable<string,string>> creating = null;
		HashTable<string,Budgie.AppletInfo?> applets = null;

		HashTable<string,Budgie.AppletInfo?> initial_config = null;

		List<string?> expected_uuids;

		construct {
			position = PanelPosition.NONE;
		}

		/* Multiplier for strut operations on hi-dpi */
		int scale = 1;

		/* Box for the start of the panel */
		Gtk.Box? start_box;
		/* Box for the center of the panel */
		Gtk.Box? center_box;
		/* Box for the end of the panel */
		Gtk.Box? end_box;

		int[] icon_sizes = {
			16, 24, 32, 48, 96, 128, 256
		};

		int current_icon_size;
		int current_small_icon_size;

		/* Track initial load */
		private bool is_fully_loaded = false;
		private bool need_migratory = false;

		public signal void panel_loaded();

		/* Animation tracking */
		private double render_scale = 0.0;
		private PanelAnimation animation = PanelAnimation.SHOW;
		private bool allow_animation = false;
		private bool screen_occluded = false;

		public double nscale {
			public set {
				render_scale = value;
				queue_draw();
			}
			public get {
				return render_scale;
			}
		}

		public bool activate_action(int remote_action) {
			unowned string? uuid = null;
			unowned Budgie.AppletInfo? info = null;

			Budgie.PanelAction action = (Budgie.PanelAction)remote_action;

			var iter = HashTableIter<string?,Budgie.AppletInfo?>(applets);
			while (iter.next(out uuid, out info)) {
				if ((info.applet.supported_actions & action) != 0) {
					this.present();

					Idle.add(() => {
						info.applet.invoke_action(action);
						return false;
					});
					return true;
				}
			}
			return false;
		}

		/**
		* Force update the geometry
		*/
		public void update_geometry(Gdk.Rectangle screen, PanelPosition position, int size = 0) {
			this.orig_scr = screen;
			string old_class = Budgie.position_class_name(this.position);

			if (old_class != "") {
				this.get_style_context().remove_class(old_class);
			}

			if (size == 0) {
				size = intended_size;
			}

			this.settings.set_int(Budgie.PANEL_KEY_SIZE, size);
			this.intended_size = size;
			this.get_style_context().add_class(Budgie.position_class_name(position));

			// Check if the position has been altered and notify our applets
			if (position != this.position) {
				this.position = position;
				this.settings.set_enum(Budgie.PANEL_KEY_POSITION, position);
				this.update_positions();
			}

			this.shadow.position = position;
			this.layout.queue_resize();
			queue_resize();
			queue_draw();
			placement();
			update_sizes();
		}

		public void update_transparency(PanelTransparency transparency) {
			this.transparency = transparency;

			switch (transparency) {
				case PanelTransparency.ALWAYS:
					set_transparent(true);
					break;
				case PanelTransparency.DYNAMIC:
					manager.check_windows();
					break;
				default:
					set_transparent(false);
					break;
			}

			this.settings.set_enum(Budgie.PANEL_KEY_TRANSPARENCY, transparency);
		}

		public void set_transparent(bool transparent) {
			layout.set_transparent(transparent);
			this.update_dock_behavior();
		}

		/**
		* Specific for docks, regardless of transparency, and determines
		* how our "screen blocked by thingy" policy works.
		*/
		public void set_occluded(bool occluded) {
			this.screen_occluded = occluded;
			if (this.autohide == AutohidePolicy.NONE) {
				return;
			}
			this.update_dock_behavior();
		}

		public override List<AppletInfo?> get_applets() {
			List<Budgie.AppletInfo?> ret = new List<Budgie.AppletInfo?>();
			unowned string? key = null;
			unowned Budgie.AppletInfo? appl_info = null;

			var iter = HashTableIter<string,Budgie.AppletInfo?>(applets);
			while (iter.next(out key, out appl_info)) {
				ret.append(appl_info);
			}
			return ret;
		}

		/**
		* Loop the applets, performing a reparent or reposition
		*/
		private void initial_applet_placement(bool repar = false, bool repos = false) {
			if (!repar && !repos) {
				return;
			}
			unowned string? uuid = null;
			unowned Budgie.AppletInfo? info = null;

			var iter = HashTableIter<string?,Budgie.AppletInfo?>(applets);

			while (iter.next(out uuid, out info)) {
				if (repar) {
					applet_reparent(info);
				}
				if (repos) {
					applet_reposition(info);
				}
			}
		}

		/* Handle being "fully" loaded */
		private void on_fully_loaded() {
			if (applets.size() < 1) {
				if (!initial_anim) {
					Idle.add(initial_animation);
				}
				return;
			}

			/* All applets loaded and positioned, now re-sort them */
			initial_applet_placement(true, false);
			initial_applet_placement(false, true);

			/* Let everyone else know we're in business */
			applets_changed();
			if (!initial_anim) {
				Idle.add(initial_animation);
			}
			lock (need_migratory) {
				if (!need_migratory) {
					return;
				}
			}
			/* In half a second, add_migratory so the user sees them added */
			Timeout.add(500, add_migratory);
		}

		public Panel(Budgie.PanelManager? manager, string? uuid, Settings? settings) {
			Object(type_hint: Gdk.WindowTypeHint.DOCK, window_position: Gtk.WindowPosition.NONE, settings: settings, uuid: uuid);

			initial_config = new HashTable<string,Budgie.AppletInfo>(str_hash, str_equal);

			intended_size = settings.get_int(Budgie.PANEL_KEY_SIZE);
			this.manager = manager;

			skip_taskbar_hint = true;
			skip_pager_hint = true;
			set_decorated(false);

			scale = get_scale_factor();
			nscale = 1.0;

			// Respond to a scale factor change
			notify["scale-factor"].connect(() => {
				this.scale = get_scale_factor();
				this.placement();
			});
			// Handle intelligent dock behavior
			notify["intersected"].connect(() => {
				if (this.autohide != AutohidePolicy.NONE) {
					this.update_dock_behavior();
				}
			});

			popover_manager = new PopoverManager();
			pending = new HashTable<string,HashTable<string,string>>(str_hash, str_equal);
			creating = new HashTable<string,HashTable<string,string>>(str_hash, str_equal);
			applets = new HashTable<string,Budgie.AppletInfo?>(str_hash, str_equal);
			expected_uuids = new List<string?>();
			panel_loaded.connect(on_fully_loaded);

			var vis = screen.get_rgba_visual();
			if (vis == null) {
				warning("Compositing not available, things will Look Bad (TM)");
			} else {
				set_visual(vis);
			}
			resizable = false;
			app_paintable = true;
			get_style_context().add_class("budgie-container");

			main_layout = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
			add(main_layout);

			layout = new MainPanel();
			layout.valign = Gtk.Align.FILL;
			layout.halign = Gtk.Align.FILL;

			main_layout.pack_start(layout, true, true, 0);
			main_layout.valign = Gtk.Align.START;

			/* Shadow.. */
			shadow = new Budgie.ShadowBlock(this.position);
			shadow.hexpand = false;
			shadow.halign = Gtk.Align.FILL;
			shadow.show_all();
			main_layout.pack_start(shadow, false, false, 0);

			this.settings.bind(Budgie.PANEL_KEY_SHADOW, shadow, "active", SettingsBindFlags.GET);
			this.settings.bind(Budgie.PANEL_KEY_DOCK_MODE, this, "dock-mode", SettingsBindFlags.DEFAULT);

			this.notify["dock-mode"].connect(this.update_dock_mode);
			layout.set_dock_mode(this.dock_mode);

			shadow_visible = this.settings.get_boolean(Budgie.PANEL_KEY_SHADOW);
			this.settings.bind(Budgie.PANEL_KEY_SHADOW, this, "shadow-visible", SettingsBindFlags.DEFAULT);

			/* Assign our applet holder boxes */
			start_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 2);
			start_box.halign = Gtk.Align.START;
			layout.pack_start(start_box, false, false, 0);
			center_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 2);
			layout.set_center_widget(center_box);
			end_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 2);
			layout.pack_end(end_box, false, false, 0);
			end_box.halign = Gtk.Align.END;

			this.theme_regions = this.settings.get_boolean(Budgie.PANEL_KEY_REGIONS);
			this.notify["theme-regions"].connect(update_theme_regions);
			this.settings.bind(Budgie.PANEL_KEY_REGIONS, this, "theme-regions", SettingsBindFlags.DEFAULT);
			this.update_theme_regions();

			this.size_allocate.connect_after(this.do_size_allocate);
			this.enter_notify_event.connect(on_enter_notify);
			this.leave_notify_event.connect(on_leave_notify);

			get_child().show_all();

			// Immediately hide our inner boxes
			start_box.hide();
			center_box.hide();
			end_box.hide();

			this.manager.extension_loaded.connect_after(this.on_extension_loaded);

			/* bit of a no-op. */
			update_sizes();
			load_applets();
		}

		void do_size_allocate() {
			this.update_screen_edge();
		}

		void update_theme_regions() {
			if (this.theme_regions) {
				start_box.get_style_context().add_class("start-region");
				center_box.get_style_context().add_class("center-region");
				end_box.get_style_context().add_class("end-region");
			} else {
				start_box.get_style_context().remove_class("start-region");
				center_box.get_style_context().remove_class("center-region");
				end_box.get_style_context().remove_class("end-region");
			}
			this.queue_draw();
		}

		void update_sizes() {
			int size = icon_sizes[0];
			int small_size = icon_sizes[0];

			unowned string? key = null;
			unowned Budgie.AppletInfo? info = null;

			for (int i = 1; i < icon_sizes.length; i++) {
				if (icon_sizes[i] > intended_size) {
					break;
				}
				size = icon_sizes[i];
				small_size = icon_sizes[i-1];
			}

			this.current_icon_size = size;
			this.current_small_icon_size = small_size;

			var iter = HashTableIter<string?,Budgie.AppletInfo?>(applets);
			while (iter.next(out key, out info)) {
				info.applet.panel_size_changed(intended_size, size, small_size);
			}
		}

		void update_positions() {
			unowned string? key = null;
			unowned Budgie.AppletInfo? info = null;

			var iter = HashTableIter<string?,Budgie.AppletInfo?>(applets);
			while (iter.next(out key, out info)) {
				info.applet.panel_position_changed(this.position);
			}
		}

		public void destroy_children() {
			unowned string key;
			unowned AppletInfo? info;

			var iter = HashTableIter<string?,AppletInfo?>(applets);
			while (iter.next(out key, out info)) {
				Settings? app_settings = info.applet.get_applet_settings(info.uuid);
				if (app_settings != null) {
					app_settings.ref();
				}

				// Stop it screaming when it dies
				ulong notify_id = info.get_data("notify_id");

				SignalHandler.disconnect(info, notify_id);
				info.applet.get_parent().remove(info.applet);

				// Clean up the settings
				this.manager.reset_dconf_path(info.settings);

				// Nuke it's own settings
				if (app_settings != null) {
					this.manager.reset_dconf_path(app_settings);
				}
			}
		}

		void on_extension_loaded(string name) {
			unowned HashTable<string,string>? todo = null;
			todo = pending.lookup(name);
			if (todo != null) {
				var iter = HashTableIter<string,string>(todo);
				string? uuid = null;

				while (iter.next(out uuid, null)) {
					string? uname = null;
					Budgie.AppletInfo? info = this.manager.load_applet_instance(uuid, out uname);
					if (info == null) {
						critical("Failed to load applet when we know it exists: %s", uname);
						return;
					}
					this.add_applet(info);
				}
				pending.remove(name);
			}

			todo = null;

			todo = creating.lookup(name);
			if (todo != null) {
				var iter = HashTableIter<string,string>(todo);
				string? uuid = null;

				while (iter.next(out uuid, null)) {
					Budgie.AppletInfo? info = this.manager.create_new_applet(name, uuid);
					if (info == null) {
						critical("Failed to load applet when we know it exists");
						return;
					}
					this.add_applet(info);
					/* this.configure_applet(info); */
				}
				creating.remove(name);
			}
		}

		/**
		* Load all pre-configured applets
		*/
		void load_applets() {
			string[]? applets = settings.get_strv(Budgie.PANEL_KEY_APPLETS);
			if (applets == null || applets.length == 0) {
				this.panel_loaded();
				this.is_fully_loaded = true;
				return;
			}

			/* Two loops so we can track when we've fully loaded the panel */
			lock (expected_uuids) {
				for (int i = 0; i < applets.length; i++) {
					this.expected_uuids.append(applets[i]);
				}

				for (int i = 0; i < applets.length; i++) {
					string? name = null;
					Budgie.AppletInfo? info = this.manager.load_applet_instance(applets[i], out name);

					if (info == null) {
						/* Faiiiil */
						if (name == null) {
							unowned List<string?> g = expected_uuids.find_custom(applets[i], strcmp);
							/* TODO: No longer expecting this guy to load */
							if (g != null) {
								expected_uuids.remove_link(g);
							}
							message("Unable to load invalid applet: %s", applets[i]);
							continue;
						}
						this.add_pending(applets[i], name);
						manager.modprobe(name);
					} else {
						/* um add this bro to the panel :o */
						this.add_applet(info);
					}
				}
			}
		}

		/**
		* Add a new applet to the panel (Raven UI)
		*
		* Explanation: Try to find the most underpopulated region first,
		* and add the applet there. Determine a suitable position,
		* set the alignment+position, stuff an initial config in,
		* and hope for the best when we initiate add_new
		*
		* If the @target_region is set, we'll use that instead
		*/
		private void add_new_applet_at(string id, Gtk.Box? target_region) {
			/* First, determine a panel to place this guy */
			int position = (int) applets.size() + 1;
			unowned Gtk.Box? target = null;
			string? align = null;
			AppletInfo? info = null;
			string? uuid = null;

			Gtk.Box?[] regions = {
				start_box,
				center_box,
				end_box
			};

			/* Use the requested target_region for internal migration adds */
			if (target_region != null) {
				var kids = target_region.get_children();
				position = (int) (kids.length());
				target = target_region;
			} else {
				/* No region specified, find the first available slot */
				foreach (var region in regions) {
					var kids = region.get_children();
					var len = kids.length();
					if (len < position) {
						position = (int)len;
						target = region;
					}
				}
			}

			if (target == start_box) {
				align = "start";
			} else if (target == center_box) {
				align = "center";
			} else {
				align = "end";
			}

			uuid = LibUUID.new(UUIDFlags.LOWER_CASE|UUIDFlags.TIME_SAFE_TYPE);
			info = new AppletInfo.from_uuid(uuid);
			info.alignment = align;

			/* Safety clamp */
			var kids = target.get_children();
			uint nkids = kids.length();

			if (position >= nkids) {
				position = (int) nkids;
			}

			if (position < 0) {
				position = 0;
			}

			info.position = position;

			initial_config.insert(uuid, info);
			add_new(id, uuid);
		}

		/**
		* Add a new applet to the panel (Raven UI)
		*/
		public override void add_new_applet(string id) {
			add_new_applet_at(id, null);
		}

		public void create_default_layout(string name, KeyFile config) {
			int s_index = -1;
			int c_index = -1;
			int e_index = -1;
			int index = 0;

			try {
				if (!config.has_key(name, "Children")) {
					warning("Config for panel %s does not specify applets", name);
					return;
				}
				string[] applets = config.get_string_list(name, "Children");
				foreach (string appl in applets) {
					AppletInfo? info = null;
					string? uuid = null;
					appl = appl.strip();
					string alignment = "start"; /* center, end */

					if (!config.has_group(appl)) {
						warning("Panel applet %s missing from config", appl);
						continue;
					}

					if (!config.has_key(appl, "ID")) {
						warning("Applet %s is missing ID", appl);
						continue;
					}

					uuid = LibUUID.new(UUIDFlags.LOWER_CASE|UUIDFlags.TIME_SAFE_TYPE);

					var id = config.get_string(appl, "ID").strip();
					if (uuid == null || uuid.strip() == "") {
						warning("Could not add new applet %s from config %s", id, name);
						continue;
					}

					info = new AppletInfo.from_uuid(uuid);
					if (config.has_key(appl, "Alignment")) {
						alignment = config.get_string(appl, "Alignment").strip();
					}

					switch (alignment) {
						case "center":
							index = ++c_index;
							break;
						case "end":
							index = ++e_index;
							break;
						default:
							index = ++s_index;
							break;
					}
					info.alignment = alignment;
					info.position = index;

					initial_config.insert(uuid, info);
					add_new(id, uuid);
				}
			} catch (Error e) {
				warning("Error loading default config: %s", e.message);
			}
		}

		// toggle_container_visibilities is used to toggle the visibility of a panel container (start, center, end) based on if it has children
		void toggle_container_visibilities() {
			Gtk.Box?[] regions = { start_box, center_box, end_box };

			for (var i = 0; i < regions.length; i++) {
				Gtk.Box region = regions[i];

				if (region.get_children().length() > 0) { // If this has a child
					if (!region.get_visible()) { // Not already visible
						region.show(); // Ensure we show the panel specifically. Using show_all results in hidden widgets of children being shown, like Budgie Menu label. Only happens in weird cases like reset.
						region.queue_draw(); // Ensure we queue a draw
					}
				} else {
					region.hide(); // Hide this area
				}
			}
		}

		void set_applets() {
			string[]? uuids = null;
			unowned string? uuid = null;
			unowned Budgie.AppletInfo? plugin = null;

			var iter = HashTableIter<string,Budgie.AppletInfo?>(applets);
			while (iter.next(out uuid, out plugin)) {
				uuids += uuid;
			}

			settings.set_strv(Budgie.PANEL_KEY_APPLETS, uuids);
		}

		public override void remove_applet(Budgie.AppletInfo? info) {
			if (info == null) {
				return;
			}

			int position = info.position;
			string alignment = info.alignment;
			string uuid = info.uuid;

			ulong notify_id = info.get_data("notify_id");

			SignalHandler.disconnect(info, notify_id);
			Gtk.Box applet_parent = (Gtk.Box) info.applet.get_parent();
			applet_parent.remove(info.applet);
			toggle_container_visibilities();

			Settings? app_settings = info.applet.get_applet_settings(uuid);
			if (app_settings != null) {
				app_settings.ref();
			}

			this.manager.reset_dconf_path(info.settings);

			/* TODO: Add refcounting and unload unused plugins. */
			applets.remove(uuid);
			applet_removed(uuid);

			if (app_settings != null) {
				this.manager.reset_dconf_path(app_settings);
			}

			set_applets();
			budge_em_left(alignment, position);
		}

		void add_applet(Budgie.AppletInfo? info) {
			unowned Gtk.Box? pack_target = null;
			Budgie.AppletInfo? initial_info = null;

			initial_info = initial_config.lookup(info.uuid);
			if (initial_info != null) {
				info.alignment = initial_info.alignment;
				info.position = initial_info.position;
				initial_config.remove(info.uuid);
			}

			if (!this.is_fully_loaded) {
				lock (expected_uuids) {
					unowned List<string?> exp_fin = expected_uuids.find_custom(info.uuid, strcmp);
					if (exp_fin != null) {
						expected_uuids.remove_link(exp_fin);
					}
				}
			}

			/* figure out the alignment */
			switch (info.alignment) {
				case "start":
					pack_target = start_box;
					break;
				case "end":
					pack_target = end_box;
					break;
				default:
					pack_target = center_box;
					break;
			}

			this.applets.insert(info.uuid, info);
			this.set_applets();

			info.applet.update_popovers(this.popover_manager);
			info.applet.panel_size_changed(intended_size, this.current_icon_size, this.current_small_icon_size);
			info.applet.panel_position_changed(this.position);
			pack_target.pack_start(info.applet, false, false, 0);

			pack_target.child_set(info.applet, "position", info.position);
			toggle_container_visibilities(); // Ensure container is updated

			ulong id = info.notify.connect(applet_updated);
			info.set_data("notify_id", id);
			this.applet_added(info);

			if (this.is_fully_loaded) {
				return;
			}

			lock (expected_uuids) {
				if (expected_uuids.length() == 0) {
					this.is_fully_loaded = true;
					this.panel_loaded();
				}
			}
		}

		void applet_reparent(Budgie.AppletInfo? info) {
			/* Handle being reparented. */
			unowned Gtk.Box? new_parent = null;
			switch (info.alignment) {
				case "start":
					new_parent = this.start_box;
					break;
				case "end":
					new_parent = this.end_box;
					break;
				default:
					new_parent = this.center_box;
					break;
			}
			/* Don't needlessly reparent */
			Gtk.Box current_parent = (Gtk.Box) info.applet.get_parent();
			if (new_parent != current_parent) {
				current_parent.remove(info.applet);
				new_parent.add(info.applet);

				toggle_container_visibilities(); // Update the containers

				info.applet.queue_resize();
			}
		}

		void applet_reposition(Budgie.AppletInfo? info) {
			info.applet.get_parent().child_set(info.applet, "position", info.position);
			toggle_container_visibilities(); // Update the containers
		}

		void applet_updated(Object o, ParamSpec p) {
			unowned AppletInfo? info = o as AppletInfo;

			/* Prevent a massive amount of resorting */
			if (!this.is_fully_loaded) {
				return;
			}

			if (p.name == "alignment") {
				applet_reparent(info);
			} else if (p.name == "position") {
				applet_reposition(info);
			}
			this.applets_changed();
		}

		void add_new(string plugin_name, string? initial_uuid = null) {
			string? uuid = null;
			unowned HashTable<string,string>? table = null;

			if (!this.manager.is_extension_valid(plugin_name)) {
				warning("Not loading invalid plugin: %s", plugin_name);
				return;
			}
			if (initial_uuid == null) {
				uuid = LibUUID.new(UUIDFlags.LOWER_CASE|UUIDFlags.TIME_SAFE_TYPE);
			} else {
				uuid = initial_uuid;
			}

			if (!this.manager.is_extension_loaded(plugin_name)) {
				/* Request a load of the new guy */
				table = creating.lookup(plugin_name);
				if (table != null) {
					if (!table.contains(uuid)) {
						table.insert(uuid, uuid);
					}
					return;
				}
				/* Looks insane but avoids copies */
				creating.insert(plugin_name, new HashTable<string,string>(str_hash, str_equal));
				table = creating.lookup(plugin_name);
				table.insert(uuid, uuid);
				this.manager.modprobe(plugin_name);
				return;
			}
			/* Already exists */
			Budgie.AppletInfo? info = this.manager.create_new_applet(plugin_name, uuid);
			if (info == null) {
				critical("Failed to load applet when we know it exists");
				return;
			}
			this.add_applet(info);
			return;
		}

		void add_pending(string uuid, string plugin_name) {
			string? rname = null;
			unowned HashTable<string,string>? table = null;

			if (!this.manager.is_extension_valid(plugin_name)) {
				warning("Not adding invalid plugin: %s %s", plugin_name, uuid);
				return;
			}

			if (!this.manager.is_extension_loaded(plugin_name)) {
				/* Request a load of the new guy */
				table = pending.lookup(plugin_name);
				if (table != null) {
					if (!table.contains(uuid)) {
						table.insert(uuid, uuid);
					}
					return;
				}
				/* Looks insane but avoids copies */
				pending.insert(plugin_name, new HashTable<string,string>(str_hash, str_equal));
				table = pending.lookup(plugin_name);
				table.insert(uuid, uuid);
				this.manager.modprobe(plugin_name);
				return;
			}

			/* Already exists */
			Budgie.AppletInfo? info = this.manager.load_applet_instance(uuid, out rname);
			if (info == null) {
				critical("Failed to load applet when we know it exists");
				return;
			}
			this.add_applet(info);
		}

		public override void map() {
			base.map();
			placement();
		}

		public void set_autohide_policy(AutohidePolicy policy) {
			if (policy != this.autohide) {
				this.settings.set_enum(Budgie.PANEL_KEY_AUTOHIDE, policy);
				this.autohide = policy;
				this.apply_strut_policy();
				this.update_dock_behavior();
			}
		}

		/**
		* Update the internal representation of the panel based on whether
		* we're in dock mode or not
		*/
		void update_dock_mode() {
			layout.set_dock_mode(this.dock_mode);
			this.placement();
		}

		int old_width = 0;
		int old_height = 0;

		void update_screen_edge() {
			Gtk.Allocation alloc;
			main_layout.get_allocation(out alloc);
			int x = 0, y = 0;
			int nx = 0, ny = 0;
			int nw = 0, nh = 0;
			this.get_position(out nx, out ny);
			this.get_size(out nw, out nh);

			if (this.dock_mode) {
				switch (position) {
				case Budgie.PanelPosition.TOP:
					x = (orig_scr.x / 2) + (((orig_scr.x + orig_scr.width) / 2) - (alloc.width / 2));
					if (x < orig_scr.x) {
						x = orig_scr.x;
					}
					y = orig_scr.y;
					break;
				case Budgie.PanelPosition.LEFT:
					x = orig_scr.x;
					y = (orig_scr.y / 2) + (((orig_scr.y + orig_scr.height) / 2) - (alloc.height / 2));
					if (y < orig_scr.y) {
						y = orig_scr.y;
					}
					break;
				case Budgie.PanelPosition.RIGHT:
					x = (orig_scr.x + orig_scr.width) - alloc.width;
					y = (orig_scr.y / 2) + (((orig_scr.y + orig_scr.height) / 2) - (alloc.height / 2));
					if (y < orig_scr.y) {
						y = orig_scr.y;
					}
					break;
				case Budgie.PanelPosition.BOTTOM:
				default:
					x = (orig_scr.x / 2) + (((orig_scr.x + orig_scr.width) / 2) - (alloc.width / 2));
					y = orig_scr.y + (orig_scr.height - alloc.height);
					if (x < orig_scr.x) {
						x = orig_scr.x;
					}
					break;
				}
			} else {
				switch (position) {
				case Budgie.PanelPosition.TOP:
					x = orig_scr.x;
					y = orig_scr.y;
					break;
				case Budgie.PanelPosition.LEFT:
					x = orig_scr.x;
					y = orig_scr.y;
					break;
				case Budgie.PanelPosition.RIGHT:
					x = (orig_scr.x + orig_scr.width) - alloc.width;
					y = orig_scr.y;
					break;
				case Budgie.PanelPosition.BOTTOM:
				default:
					x = orig_scr.x;
					y = orig_scr.y + (orig_scr.height - alloc.height);
					break;
				}
			}

			// Don't update input regions unless needed
			if (old_width != nw || old_height != nh) {
				if (get_visible()) {
					this.set_input_region();
				}
				old_width = nw;
				old_height = nh;
			}

			// Don't move if we don't need to.
			if (nx == x && ny == y) {
				return;
			}

			move(x, y);
			this.queue_draw();
		}

		void apply_strut_policy() {
			if (this.autohide != AutohidePolicy.NONE) {
				Budgie.unset_struts(this);
			} else {
				Budgie.set_struts(this, position, intended_size);
			}
		}

		void placement() {
			this.apply_strut_policy();
			bool horizontal = false;
			Gtk.Allocation alloc;
			main_layout.get_allocation(out alloc);

			int width = 0, height = 0;
			int x = 0, y = 0;
			int shadow_position = 0;

			switch (position) {
				case Budgie.PanelPosition.TOP:
					x = orig_scr.x;
					y = orig_scr.y;
					width = orig_scr.width;
					height = intended_size;
					shadow_position = 1;
					horizontal = true;
					break;
				case Budgie.PanelPosition.LEFT:
					x = orig_scr.x;
					y = orig_scr.y;
					width = intended_size;
					height = orig_scr.height;
					shadow_position = 1;
					break;
				case Budgie.PanelPosition.RIGHT:
					x = (orig_scr.x + orig_scr.width) - alloc.width;
					y = orig_scr.y;
					width = intended_size;
					height = orig_scr.height;
					shadow_position = 0;
					break;
				case Budgie.PanelPosition.BOTTOM:
				default:
					x = orig_scr.x;
					y = orig_scr.y + (orig_scr.height - alloc.height);
					width = orig_scr.width;
					height = intended_size;
					shadow_position = 0;
					horizontal = true;
					break;
			}

			// Special considerations for dock mode
			if (this.dock_mode) {
				if (horizontal) {
					if (alloc.width > orig_scr.width) {
						width = orig_scr.width;
					} else {
						width = 100;
					}
				} else {
					if (alloc.height > orig_scr.height) {
						height = orig_scr.height;
					} else {
						height = 100;
					}
				}
			}

			main_layout.child_set(shadow, "position", shadow_position);

			if (horizontal) {
				start_box.halign = Gtk.Align.START;
				center_box.halign = Gtk.Align.CENTER;
				end_box.halign = Gtk.Align.END;

				start_box.valign = Gtk.Align.FILL;
				center_box.valign = Gtk.Align.FILL;
				end_box.valign = Gtk.Align.FILL;

				start_box.set_orientation(Gtk.Orientation.HORIZONTAL);
				center_box.set_orientation(Gtk.Orientation.HORIZONTAL);
				end_box.set_orientation(Gtk.Orientation.HORIZONTAL);
				layout.set_orientation(Gtk.Orientation.HORIZONTAL);

				main_layout.set_orientation(Gtk.Orientation.VERTICAL);
				main_layout.valign = Gtk.Align.FILL;
				if (this.dock_mode) {
					main_layout.halign = Gtk.Align.START;
				} else {
					main_layout.halign = Gtk.Align.FILL;
				}
				main_layout.hexpand = false;
				layout.valign = Gtk.Align.FILL;
			} else {
				start_box.halign = Gtk.Align.FILL;
				center_box.halign = Gtk.Align.FILL;
				end_box.halign = Gtk.Align.FILL;

				start_box.valign = Gtk.Align.START;
				center_box.valign = Gtk.Align.CENTER;
				end_box.valign = Gtk.Align.END;

				start_box.set_orientation(Gtk.Orientation.VERTICAL);
				center_box.set_orientation(Gtk.Orientation.VERTICAL);
				end_box.set_orientation(Gtk.Orientation.VERTICAL);
				layout.set_orientation(Gtk.Orientation.VERTICAL);

				main_layout.set_orientation(Gtk.Orientation.HORIZONTAL);
				if (this.dock_mode) {
					main_layout.valign = Gtk.Align.START;
				} else {
					main_layout.valign = Gtk.Align.FILL;
				}
				main_layout.halign = Gtk.Align.FILL;
				main_layout.hexpand = true;
			}

			layout.set_size_request(width, height);
			set_size_request(width, height);
			this.update_screen_edge();
		}

		private bool applet_at_start_of_region(Budgie.AppletInfo? info) {
			return (info.position == 0);
		}

		private bool applet_at_end_of_region(Budgie.AppletInfo? info) {
			return (info.position >= info.applet.get_parent().get_children().length() - 1);
		}

		private string? get_box_left(Budgie.AppletInfo? info) {
			unowned Gtk.Widget? parent = null;

			if ((parent = info.applet.get_parent()) == end_box) {
				return "center";
			} else if (parent == center_box) {
				return "start";
			} else {
				return null;
			}
		}

		private string? get_box_right(Budgie.AppletInfo? info) {
			unowned Gtk.Widget? parent = null;

			if ((parent = info.applet.get_parent()) == start_box) {
				return "center";
			} else if (parent == center_box) {
				return "end";
			} else {
				return null;
			}
		}

		public override bool can_move_applet_left(Budgie.AppletInfo? info) {
			if (!applet_at_start_of_region(info)) {
				return true;
			}
			if (get_box_left(info) != null) {
				return true;
			}
			return false;
		}

		public override bool can_move_applet_right(Budgie.AppletInfo? info) {
			if (!applet_at_end_of_region(info)) {
				return true;
			}
			if (get_box_right(info) != null) {
				return true;
			}
			return false;
		}

		void conflict_swap(Budgie.AppletInfo? info, int old_position) {
			unowned string key;
			unowned Budgie.AppletInfo? val;
			unowned Budgie.AppletInfo? conflict = null;
			var iter = HashTableIter<string,Budgie.AppletInfo?>(applets);

			while (iter.next(out key, out val)) {
				if (val.alignment == info.alignment && val.position == info.position && info != val) {
					conflict = val;
					break;
				}
			}

			if (conflict == null) {
				return;
			}

			conflict.position = old_position;
		}

		void budge_em_right(string alignment, int after = -1) {
			unowned string key;
			unowned Budgie.AppletInfo? val;
			var iter = HashTableIter<string,Budgie.AppletInfo?>(applets);

			while (iter.next(out key, out val)) {
				if (val.alignment == alignment) {
					if (val.position > after) {
						val.position++;
					}
				}
			}
			this.reinforce_positions();
		}

		void budge_em_left(string alignment, int after) {
			unowned string key;
			unowned Budgie.AppletInfo? val;
			var iter = HashTableIter<string,Budgie.AppletInfo?>(applets);

			while (iter.next(out key, out val)) {
				if (val.alignment == alignment) {
					if (val.position > after) {
						val.position--;
					}
				}
			}
			this.reinforce_positions();
		}

		private void reinforce_positions() {
			unowned string key;
			unowned Budgie.AppletInfo? val;
			var iter = HashTableIter<string,Budgie.AppletInfo?>(applets);

			while (iter.next(out key, out val)) {
				applet_reposition(val);
			}

			/* We may have ugly artifacts now */
			this.queue_draw();
		}

		public override void move_applet_left(Budgie.AppletInfo? info) {
			string? new_home = null;
			int new_position = info.position;
			int old_position = info.position;

			if (!applet_at_start_of_region(info)) {
				new_position--;
				if (new_position < 0) {
					new_position = 0;
				}
				info.position = new_position;
				conflict_swap(info, old_position);
				applets_changed();
				return;
			}
			if ((new_home = get_box_left(info)) != null) {
				unowned Gtk.Box? new_parent = null;
				switch (info.alignment) {
					case "end":
						new_parent = center_box;
						break;
					case "center":
						new_parent = start_box;
						break;
					default:
						new_parent = end_box;
						break;
				}

				string old_home = info.alignment;
				uint len = new_parent.get_children().length();
				info.alignment = new_home;
				info.position = (int)len;
				budge_em_left(old_home, 0);
				applets_changed();
			}
		}

		public override void move_applet_right(Budgie.AppletInfo? info) {
			string? new_home = null;
			int new_position = info.position;
			int old_position = info.position;
			uint len;

			if (!applet_at_end_of_region(info)) {
				new_position++;
				len = info.applet.get_parent().get_children().length() - 1;
				if (new_position > len) {
					new_position = (int) len;
				}
				info.position = new_position;
				conflict_swap(info, old_position);
				applets_changed();
				return;
			}
			if ((new_home = get_box_right(info)) != null) {
				info.alignment = new_home;
				budge_em_right(new_home);
				info.position = 0;
				this.reinforce_positions();
				applets_changed();
			}
		}

		private bool initial_anim = false;
		private Budgie.Animation? dock_animation = null;

		private bool initial_animation() {
			this.allow_animation = true;
			this.initial_anim = true;

			this.show_panel();
			return false;
		}

		/**
		* Remove existing animations
		*/
		private void remove_panel_animations() {
			if (dock_animation == null) {
				return;
			}
			dock_animation.stop();
			dock_animation = null;
			animation = PanelAnimation.NONE;
		}

		private bool render_panel = true;

		/* Track update dock requests */
		private uint update_dock_id = 0;

		private bool cursor_within_bounds() {
			int cx = 0, cy = 0;
			int x = 0, y = 0;
			int w = 0, h = 0;
			var display = this.get_display();
			unowned Gdk.Device? pointer = null;

			var seat = display.get_default_seat();
			pointer = seat.get_pointer();
			this.get_position(out x, out y);
			this.get_size(out w, out h);
			pointer.get_position(null, out cx, out cy);

			if ((cx >= x && cx <= x + w) && (cy >= y && cy <= y + h)) {
				return true;
			}

			return false;
		}

		/**
		* Handle dock like functionality
		*/
		bool update_dock_behavior() {
			update_dock_id = 0;

			PanelAnimation target_state = PanelAnimation.NONE;

			if (this.autohide == AutohidePolicy.NONE) {
				this.remove_panel_animations();
				this.animation = PanelAnimation.NONE;
				this.placement();
				this.show_panel();
				return false;
			}

			/* Intellihide is basically: Are we intersected */
			if (this.autohide == AutohidePolicy.INTELLIGENT) {
				if (this.intersected) {
					target_state = PanelAnimation.HIDE;
				} else {
					target_state = PanelAnimation.SHOW;
				}
			} else {
				if (this.screen_occluded) {
					target_state = PanelAnimation.HIDE;
				} else {
					target_state = PanelAnimation.SHOW;
				}
			}

			if (target_state == PanelAnimation.SHOW && nscale == 1.0) {
				return false;
			}

			if (target_state == PanelAnimation.HIDE && nscale == 0.0) {
				return false;
			}

			this.remove_panel_animations();

			if (target_state == PanelAnimation.SHOW) {
				this.show_panel();
			} else {
				this.hide_panel();
			}
			return false;
		}

		/**
		* Unset the input region to allow peek events
		*/
		private void unset_input_region() {
			// Set 1px input region to receive enter-notify
			render_panel = false;
			Cairo.Region? region = null;

			switch (position) {
				case PanelPosition.TOP:
					region = new Cairo.Region.rectangle(Cairo.RectangleInt() {
						x = 0, y = 0,
						width = get_allocated_width() * this.scale_factor,
						height = 1 * this.scale_factor
					});
					break;
				case PanelPosition.LEFT:
					region = new Cairo.Region.rectangle(Cairo.RectangleInt() {
						x = 0, y = 0,
						width = 1 * this.scale_factor,
						height = get_allocated_height() * this.scale_factor
					});
					break;
				case PanelPosition.RIGHT:
					region = new Cairo.Region.rectangle(Cairo.RectangleInt() {
						x = (get_allocated_width() * this.scale_factor) - (1 * this.scale_factor),
						y = 0,
						width = 1 * this.scale_factor,
						height = get_allocated_height() * this.scale_factor
					});
					break;
				case PanelPosition.BOTTOM:
				default:
					region = new Cairo.Region.rectangle(Cairo.RectangleInt() {
						x = 0,
						y = (get_allocated_height() * this.scale_factor) - (1 * this.scale_factor),
						width = get_allocated_width() * this.scale_factor,
						height = 1 * this.scale_factor
					});
					break;
			}

			get_window().input_shape_combine_region(region, 0, 0);
		}

		/**
		* Restore the full input region for "normal" usage
		*/
		private void set_input_region() {
			var region = new Cairo.Region.rectangle(Cairo.RectangleInt() {
				x = 0, y = 0,
				width = get_allocated_width(),
				height = get_allocated_height()
			});
			get_window().input_shape_combine_region(region, 0, 0);
		}

		/**
		* In an autohidden mode, if we're not visible, and get peeked, say
		* hello
		*/
		private bool on_enter_notify(Gdk.EventCrossing cr) {
			if (this.render_panel) {
				return Gdk.EVENT_PROPAGATE;
			}
			if (this.autohide == AutohidePolicy.NONE) {
				return Gdk.EVENT_PROPAGATE;
			}
			if (cr.detail == Gdk.NotifyType.INFERIOR) {
				return Gdk.EVENT_PROPAGATE;
			}

			if (update_dock_id > 0) {
				Source.remove(update_dock_id);
				update_dock_id = 0;
			}

			if (show_panel_id > 0) {
				Source.remove(show_panel_id);
			}
			show_panel_id = Timeout.add(150, this.show_panel);
			return Gdk.EVENT_STOP;
		}

		private bool on_leave_notify(Gdk.EventCrossing cr) {
			if (this.autohide == AutohidePolicy.NONE) {
				return Gdk.EVENT_PROPAGATE;
			}
			if (cr.detail == Gdk.NotifyType.INFERIOR) {
				return Gdk.EVENT_PROPAGATE;
			}

			if (show_panel_id > 0) {
				Source.remove(show_panel_id);
				show_panel_id = 0;
			}

			if (update_dock_id > 0) {
				Source.remove(update_dock_id);
			}
			update_dock_id = Timeout.add(175, this.update_dock_behavior);
			return Gdk.EVENT_STOP;
		}

		uint show_panel_id = 0;

		/**
		* Show the panel through a small animation
		*/
		private bool show_panel() {
			show_panel_id = 0;

			if (!this.allow_animation) {
				return false;
			}
			this.animation = PanelAnimation.SHOW;
			render_panel = true;

			this.queue_draw();
			this.show();

			if (!this.get_settings().gtk_enable_animations) {
				this.nscale = 1.0;
				this.set_input_region();
				this.animation = PanelAnimation.NONE;
				this.queue_draw();
				return false;
			}

			this.set_input_region();

			dock_animation = new Budgie.Animation();
			dock_animation.widget = this;
			dock_animation.length = 360 * Budgie.MSECOND;
			dock_animation.tween = Budgie.expo_ease_out;
			dock_animation.changes = new Budgie.PropChange[] {
				Budgie.PropChange() {
					property = "nscale",
					old = this.nscale,
					@new = 1.0
				}
			};

			dock_animation.start((a) => {
				this.set_input_region();
				this.animation = PanelAnimation.NONE;
			});
			return false;
		}

		/**
		* Hide the panel through a small animation
		*/
		private void hide_panel() {
			if (!this.allow_animation) {
				return;
			}

			if (this.cursor_within_bounds()) {
				return;
			}

			if (!this.get_settings().gtk_enable_animations) {
				this.nscale = 0.0;
				this.unset_input_region();
				this.animation = PanelAnimation.NONE;
				this.queue_draw();
				return;
			}

			this.unset_input_region();

			render_panel = true;
			this.animation = PanelAnimation.SHOW;
			dock_animation = new Budgie.Animation();
			dock_animation.widget = this;
			dock_animation.length = 360 * Budgie.MSECOND;
			dock_animation.tween = Budgie.expo_ease_out;
			dock_animation.changes = new Budgie.PropChange[] {
				Budgie.PropChange() {
					property = "nscale",
					old = this.nscale,
					@new = 0.0
				}
			};

			dock_animation.start((a) => {
				this.unset_input_region();
				this.animation = PanelAnimation.NONE;
			});
		}

		public override bool draw(Cairo.Context cr) {
			if (!render_panel) {
				/* Don't need to render */
				return Gdk.EVENT_STOP;
			}

			if (animation == PanelAnimation.NONE) {
				return base.draw(cr);
			}

			var window = this.get_window();
			if (window == null) {
				return Gdk.EVENT_STOP;
			}

			Gtk.Allocation alloc;
			get_allocation(out alloc);
			/* Create a compatible buffer for the current scaling factor */
			var buffer = window.create_similar_image_surface(Cairo.Format.ARGB32,
															alloc.width * this.scale_factor,
															alloc.height * this.scale_factor,
															this.scale_factor);
			var cr2 = new Cairo.Context(buffer);

			propagate_draw(get_child(), cr2);
			var y = ((double)alloc.height) * render_scale;
			var x = ((double)alloc.width) * render_scale;

			switch (position) {
				case Budgie.PanelPosition.TOP:
					// Slide down into view
					cr.set_source_surface(buffer, 0, y - alloc.height);
					break;
				case Budgie.PanelPosition.LEFT:
					// Slide into view from left
					cr.set_source_surface(buffer, x - alloc.width, 0);
					break;
				case Budgie.PanelPosition.RIGHT:
					// Slide back into view from right
					cr.set_source_surface(buffer, alloc.width - x, 0);
					break;
				case Budgie.PanelPosition.BOTTOM:
				default:
					// Slide up into view
					cr.set_source_surface(buffer, 0, alloc.height - y);
					break;
			}

			cr.paint();

			return Gdk.EVENT_STOP;
		}

		/**
		* Specialist operation, perform a migration after we changed applet configurations
		* See: https://github.com/solus-project/budgie-desktop/issues/555
		*/
		public void perform_migration(int current_migration_level) {
			if (current_migration_level != 0) {
				warning("Unknown migration level: %d", current_migration_level);
				return;
			}
			this.need_migratory = true;
			if (this.is_fully_loaded) {
				message("Performing migration to level %d", BUDGIE_MIGRATION_LEVEL);
				this.add_migratory();
			}
		}

		/**
		* Very simple right now. Just add the applets to the end of the panel
		*/
		private bool add_migratory() {
			lock (need_migratory) {
				if (!need_migratory) {
					return false;
				}
				need_migratory = false;
				foreach (var new_applet in MIGRATION_1_APPLETS) {
					message("Adding migratory applet: %s", new_applet);
					add_new_applet_at(new_applet, end_box);
				}
			}
			return false;
		}
	}
}
