/*
 * Copyright 2017 Philippe Proulx <pproulx@efficios.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <babeltrace2/babeltrace.h>
#include "common/common.h"
#include "babeltrace2-cfg.h"
#include "babeltrace2-cfg-cli-args-connect.h"

static bool all_named_and_printable_in_array(GPtrArray *comps)
{
	size_t i;
	bool all_named_and_printable = true;

	for (i = 0; i < comps->len; i++) {
		struct bt_config_component *comp = g_ptr_array_index(comps, i);

		if (comp->instance_name->len == 0) {
			all_named_and_printable = false;
			goto end;
		}

		if (!bt_common_string_is_printable(comp->instance_name->str)) {
			all_named_and_printable = false;
			goto end;
		}
	}

end:
	return all_named_and_printable;
}

static bool all_named_and_printable(struct bt_config *cfg)
{
	return all_named_and_printable_in_array(cfg->cmd_data.run.sources) &&
		all_named_and_printable_in_array(cfg->cmd_data.run.filters) &&
		all_named_and_printable_in_array(cfg->cmd_data.run.sinks);
}

static struct bt_config_connection *bt_config_connection_create(const char *arg)
{
	struct bt_config_connection *cfg_connection;

	cfg_connection = g_new0(struct bt_config_connection, 1);
	if (!cfg_connection) {
		goto error;
	}

	cfg_connection->upstream_comp_name = g_string_new(NULL);
	if (!cfg_connection->upstream_comp_name) {
		goto error;
	}

	cfg_connection->downstream_comp_name = g_string_new(NULL);
	if (!cfg_connection->downstream_comp_name) {
		goto error;
	}

	cfg_connection->upstream_port_glob = g_string_new("*");
	if (!cfg_connection->upstream_port_glob) {
		goto error;
	}

	cfg_connection->downstream_port_glob = g_string_new("*");
	if (!cfg_connection->downstream_port_glob) {
		goto error;
	}

	cfg_connection->arg = g_string_new(arg);
	if (!cfg_connection->arg) {
		goto error;
	}

	goto end;

error:
	g_free(cfg_connection);
	cfg_connection = NULL;

end:
	return cfg_connection;
}

static bool validate_port_glob(const char *port_glob)
{
	bool is_valid = true;
	const char *ch = port_glob;

	BT_ASSERT(port_glob);

	while (*ch != '\0') {
		switch (*ch) {
		case '\\':
			switch (ch[1]) {
			case '\0':
				goto end;
			default:
				ch += 2;
				continue;
			}
		case '?':
		case '[':
			/*
			 * This is reserved for future use, to support
			 * full globbing patterns. Those characters must
			 * be escaped with `\`.
			 */
			is_valid = false;
			goto end;
		default:
			ch++;
			break;
		}
	}

end:
	return is_valid;
}

static int normalize_glob_pattern(GString *glob_pattern_gs)
{
	int ret = 0;
	char *glob_pattern = strdup(glob_pattern_gs->str);

	if (!glob_pattern) {
		ret = -1;
		goto end;
	}

	bt_common_normalize_star_glob_pattern(glob_pattern);
	g_string_assign(glob_pattern_gs, glob_pattern);
	free(glob_pattern);

end:
	return ret;
}

static struct bt_config_connection *cfg_connection_from_arg(const char *arg)
{
	const char *at = arg;
	size_t end_pos;
	struct bt_config_connection *cfg_conn = NULL;
	GString *gs = NULL;
	enum {
		UPSTREAM_NAME,
		DOWNSTREAM_NAME,
		UPSTREAM_PORT_GLOB,
		DOWNSTREAM_PORT_GLOB,
	} state = UPSTREAM_NAME;

	if (!bt_common_string_is_printable(arg)) {
		goto error;
	}

	cfg_conn = bt_config_connection_create(arg);
	if (!cfg_conn) {
		goto error;
	}

	while (true) {
		switch (state) {
		case UPSTREAM_NAME:
			gs = bt_common_string_until(at, ".:\\", ".:", &end_pos);
			if (!gs || gs->len == 0) {
				goto error;
			}

			g_string_free(cfg_conn->upstream_comp_name, TRUE);
			cfg_conn->upstream_comp_name = gs;
			gs = NULL;

			if (at[end_pos] == ':') {
				state = DOWNSTREAM_NAME;
			} else if (at[end_pos] == '.') {
				state = UPSTREAM_PORT_GLOB;
			} else {
				goto error;
			}

			at += end_pos + 1;
			break;
		case DOWNSTREAM_NAME:
			gs = bt_common_string_until(at, ".:\\", ".:", &end_pos);
			if (!gs || gs->len == 0) {
				goto error;
			}

			g_string_free(cfg_conn->downstream_comp_name, TRUE);
			cfg_conn->downstream_comp_name = gs;
			gs = NULL;

			if (at[end_pos] == '.') {
				state = DOWNSTREAM_PORT_GLOB;
			} else if (at[end_pos] == '\0') {
				goto end;
			} else {
				goto error;
			}

			at += end_pos + 1;
			break;
		case UPSTREAM_PORT_GLOB:
			gs = bt_common_string_until(at, ".:", ".:", &end_pos);
			if (!gs || gs->len == 0) {
				goto error;
			}

			if (!validate_port_glob(gs->str)) {
				goto error;
			}

			if (normalize_glob_pattern(gs)) {
				goto error;
			}

			g_string_free(cfg_conn->upstream_port_glob, TRUE);
			cfg_conn->upstream_port_glob = gs;
			gs = NULL;

			if (at[end_pos] == ':') {
				state = DOWNSTREAM_NAME;
			} else {
				goto error;
			}

			at += end_pos + 1;
			break;
		case DOWNSTREAM_PORT_GLOB:
			gs = bt_common_string_until(at, ".:", ".:", &end_pos);
			if (!gs || gs->len == 0) {
				goto error;
			}

			if (!validate_port_glob(gs->str)) {
				goto error;
			}

			if (normalize_glob_pattern(gs)) {
				goto error;
			}

			g_string_free(cfg_conn->downstream_port_glob, TRUE);
			cfg_conn->downstream_port_glob = gs;
			gs = NULL;

			if (at[end_pos] == '\0') {
				goto end;
			} else {
				goto error;
			}
			break;
		default:
			bt_common_abort();
		}
	}

error:
	bt_config_connection_destroy(cfg_conn);
	cfg_conn = NULL;

end:
	if (gs) {
		g_string_free(gs, TRUE);
	}

	return cfg_conn;
}

static struct bt_config_component *find_component_in_array(GPtrArray *comps,
		const char *name)
{
	size_t i;
	struct bt_config_component *found_comp = NULL;

	for (i = 0; i < comps->len; i++) {
		struct bt_config_component *comp = g_ptr_array_index(comps, i);

		if (strcmp(name, comp->instance_name->str) == 0) {
			found_comp = comp;
			bt_object_get_ref(found_comp);
			goto end;
		}
	}

end:
	return found_comp;
}

static struct bt_config_component *find_component(struct bt_config *cfg,
		const char *name)
{
	struct bt_config_component *comp;

	comp = find_component_in_array(cfg->cmd_data.run.sources, name);
	if (comp) {
		goto end;
	}

	comp = find_component_in_array(cfg->cmd_data.run.filters, name);
	if (comp) {
		goto end;
	}

	comp = find_component_in_array(cfg->cmd_data.run.sinks, name);
	if (comp) {
		goto end;
	}

end:
	return comp;
}

static int validate_all_endpoints_exist(struct bt_config *cfg, char *error_buf,
		size_t error_buf_size)
{
	size_t i;
	int ret = 0;

	for (i = 0; i < cfg->cmd_data.run.connections->len; i++) {
		struct bt_config_connection *connection =
			g_ptr_array_index(cfg->cmd_data.run.connections, i);
		struct bt_config_component *comp;

		comp = find_component(cfg, connection->upstream_comp_name->str);
		bt_object_put_ref(comp);
		if (!comp) {
			snprintf(error_buf, error_buf_size,
				"Invalid connection: cannot find upstream component `%s`:\n    %s\n",
				connection->upstream_comp_name->str,
				connection->arg->str);
			ret = -1;
			goto end;
		}

		comp = find_component(cfg, connection->downstream_comp_name->str);
		bt_object_put_ref(comp);
		if (!comp) {
			snprintf(error_buf, error_buf_size,
				"Invalid connection: cannot find downstream component `%s`:\n    %s\n",
				connection->downstream_comp_name->str,
				connection->arg->str);
			ret = -1;
			goto end;
		}
	}

end:
	return ret;
}

static int validate_connection_directions(struct bt_config *cfg,
		char *error_buf, size_t error_buf_size)
{
	size_t i;
	int ret = 0;
	struct bt_config_component *src_comp = NULL;
	struct bt_config_component *dst_comp = NULL;

	for (i = 0; i < cfg->cmd_data.run.connections->len; i++) {
		struct bt_config_connection *connection =
			g_ptr_array_index(cfg->cmd_data.run.connections, i);

		src_comp = find_component(cfg,
			connection->upstream_comp_name->str);
		BT_ASSERT(src_comp);
		dst_comp = find_component(cfg,
			connection->downstream_comp_name->str);
		BT_ASSERT(dst_comp);

		if (src_comp->type == BT_COMPONENT_CLASS_TYPE_SOURCE) {
			if (dst_comp->type != BT_COMPONENT_CLASS_TYPE_FILTER &&
					dst_comp->type != BT_COMPONENT_CLASS_TYPE_SINK) {
				snprintf(error_buf, error_buf_size,
					"Invalid connection: source component `%s` not connected to filter or sink component:\n    %s\n",
					connection->upstream_comp_name->str,
					connection->arg->str);
				ret = -1;
				goto end;
			}
		} else if (src_comp->type == BT_COMPONENT_CLASS_TYPE_FILTER) {
			if (dst_comp->type != BT_COMPONENT_CLASS_TYPE_FILTER &&
					dst_comp->type != BT_COMPONENT_CLASS_TYPE_SINK) {
				snprintf(error_buf, error_buf_size,
					"Invalid connection: filter component `%s` not connected to filter or sink component:\n    %s\n",
					connection->upstream_comp_name->str,
					connection->arg->str);
				ret = -1;
				goto end;
			}
		} else {
			snprintf(error_buf, error_buf_size,
				"Invalid connection: cannot connect sink component `%s` to component `%s`:\n    %s\n",
				connection->upstream_comp_name->str,
				connection->downstream_comp_name->str,
				connection->arg->str);
			ret = -1;
			goto end;
		}

		BT_OBJECT_PUT_REF_AND_RESET(src_comp);
		BT_OBJECT_PUT_REF_AND_RESET(dst_comp);
	}

end:
	bt_object_put_ref(src_comp);
	bt_object_put_ref(dst_comp);
	return ret;
}

static int validate_no_cycles_rec(struct bt_config *cfg, GPtrArray *path,
		char *error_buf, size_t error_buf_size)
{
	int ret = 0;
	size_t conn_i;
	const char *src_comp_name;

	BT_ASSERT(path && path->len > 0);
	src_comp_name = g_ptr_array_index(path, path->len - 1);

	for (conn_i = 0; conn_i < cfg->cmd_data.run.connections->len; conn_i++) {
		struct bt_config_connection *conn =
			g_ptr_array_index(cfg->cmd_data.run.connections, conn_i);

		if (strcmp(conn->upstream_comp_name->str, src_comp_name) == 0) {
			size_t path_i;

			for (path_i = 0; path_i < path->len; path_i++) {
				const char *comp_name =
					g_ptr_array_index(path, path_i);

				if (strcmp(comp_name, conn->downstream_comp_name->str) == 0) {
					snprintf(error_buf, error_buf_size,
						"Invalid connection: connection forms a cycle:\n    %s\n",
						conn->arg->str);
					ret = -1;
					goto end;
				}
			}

			g_ptr_array_add(path, conn->downstream_comp_name->str);
			ret = validate_no_cycles_rec(cfg, path, error_buf,
					error_buf_size);
			if (ret) {
				goto end;
			}

			g_ptr_array_remove_index(path, path->len - 1);
		}
	}

end:
	return ret;
}

static int validate_no_cycles(struct bt_config *cfg, char *error_buf,
		size_t error_buf_size)
{
	size_t i;
	int ret = 0;
	GPtrArray *path;

	path = g_ptr_array_new();
	if (!path) {
		ret = -1;
		goto end;
	}

	g_ptr_array_add(path, NULL);

	for (i = 0; i < cfg->cmd_data.run.connections->len; i++) {
		struct bt_config_connection *conn =
			g_ptr_array_index(cfg->cmd_data.run.connections, i);

		g_ptr_array_index(path, 0) = conn->upstream_comp_name->str;
		ret = validate_no_cycles_rec(cfg, path,
			error_buf, error_buf_size);
		if (ret) {
			goto end;
		}
	}

end:
	if (path) {
		g_ptr_array_free(path, TRUE);
	}

	return ret;
}

static int validate_all_components_connected_in_array(GPtrArray *comps,
		const bt_value *connected_components,
		char *error_buf, size_t error_buf_size)
{
	int ret = 0;
	size_t i;

	for (i = 0; i < comps->len; i++) {
		struct bt_config_component *comp = g_ptr_array_index(comps, i);

		if (!bt_value_map_has_entry(connected_components,
				comp->instance_name->str)) {
			snprintf(error_buf, error_buf_size,
				"Component `%s` is not connected\n",
				comp->instance_name->str);
			ret = -1;
			goto end;
		}
	}

end:
	return ret;
}

static int validate_all_components_connected(struct bt_config *cfg,
		char *error_buf, size_t error_buf_size)
{
	size_t i;
	int ret = 0;
	bt_value *connected_components = bt_value_map_create();

	if (!connected_components) {
		ret = -1;
		goto end;
	}

	for (i = 0; i < cfg->cmd_data.run.connections->len; i++) {
		struct bt_config_connection *connection =
			g_ptr_array_index(cfg->cmd_data.run.connections, i);

		ret = bt_value_map_insert_entry(connected_components,
			connection->upstream_comp_name->str, bt_value_null);
		if (ret) {
			goto end;
		}

		ret = bt_value_map_insert_entry(connected_components,
			connection->downstream_comp_name->str, bt_value_null);
		if (ret) {
			goto end;
		}
	}

	ret = validate_all_components_connected_in_array(
		cfg->cmd_data.run.sources,
		connected_components,
		error_buf, error_buf_size);
	if (ret) {
		goto end;
	}

	ret = validate_all_components_connected_in_array(
		cfg->cmd_data.run.filters,
		connected_components,
		error_buf, error_buf_size);
	if (ret) {
		goto end;
	}

	ret = validate_all_components_connected_in_array(
		cfg->cmd_data.run.sinks,
		connected_components,
		error_buf, error_buf_size);
	if (ret) {
		goto end;
	}

end:
	bt_value_put_ref(connected_components);
	return ret;
}

static int validate_no_duplicate_connection(struct bt_config *cfg,
		char *error_buf, size_t error_buf_size)
{
	size_t i;
	int ret = 0;
	bt_value *flat_connection_names =
		bt_value_map_create();
	GString *flat_connection_name = NULL;

	if (!flat_connection_names) {
		ret = -1;
		goto end;
	}

	flat_connection_name = g_string_new(NULL);
	if (!flat_connection_name) {
		ret = -1;
		goto end;
	}

	for (i = 0; i < cfg->cmd_data.run.connections->len; i++) {
		struct bt_config_connection *connection =
			g_ptr_array_index(cfg->cmd_data.run.connections, i);

		g_string_printf(flat_connection_name, "%s\x01%s\x01%s\x01%s",
			connection->upstream_comp_name->str,
			connection->upstream_port_glob->str,
			connection->downstream_comp_name->str,
			connection->downstream_port_glob->str);

		if (bt_value_map_has_entry(flat_connection_names,
					   flat_connection_name->str)) {
			snprintf(error_buf, error_buf_size,
				"Duplicate connection:\n    %s\n",
				connection->arg->str);
			ret = -1;
			goto end;
		}

		ret = bt_value_map_insert_entry(flat_connection_names,
			flat_connection_name->str, bt_value_null);
		if (ret) {
			goto end;
		}
	}

end:
	bt_value_put_ref(flat_connection_names);

	if (flat_connection_name) {
		g_string_free(flat_connection_name, TRUE);
	}

	return ret;
}

static int validate_connections(struct bt_config *cfg, char *error_buf,
		size_t error_buf_size)
{
	int ret;

	ret = validate_all_endpoints_exist(cfg, error_buf, error_buf_size);
	if (ret) {
		goto end;
	}

	ret = validate_connection_directions(cfg, error_buf, error_buf_size);
	if (ret) {
		goto end;
	}

	ret = validate_all_components_connected(cfg, error_buf, error_buf_size);
	if (ret) {
		goto end;
	}

	ret = validate_no_duplicate_connection(cfg, error_buf, error_buf_size);
	if (ret) {
		goto end;
	}

	ret = validate_no_cycles(cfg, error_buf, error_buf_size);
	if (ret) {
		goto end;
	}

end:
	return ret;
}

int bt_config_cli_args_create_connections(struct bt_config *cfg,
		const bt_value *connection_args,
		char *error_buf, size_t error_buf_size)
{
	int ret;
	uint64_t i;

	if (!all_named_and_printable(cfg)) {
		snprintf(error_buf, error_buf_size,
			"One or more components are unnamed (use --name) or contain a non-printable character\n");
		goto error;
	}

	for (i = 0; i < bt_value_array_get_length(connection_args); i++) {
		const bt_value *arg_value =
			bt_value_array_borrow_element_by_index_const(
				connection_args, i);
		const char *arg;
		struct bt_config_connection *cfg_connection;

		arg = bt_value_string_get(arg_value);
		cfg_connection = cfg_connection_from_arg(arg);
		if (!cfg_connection) {
			snprintf(error_buf, error_buf_size, "Cannot parse --connect option's argument:\n    %s\n",
				arg);
			goto error;
		}

		g_ptr_array_add(cfg->cmd_data.run.connections,
			cfg_connection);
	}


	ret = validate_connections(cfg, error_buf, error_buf_size);
	if (ret) {
		goto error;
	}

	goto end;

error:
	ret = -1;

end:
	return ret;
}
