#include <ctype.h>
#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <wlr/types/wlr_ext_workspace_v1.h>
#include "log.h"
#include "stringop.h"
#include "sway/desktop/transaction.h"
#include "sway/input/input-manager.h"
#include "sway/input/cursor.h"
#include "sway/input/seat.h"
#include "sway/ipc-server.h"
#include "sway/output.h"
#include "sway/server.h"
#include "sway/tree/arrange.h"
#include "sway/tree/container.h"
#include "sway/tree/node.h"
#include "sway/tree/view.h"
#include "sway/tree/workspace.h"
#include "list.h"
#include "util.h"

static const uint32_t WORKSPACE_CAPABILITIES =
	EXT_WORKSPACE_HANDLE_V1_WORKSPACE_CAPABILITIES_ACTIVATE |
	EXT_WORKSPACE_HANDLE_V1_WORKSPACE_CAPABILITIES_ASSIGN;

static const uint32_t GROUP_CAPABILITIES =
	EXT_WORKSPACE_GROUP_HANDLE_V1_GROUP_CAPABILITIES_CREATE_WORKSPACE;

// Helper to find the output associated with a workspace group.
static struct sway_output *group_to_output(
		struct wlr_ext_workspace_group_handle_v1 *group) {
	for (int i = 0; i < root->outputs->length; i++) {
		struct sway_output *output = root->outputs->items[i];
		if (output->ext_workspace_group == group) {
			return output;
		}
	}
	abort(); // unreachable
}

// Callback for ext-workspace-v1 commit events.
static void handle_commit(struct wl_listener *listener, void *data) {
	struct sway_server *server =
		wl_container_of(listener, server, workspace_manager_v1_commit);
	struct wlr_ext_workspace_v1_commit_event *event = data;

	struct wlr_ext_workspace_v1_request *req, *tmp;
	wl_list_for_each_safe(req, tmp, event->requests, link) {
		switch (req->type) {
		case WLR_EXT_WORKSPACE_V1_REQUEST_ACTIVATE:
			if (req->activate.workspace) {
				workspace_switch(req->activate.workspace->data);
			}
			break;
		case WLR_EXT_WORKSPACE_V1_REQUEST_CREATE_WORKSPACE:;
			struct sway_output *output = group_to_output(req->create_workspace.group);
			sway_assert(output, "NULL output given to create_workspace");

			char *name;
			if (req->create_workspace.name) {
				if (workspace_by_name(req->create_workspace.name)) {
					sway_log(SWAY_ERROR, "Refusing to create workspace with duplicate name.");
					break; // Already exists.
				}
				name = strdup(req->create_workspace.name);
			} else {
				name = workspace_next_name(output->wlr_output->name);
			}

			struct sway_workspace *new_ws = workspace_create(output, name);
			if (new_ws) {
				workspace_switch(new_ws);
			}
			free(name);
			break;
		case WLR_EXT_WORKSPACE_V1_REQUEST_ASSIGN:;
			if (!req->assign.workspace || !req->assign.group) break;

			struct sway_workspace *ws = req->assign.workspace->data;
			struct sway_output *new_output = group_to_output(req->assign.group);
			struct sway_output *old_output = ws->output;
			workspace_move_to_output(ws, new_output);
			arrange_output(old_output);
			arrange_output(new_output);
			break;
		case WLR_EXT_WORKSPACE_V1_REQUEST_DEACTIVATE:
		case WLR_EXT_WORKSPACE_V1_REQUEST_REMOVE:
			break; // No-op.
		}
	}

	transaction_commit_dirty();
}

// Initialize ext-workspace. Must be called once at startup.
bool sway_ext_workspace_init(void) {
	server.workspace_manager_v1 =
		wlr_ext_workspace_manager_v1_create(server.wl_display, 1);
	if (!server.workspace_manager_v1) {
		sway_log(SWAY_ERROR, "Failed to create ext_workspace_manager_v1");
		return false;
	}

	server.workspace_manager_v1_commit.notify = handle_commit;
	wl_signal_add(&server.workspace_manager_v1->events.commit,
		&server.workspace_manager_v1_commit);
	return true;
}

// Must be called whenever an output is enabled.
void sway_ext_workspace_output_enable(struct sway_output *output) {
	if (!output->wlr_output) {
		return;
	}

	output->ext_workspace_group =
		wlr_ext_workspace_group_handle_v1_create(
			server.workspace_manager_v1, GROUP_CAPABILITIES);
	if (!output->ext_workspace_group) {
		sway_log(SWAY_ERROR, "Failed to create workspace group for output '%s'",
			output->wlr_output->name);
		return;
	}

	wlr_ext_workspace_group_handle_v1_output_enter(
		output->ext_workspace_group, output->wlr_output);
}

// Must be called whenever an output is disabled.
void sway_ext_workspace_output_disable(struct sway_output *output) {
	if (!output->ext_workspace_group) {
		return;
	}

	wlr_ext_workspace_group_handle_v1_destroy(output->ext_workspace_group);
	output->ext_workspace_group = NULL;
}

struct workspace_config *workspace_find_config(const char *ws_name) {
	for (int i = 0; i < config->workspace_configs->length; ++i) {
		struct workspace_config *wsc = config->workspace_configs->items[i];
		if (strcmp(wsc->workspace, ws_name) == 0) {
			return wsc;
		}
	}
	return NULL;
}

struct sway_output *workspace_get_initial_output(const char *name) {
	// Check workspace configs for a workspace<->output pair
	struct workspace_config *wsc = workspace_find_config(name);
	if (wsc) {
		for (int i = 0; i < wsc->outputs->length; i++) {
			struct sway_output *output =
				output_by_name_or_id(wsc->outputs->items[i]);
			if (output) {
				return output;
			}
		}
	}
	// Otherwise try to put it on the focused output
	struct sway_seat *seat = input_manager_current_seat();
	struct sway_node *focus = seat_get_focus_inactive(seat, &root->node);
	if (focus && focus->type == N_WORKSPACE) {
		return focus->sway_workspace->output;
	} else if (focus && focus->type == N_CONTAINER) {
		return focus->sway_container->pending.workspace->output;
	}
	// Fallback to the first output or the headless output
	return root->outputs->length ? root->outputs->items[0] : root->fallback_output;
}

struct sway_workspace *workspace_create(struct sway_output *output,
		const char *name) {
	sway_assert(name, "NULL name given to workspace_create");

	if (output == NULL) {
		output = workspace_get_initial_output(name);
	}

	sway_log(SWAY_DEBUG, "Adding workspace %s for output %s", name,
			output->wlr_output->name);

	struct sway_workspace *ws = calloc(1, sizeof(struct sway_workspace));
	if (!ws) {
		sway_log(SWAY_ERROR, "Unable to allocate sway_workspace");
		return NULL;
	}

	ws->ext_workspace = wlr_ext_workspace_handle_v1_create(
		server.workspace_manager_v1, NULL, WORKSPACE_CAPABILITIES);
	if (!ws->ext_workspace) {
		sway_log(SWAY_ERROR, "Failed to create ext_workspace for '%s'", name);
		free(ws);
		return NULL;
	}
	ws->ext_workspace->data = ws;

	node_init(&ws->node, N_WORKSPACE, ws);

	bool failed = false;
	ws->layers.tiling = alloc_scene_tree(root->staging, &failed);
	ws->layers.fullscreen = alloc_scene_tree(root->staging, &failed);

	if (failed) {
		wlr_scene_node_destroy(&ws->layers.tiling->node);
		wlr_scene_node_destroy(&ws->layers.fullscreen->node);
		wlr_ext_workspace_handle_v1_destroy(ws->ext_workspace);
		free(ws);
		return NULL;
	}

	ws->name = strdup(name);
	ws->prev_split_layout = L_NONE;
	ws->layout = output_get_default_layout(output);
	ws->floating = create_list();
	ws->tiling = create_list();
	ws->output_priority = create_list();

	ws->gaps_outer = config->gaps_outer;
	ws->gaps_inner = config->gaps_inner;
	if (name) {
		struct workspace_config *wsc = workspace_find_config(name);
		if (wsc) {
			if (wsc->gaps_outer.top != INT_MIN) {
				ws->gaps_outer.top = wsc->gaps_outer.top;
			}
			if (wsc->gaps_outer.right != INT_MIN) {
				ws->gaps_outer.right = wsc->gaps_outer.right;
			}
			if (wsc->gaps_outer.bottom != INT_MIN) {
				ws->gaps_outer.bottom = wsc->gaps_outer.bottom;
			}
			if (wsc->gaps_outer.left != INT_MIN) {
				ws->gaps_outer.left = wsc->gaps_outer.left;
			}
			if (wsc->gaps_inner != INT_MIN) {
				ws->gaps_inner = wsc->gaps_inner;
			}

			// Add output priorities
			for (int i = 0; i < wsc->outputs->length; ++i) {
				char *name = wsc->outputs->items[i];
				if (strcmp(name, "*") != 0) {
					list_add(ws->output_priority, strdup(name));
				}
			}
		}
	}

	// If not already added, add the output to the lowest priority
	workspace_output_add_priority(ws, output);

	output_add_workspace(output, ws);
	output_sort_workspaces(output);

	wlr_ext_workspace_handle_v1_set_name(ws->ext_workspace, ws->name);
	if (ws->output && ws->output->ext_workspace_group) {
		wlr_ext_workspace_handle_v1_set_group(ws->ext_workspace,
			ws->output->ext_workspace_group);
	}
	wlr_ext_workspace_handle_v1_set_active(ws->ext_workspace,
		workspace_is_visible(ws));
	ipc_event_workspace(NULL, ws, "init");
	wl_signal_emit_mutable(&root->events.new_node, &ws->node);

	return ws;
}

void workspace_destroy(struct sway_workspace *workspace) {
	if (!sway_assert(workspace->node.destroying,
				"Tried to free workspace which wasn't marked as destroying")) {
		return;
	}
	if (!sway_assert(workspace->node.ntxnrefs == 0, "Tried to free workspace "
				"which is still referenced by transactions")) {
		return;
	}

	scene_node_disown_children(workspace->layers.tiling);
	scene_node_disown_children(workspace->layers.fullscreen);
	wlr_scene_node_destroy(&workspace->layers.tiling->node);
	wlr_scene_node_destroy(&workspace->layers.fullscreen->node);

	free(workspace->name);
	free(workspace->representation);
	list_free_items_and_destroy(workspace->output_priority);
	list_free(workspace->floating);
	list_free(workspace->tiling);
	list_free(workspace->current.floating);
	list_free(workspace->current.tiling);
	free(workspace);
}

void workspace_begin_destroy(struct sway_workspace *workspace) {
	sway_log(SWAY_DEBUG, "Destroying workspace '%s'", workspace->name);
	ipc_event_workspace(NULL, workspace, "empty"); // intentional
	wl_signal_emit_mutable(&workspace->node.events.destroy, &workspace->node);

	wlr_ext_workspace_handle_v1_destroy(workspace->ext_workspace);
	workspace->ext_workspace = NULL;

	if (workspace->output) {
		workspace_detach(workspace);
	}
	node_set_dirty(&workspace->node);
	workspace->node.destroying = true;
}

void workspace_consider_destroy(struct sway_workspace *ws) {
	if (ws->tiling->length || ws->floating->length) {
		return;
	}

	if (ws->output && output_get_active_workspace(ws->output) == ws) {
		return;
	}

	struct sway_seat *seat;
	wl_list_for_each(seat, &server.input->seats, link) {
		struct sway_node *node = seat_get_focus_inactive(seat, &root->node);
		if (node == &ws->node) {
			return;
		}
	}

	workspace_begin_destroy(ws);
}

static bool workspace_valid_on_output(const char *output_name,
		const char *ws_name) {
	struct workspace_config *wsc = workspace_find_config(ws_name);
	struct sway_output *output = output_by_name_or_id(output_name);
	if (!output) {
		return false;
	}
	if (!wsc) {
		return true;
	}

	for (int i = 0; i < wsc->outputs->length; i++) {
		struct sway_output *ws_output =
			output_by_name_or_id(wsc->outputs->items[i]);
		if (ws_output) {
			return ws_output == output;
		}
	}

	return false;
}

static void workspace_name_from_binding(const struct sway_binding * binding,
		const char* output_name, int *min_order, char **earliest_name) {
	char *cmdlist = strdup(binding->command);
	char *dup = cmdlist;
	char *name = NULL;

	// workspace n
	char *cmd = argsep(&cmdlist, " ", NULL);
	if (cmdlist) {
		name = argsep(&cmdlist, ",;", NULL);
	}

	// TODO: support "move container to workspace" bindings as well

	if (strcmp("workspace", cmd) == 0 && name) {
		char *_target = strdup(name);
		_target = do_var_replacement(_target);
		strip_quotes(_target);
		sway_log(SWAY_DEBUG, "Got valid workspace command for target: '%s'",
				_target);

		// Make sure that the command references an actual workspace
		// not a command about workspaces
		if (strcmp(_target, "next") == 0 ||
				strcmp(_target, "prev") == 0 ||
				strcmp(_target, "next_on_output") == 0 ||
				strcmp(_target, "prev_on_output") == 0 ||
				strcmp(_target, "number") == 0 ||
				strcmp(_target, "back_and_forth") == 0 ||
				strcmp(_target, "current") == 0) {
			free(_target);
			free(dup);
			return;
		}

		// If the command is workspace number <name>, isolate the name
		if (has_prefix(_target, "number ")) {
			size_t length = strlen(_target) - strlen("number ") + 1;
			char *temp = malloc(length);
			strncpy(temp, _target + strlen("number "), length - 1);
			temp[length - 1] = '\0';
			free(_target);
			_target = temp;
			sway_log(SWAY_DEBUG, "Isolated name from workspace number: '%s'", _target);

			// Make sure the workspace number doesn't already exist
			if (isdigit(_target[0]) && workspace_by_number(_target)) {
				free(_target);
				free(dup);
				return;
			}
		}

		// Make sure that the workspace doesn't already exist
		if (workspace_by_name(_target)) {
			free(_target);
			free(dup);
			return;
		}

		// make sure that the workspace can appear on the given
		// output
		if (!workspace_valid_on_output(output_name, _target)) {
			free(_target);
			free(dup);
			return;
		}

		if (binding->order < *min_order) {
			*min_order = binding->order;
			free(*earliest_name);
			*earliest_name = _target;
			sway_log(SWAY_DEBUG, "Workspace: Found free name %s", _target);
		} else {
			free(_target);
		}
	}
	free(dup);
}

char *workspace_next_name(const char *output_name) {
	sway_log(SWAY_DEBUG, "Workspace: Generating new workspace name for output %s",
			output_name);
	// Scan for available workspace names by looking through output-workspace
	// assignments primarily, falling back to bindings and numbers.
	struct sway_mode *mode = config->current_mode;

	struct sway_output *output = output_by_name_or_id(output_name);
	if (!output) {
		return NULL;
	}

	int order = INT_MAX;
	char *target = NULL;
	for (int i = 0; i < mode->keysym_bindings->length; ++i) {
		workspace_name_from_binding(mode->keysym_bindings->items[i],
				output_name, &order, &target);
	}
	for (int i = 0; i < mode->keycode_bindings->length; ++i) {
		workspace_name_from_binding(mode->keycode_bindings->items[i],
				output_name, &order, &target);
	}
	for (int i = 0; i < config->workspace_configs->length; ++i) {
		// Unlike with bindings, this does not guarantee order
		const struct workspace_config *wsc = config->workspace_configs->items[i];
		if (workspace_by_name(wsc->workspace)) {
			continue;
		}
		bool found = false;
		for (int j = 0; j < wsc->outputs->length; ++j) {
			struct sway_output *ws_output =
				output_by_name_or_id(wsc->outputs->items[j]);
			if (ws_output) {
				if (ws_output == output) {
					found = true;
					free(target);
					target = strdup(wsc->workspace);
				}
				break;
			}
		}
		if (found) {
			break;
		}
	}
	if (target != NULL) {
		return target;
	}
	// As a fall back, use the next available number
	char name[12] = "";
	unsigned int ws_num = 1;
	do {
		snprintf(name, sizeof(name), "%u", ws_num++);
	} while (workspace_by_number(name));
	return strdup(name);
}

static bool _workspace_by_number(struct sway_workspace *ws, void *data) {
	char *name = data;
	char *ws_name = ws->name;
	while (isdigit(*name)) {
		if (*name++ != *ws_name++) {
			return false;
		}
	}
	return !isdigit(*ws_name);
}

struct sway_workspace *workspace_by_number(const char* name) {
	return root_find_workspace(_workspace_by_number, (void *) name);
}

static bool _workspace_by_name(struct sway_workspace *ws, void *data) {
	return strcasecmp(ws->name, data) == 0;
}

struct sway_workspace *workspace_by_name(const char *name) {
	struct sway_seat *seat = input_manager_current_seat();
	struct sway_workspace *current = seat_get_focused_workspace(seat);

	if (current && strcmp(name, "prev") == 0) {
		return workspace_prev(current);
	} else if (current && strcmp(name, "prev_on_output") == 0) {
		return workspace_output_prev(current);
	} else if (current && strcmp(name, "next") == 0) {
		return workspace_next(current);
	} else if (current && strcmp(name, "next_on_output") == 0) {
		return workspace_output_next(current);
	} else if (strcmp(name, "current") == 0) {
		return current;
	} else if (strcasecmp(name, "back_and_forth") == 0) {
		struct sway_seat *seat = input_manager_current_seat();
		if (!seat->prev_workspace_name) {
			return NULL;
		}
		return root_find_workspace(_workspace_by_name,
				(void*)seat->prev_workspace_name);
	} else {
		return root_find_workspace(_workspace_by_name, (void*)name);
	}
}

static int workspace_get_number(struct sway_workspace *workspace) {
	char *endptr = NULL;
	errno = 0;
	long long n = strtoll(workspace->name, &endptr, 10);
	if (errno != 0 || n > INT32_MAX || n < 0 || endptr == workspace->name) {
		n = -1;
	}
	return n;
}

struct sway_workspace *workspace_prev(struct sway_workspace *workspace) {
	int n = workspace_get_number(workspace);
	struct sway_workspace *prev = NULL, *last = NULL, *other = NULL;
	bool found = false;
	if (n < 0) {
		// Find the prev named workspace
		int othern = -1;
		for (int i = root->outputs->length - 1; i >= 0; i--) {
			struct sway_output *output = root->outputs->items[i];
			for (int j = output->workspaces->length - 1; j >= 0; j--) {
				struct sway_workspace *ws = output->workspaces->items[j];
				int wsn = workspace_get_number(ws);
				if (!last) {
					// The first workspace in reverse order
					last = ws;
				}
				if (!other || (wsn >= 0 && wsn > othern)) {
					// The last (greatest) numbered workspace.
					other = ws;
					othern = workspace_get_number(other);
				}
				if (ws == workspace) {
					found = true;
				} else if (wsn < 0 && found) {
					// Found a non-numbered workspace before current
					return ws;
				}
			}
		}
	} else {
		// Find the prev numbered workspace
		int prevn = -1, lastn = -1;
		for (int i = root->outputs->length - 1; i >= 0; i--) {
			struct sway_output *output = root->outputs->items[i];
			for (int j = output->workspaces->length - 1; j >= 0; j--) {
				struct sway_workspace *ws = output->workspaces->items[j];
				int wsn = workspace_get_number(ws);
				if (!last || (wsn >= 0 && wsn > lastn)) {
					// The greatest numbered (or last) workspace
					last = ws;
					lastn = workspace_get_number(last);
				}
				if (!other && wsn < 0) {
					// The last named workspace
					other = ws;
				}
				if (wsn < 0) {
					// Haven't reached the numbered workspaces
					continue;
				}
				if (wsn < n && (!prev || wsn > prevn)) {
					// The closest workspace before the current
					prev = ws;
					prevn = workspace_get_number(prev);
				}
			}
		}
	}

	if (!prev) {
		prev = other ? other : last;
	}
	return prev;
}

struct sway_workspace *workspace_next(struct sway_workspace *workspace) {
	int n = workspace_get_number(workspace);
	struct sway_workspace *next = NULL, *first = NULL, *other = NULL;
	bool found = false;
	if (n < 0) {
		// Find the next named workspace
		int othern = -1;
		for (int i = 0; i < root->outputs->length; i++) {
			struct sway_output *output = root->outputs->items[i];
			for (int j = 0; j < output->workspaces->length; j++) {
				struct sway_workspace *ws = output->workspaces->items[j];
				int wsn = workspace_get_number(ws);
				if (!first) {
					// The first named workspace
					first = ws;
				}
				if (!other || (wsn >= 0 && wsn < othern)) {
					// The first (least) numbered workspace
					other = ws;
					othern = workspace_get_number(other);
				}
				if (ws == workspace) {
					found = true;
				} else if (wsn < 0 && found) {
					// The first non-numbered workspace after the current
					return ws;
				}
			}
		}
	} else {
		// Find the next numbered workspace
		int nextn = -1, firstn = -1;
		for (int i = 0; i < root->outputs->length; i++) {
			struct sway_output *output = root->outputs->items[i];
			for (int j = 0; j < output->workspaces->length; j++) {
				struct sway_workspace *ws = output->workspaces->items[j];
				int wsn = workspace_get_number(ws);
				if (!first || (wsn >= 0 && wsn < firstn)) {
					// The first (or least numbered) workspace
					first = ws;
					firstn = workspace_get_number(first);
				}
				if (!other && wsn < 0) {
					// The first non-numbered workspace
					other = ws;
				}
				if (wsn < 0) {
					// Checked all the numbered workspaces
					break;
				}
				if (n < wsn && (!next || wsn < nextn)) {
					// The first workspace numerically after the current
					next = ws;
					nextn = workspace_get_number(next);
				}
			}
		}
	}

	if (!next) {
		// If there is no next workspace from the same category, return the
		// first from this category.
		next = other ? other : first;
	}
	return next;
}

/**
 * Get the previous or next workspace on the specified output. Wraps around at
 * the end and beginning.  If next is false, the previous workspace is returned,
 * otherwise the next one is returned.
 */
static struct sway_workspace *workspace_output_prev_next_impl(
		struct sway_output *output, int dir) {
	struct sway_seat *seat = input_manager_current_seat();
	struct sway_workspace *workspace = seat_get_focused_workspace(seat);
	if (!workspace) {
		sway_log(SWAY_DEBUG,
				"No focused workspace to base prev/next on output off of");
		return NULL;
	}

	int index = list_find(output->workspaces, workspace);
	size_t new_index = wrap(index + dir, output->workspaces->length);
	return output->workspaces->items[new_index];
}


struct sway_workspace *workspace_output_next(struct sway_workspace *current) {
	return workspace_output_prev_next_impl(current->output, 1);
}

struct sway_workspace *workspace_output_prev(struct sway_workspace *current) {
	return workspace_output_prev_next_impl(current->output, -1);
}

struct sway_workspace *workspace_auto_back_and_forth(
		struct sway_workspace *workspace) {
	struct sway_seat *seat = input_manager_current_seat();
	struct sway_workspace *active_ws = NULL;
	struct sway_node *focus = seat_get_focus_inactive(seat, &root->node);
	if (focus && focus->type == N_WORKSPACE) {
		active_ws = focus->sway_workspace;
	} else if (focus && focus->type == N_CONTAINER) {
		active_ws = focus->sway_container->pending.workspace;
	}

	if (config->auto_back_and_forth && active_ws && active_ws == workspace &&
			seat->prev_workspace_name) {
		struct sway_workspace *new_ws =
			workspace_by_name(seat->prev_workspace_name);
		workspace = new_ws ?
			new_ws :
			workspace_create(NULL, seat->prev_workspace_name);
	}
	return workspace;
}

bool workspace_switch(struct sway_workspace *workspace) {
	struct sway_seat *seat = input_manager_current_seat();

	sway_log(SWAY_DEBUG, "Switching to workspace %p:%s",
		workspace, workspace->name);
	struct sway_node *next = seat_get_focus_inactive(seat, &workspace->node);
	if (next == NULL) {
		next = &workspace->node;
	}
	seat_set_focus(seat, next);
	arrange_workspace(workspace);
	return true;
}

bool workspace_is_visible(struct sway_workspace *ws) {
	if (ws->node.destroying) {
		return false;
	}
	return output_get_active_workspace(ws->output) == ws;
}

bool workspace_is_empty(struct sway_workspace *ws) {
	if (ws->tiling->length) {
		return false;
	}
	// Sticky views are not considered to be part of this workspace
	for (int i = 0; i < ws->floating->length; ++i) {
		struct sway_container *floater = ws->floating->items[i];
		if (!container_is_sticky(floater)) {
			return false;
		}
	}
	return true;
}

static int find_output(const void *id1, const void *id2) {
	return strcmp(id1, id2);
}

static int workspace_output_get_priority(struct sway_workspace *ws,
		struct sway_output *output) {
	char identifier[128];
	output_get_identifier(identifier, sizeof(identifier), output);
	int index_id = list_seq_find(ws->output_priority, find_output, identifier);
	int index_name = list_seq_find(ws->output_priority, find_output,
			output->wlr_output->name);
	return index_name < 0 || index_id < index_name ? index_id : index_name;
}

void workspace_output_raise_priority(struct sway_workspace *ws,
		struct sway_output *old_output, struct sway_output *output) {
	int old_index = workspace_output_get_priority(ws, old_output);
	if (old_index < 0) {
		return;
	}

	int new_index = workspace_output_get_priority(ws, output);
	if (new_index < 0) {
		char identifier[128];
		output_get_identifier(identifier, sizeof(identifier), output);
		list_insert(ws->output_priority, old_index, strdup(identifier));
	} else if (new_index > old_index) {
		char *name = ws->output_priority->items[new_index];
		list_del(ws->output_priority, new_index);
		list_insert(ws->output_priority, old_index, name);
	}
}

void workspace_output_add_priority(struct sway_workspace *workspace,
		struct sway_output *output) {
	if (workspace_output_get_priority(workspace, output) < 0) {
		char identifier[128];
		output_get_identifier(identifier, sizeof(identifier), output);
		list_add(workspace->output_priority, strdup(identifier));
	}
}

struct sway_output *workspace_output_get_highest_available(
		struct sway_workspace *ws) {
	for (int i = 0; i < ws->output_priority->length; i++) {
		const char *name = ws->output_priority->items[i];
		struct sway_output *output = output_by_name_or_id(name);
		if (output) {
			return output;
		}
	}

	return NULL;
}

static bool find_urgent_iterator(struct sway_container *con, void *data) {
	return con->view && view_is_urgent(con->view);
}

void workspace_detect_urgent(struct sway_workspace *workspace) {
	bool new_urgent = (bool)workspace_find_container(workspace,
			find_urgent_iterator, NULL);

	if (workspace->urgent != new_urgent) {
		workspace->urgent = new_urgent;
		wlr_ext_workspace_handle_v1_set_urgent(workspace->ext_workspace, workspace->urgent);
		ipc_event_workspace(NULL, workspace, "urgent");
	}
}

void workspace_for_each_container(struct sway_workspace *ws,
		void (*f)(struct sway_container *con, void *data), void *data) {
	// Tiling
	for (int i = 0; i < ws->tiling->length; ++i) {
		struct sway_container *container = ws->tiling->items[i];
		f(container, data);
		container_for_each_child(container, f, data);
	}
	// Floating
	for (int i = 0; i < ws->floating->length; ++i) {
		struct sway_container *container = ws->floating->items[i];
		f(container, data);
		container_for_each_child(container, f, data);
	}
}

struct sway_container *workspace_find_container(struct sway_workspace *ws,
		bool (*test)(struct sway_container *con, void *data), void *data) {
	struct sway_container *result = NULL;
    if (ws == NULL){
        sway_log(SWAY_ERROR, "Cannot find container with no workspace.");
        return NULL;
    }

	// Tiling
	for (int i = 0; i < ws->tiling->length; ++i) {
		struct sway_container *child = ws->tiling->items[i];
		if (test(child, data)) {
			return child;
		}
		if ((result = container_find_child(child, test, data))) {
			return result;
		}
	}
	// Floating
	for (int i = 0; i < ws->floating->length; ++i) {
		struct sway_container *child = ws->floating->items[i];
		if (test(child, data)) {
			return child;
		}
		if ((result = container_find_child(child, test, data))) {
			return result;
		}
	}
	return NULL;
}

static void set_workspace(struct sway_container *container, void *data) {
	container->pending.workspace = container->pending.parent->pending.workspace;
}

static void workspace_attach_tiling(struct sway_workspace *ws,
		struct sway_container *con) {
	list_add(ws->tiling, con);
	con->pending.workspace = ws;
	container_for_each_child(con, set_workspace, NULL);
	container_handle_fullscreen_reparent(con);
	workspace_update_representation(ws);
	node_set_dirty(&ws->node);
	node_set_dirty(&con->node);
}

struct sway_container *workspace_wrap_children(struct sway_workspace *ws) {
	struct sway_container *fs = ws->fullscreen;
	struct sway_container *middle = container_create(NULL);
	middle->pending.layout = ws->layout;
	while (ws->tiling->length) {
		struct sway_container *child = ws->tiling->items[0];
		container_detach(child);
		container_add_child(middle, child);
	}
	workspace_attach_tiling(ws, middle);
	ws->fullscreen = fs;
	return middle;
}

void workspace_unwrap_children(struct sway_workspace *ws,
		struct sway_container *wrap) {
	if (!sway_assert(workspace_is_empty(ws),
			"target workspace must be empty")) {
		return;
	}

	ws->layout = wrap->pending.layout;
	while (wrap->pending.children->length) {
		struct sway_container *child = wrap->pending.children->items[0];
		container_detach(child);
		workspace_insert_tiling_direct(ws, child, ws->tiling->length);
	}
}

void workspace_detach(struct sway_workspace *workspace) {
	struct sway_output *output = workspace->output;
	int index = list_find(output->workspaces, workspace);
	if (index != -1) {
		list_del(output->workspaces, index);
	}
	workspace->output = NULL;

	node_set_dirty(&workspace->node);
	node_set_dirty(&output->node);
}

struct sway_container *workspace_add_tiling(struct sway_workspace *workspace,
		struct sway_container *con) {
	if (con->pending.workspace) {
		struct sway_container *old_parent = con->pending.parent;
		container_detach(con);
		if (old_parent) {
			container_reap_empty(old_parent);
		}
	}
	if (config->default_layout != L_NONE) {
		con = container_split(con, config->default_layout);
	}
	list_add(workspace->tiling, con);
	con->pending.workspace = workspace;
	container_for_each_child(con, set_workspace, NULL);
	container_handle_fullscreen_reparent(con);
	workspace_update_representation(workspace);
	node_set_dirty(&workspace->node);
	node_set_dirty(&con->node);
	return con;
}

void workspace_add_floating(struct sway_workspace *workspace,
		struct sway_container *con) {
	if (con->pending.workspace) {
		container_detach(con);
	}
	list_add(workspace->floating, con);
	con->pending.workspace = workspace;
	container_for_each_child(con, set_workspace, NULL);
	container_handle_fullscreen_reparent(con);
	node_set_dirty(&workspace->node);
	node_set_dirty(&con->node);
}

void workspace_insert_tiling_direct(struct sway_workspace *workspace,
		struct sway_container *con, int index) {
	list_insert(workspace->tiling, index, con);
	con->pending.workspace = workspace;
	container_for_each_child(con, set_workspace, NULL);
	container_handle_fullscreen_reparent(con);
	workspace_update_representation(workspace);
	node_set_dirty(&workspace->node);
	node_set_dirty(&con->node);
}

struct sway_container *workspace_insert_tiling(struct sway_workspace *workspace,
		struct sway_container *con, int index) {
	if (con->pending.workspace) {
		container_detach(con);
	}
	if (config->default_layout != L_NONE) {
		con = container_split(con, config->default_layout);
	}
	workspace_insert_tiling_direct(workspace, con, index);
	return con;
}

bool workspace_has_single_visible_container(struct sway_workspace *ws) {
	struct sway_seat *seat = input_manager_get_default_seat();
	struct sway_container *focus =
		seat_get_focus_inactive_tiling(seat, ws);
	if (focus && !focus->view) {
		focus = seat_get_focus_inactive_view(seat, &focus->node);
	}
	return (focus && focus->view && view_ancestor_is_only_visible(focus->view));
}

void workspace_add_gaps(struct sway_workspace *ws) {
	if (config->smart_gaps == SMART_GAPS_ON
			&& workspace_has_single_visible_container(ws)) {
		ws->current_gaps.top = 0;
		ws->current_gaps.right = 0;
		ws->current_gaps.bottom = 0;
		ws->current_gaps.left = 0;
		return;
	}

	if (config->smart_gaps == SMART_GAPS_INVERSE_OUTER
			&& !workspace_has_single_visible_container(ws)) {
		ws->current_gaps.top = 0;
		ws->current_gaps.right = 0;
		ws->current_gaps.bottom = 0;
		ws->current_gaps.left = 0;
	} else {
		ws->current_gaps = ws->gaps_outer;
	}

	// Add inner gaps and make sure we don't turn out negative
	ws->current_gaps.top = fmax(0, ws->current_gaps.top + ws->gaps_inner);
	ws->current_gaps.right = fmax(0, ws->current_gaps.right + ws->gaps_inner);
	ws->current_gaps.bottom = fmax(0, ws->current_gaps.bottom + ws->gaps_inner);
	ws->current_gaps.left = fmax(0, ws->current_gaps.left + ws->gaps_inner);

	// Now that we have the total gaps calculated we may need to clamp them in
	// case they've made the available area too small
	if (ws->width - ws->current_gaps.left - ws->current_gaps.right < MIN_SANE_W
			&& ws->current_gaps.left + ws->current_gaps.right > 0) {
		int total_gap = fmax(0, ws->width - MIN_SANE_W);
		double left_gap_frac = ((double)ws->current_gaps.left /
			((double)ws->current_gaps.left + (double)ws->current_gaps.right));
		ws->current_gaps.left = left_gap_frac * total_gap;
		ws->current_gaps.right = total_gap - ws->current_gaps.left;
	}
	if (ws->height - ws->current_gaps.top - ws->current_gaps.bottom < MIN_SANE_H
			&& ws->current_gaps.top + ws->current_gaps.bottom > 0) {
		int total_gap = fmax(0, ws->height - MIN_SANE_H);
		double top_gap_frac = ((double) ws->current_gaps.top /
			((double)ws->current_gaps.top + (double)ws->current_gaps.bottom));
		ws->current_gaps.top = top_gap_frac * total_gap;
		ws->current_gaps.bottom = total_gap - ws->current_gaps.top;
	}

	ws->x += ws->current_gaps.left;
	ws->y += ws->current_gaps.top;
	ws->width -= ws->current_gaps.left + ws->current_gaps.right;
	ws->height -= ws->current_gaps.top + ws->current_gaps.bottom;
}

struct sway_container *workspace_split(struct sway_workspace *workspace,
		enum sway_container_layout layout) {
	if (workspace->tiling->length == 0) {
		workspace->prev_split_layout = workspace->layout;
		workspace->layout = layout;
		return NULL;
	}

	enum sway_container_layout old_layout = workspace->layout;
	struct sway_container *middle = workspace_wrap_children(workspace);
	workspace->layout = layout;
	middle->pending.layout = old_layout;

	struct sway_seat *seat;
	wl_list_for_each(seat, &server.input->seats, link) {
		if (seat_get_focus(seat) == &workspace->node) {
			seat_set_focus(seat, &middle->node);
		}
	}

	return middle;
}

void workspace_update_representation(struct sway_workspace *ws) {
	size_t len = container_build_representation(ws->layout, ws->tiling, NULL);
	free(ws->representation);
	ws->representation = calloc(len + 1, sizeof(char));
	if (!sway_assert(ws->representation, "Unable to allocate title string")) {
		return;
	}
	container_build_representation(ws->layout, ws->tiling, ws->representation);
}

void workspace_get_box(struct sway_workspace *workspace, struct wlr_box *box) {
	box->x = workspace->x;
	box->y = workspace->y;
	box->width = workspace->width;
	box->height = workspace->height;
}

static void count_tiling_views(struct sway_container *con, void *data) {
	if (con->view && !container_is_floating_or_child(con)) {
		size_t *count = data;
		*count += 1;
	}
}

size_t workspace_num_tiling_views(struct sway_workspace *ws) {
	size_t count = 0;
	workspace_for_each_container(ws, count_tiling_views, &count);
	return count;
}

static void count_sticky_containers(struct sway_container *con, void *data) {
	if (container_is_sticky(con)) {
		size_t *count = data;
		*count += 1;
	}
}

size_t workspace_num_sticky_containers(struct sway_workspace *ws) {
	size_t count = 0;
	workspace_for_each_container(ws, count_sticky_containers, &count);
	return count;
}

void workspace_squash(struct sway_workspace *workspace) {
	for (int i = 0; i < workspace->tiling->length; i++) {
		struct sway_container *child = workspace->tiling->items[i];
		i += container_squash(child);
	}
}

void workspace_move_to_output(struct sway_workspace *workspace,
		struct sway_output *output) {
	if (workspace->output == output) {
		return;
	}
	struct sway_output *old_output = workspace->output;
	workspace_detach(workspace);
	struct sway_workspace *new_output_old_ws =
		output_get_active_workspace(output);
	if (!sway_assert(new_output_old_ws, "Expected output to have a workspace")) {
		return;
	}

	output_add_workspace(output, workspace);

	// If moving the last workspace from the old output, create a new workspace
	// on the old output
	if (old_output->workspaces->length == 0) {
		char *ws_name = workspace_next_name(old_output->wlr_output->name);
		struct sway_workspace *ws = workspace_create(old_output, ws_name);
		free(ws_name);
		struct sway_seat *seat = input_manager_current_seat();
		seat_set_raw_focus(seat, &ws->node);
	}

	workspace_consider_destroy(new_output_old_ws);

	output_sort_workspaces(output);
	workspace_output_raise_priority(workspace, old_output, output);
	ipc_event_workspace(NULL, workspace, "move");
}
