/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2014 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#ifdef AS_BUILD_DEP11
#include <yaml.h>
#endif

#include "as-node.h"
#include "as-ref-string.h"
#include "as-yaml.h"

typedef enum {
	AS_YAML_NODE_KIND_UNKNOWN,
	AS_YAML_NODE_KIND_MAP,
	AS_YAML_NODE_KIND_SEQ,
	AS_YAML_NODE_KIND_KEY,
	AS_YAML_NODE_KIND_KEY_VALUE,
	AS_YAML_NODE_KIND_LAST
} AsYamlNodeKind;

typedef struct {
	AsRefString	*key;
	AsRefString	*value;
	AsYamlNodeKind	 kind;
} AsYamlNode;

const gchar *
as_yaml_node_get_key (const AsNode *node)
{
	AsYamlNode *ym;
	if (node == NULL)
		return NULL;
	ym = node->data;
	if (ym == NULL)
		return NULL;
	if (ym->key == NULL || ym->key[0] == '\0')
		return NULL;
	return ym->key;
}

const gchar *
as_yaml_node_get_value (const AsNode *node)
{
	AsYamlNode *ym;
	if (node == NULL)
		return NULL;
	ym = node->data;
	if (ym == NULL)
		return NULL;
	if (ym->value == NULL || ym->value[0] == '\0')
		return NULL;
	return ym->value;
}

gint
as_yaml_node_get_value_as_int (const AsNode *node)
{
	const gchar *tmp;
	gchar *endptr = NULL;
	gint64 value_tmp;

	tmp = as_yaml_node_get_value (node);
	if (tmp == NULL)
		return G_MAXINT;
	value_tmp = g_ascii_strtoll (tmp, &endptr, 10);
	if (value_tmp == 0 && tmp == endptr)
		return G_MAXINT;
	if (value_tmp > G_MAXINT || value_tmp < G_MININT)
		return G_MAXINT;
	return (gint) value_tmp;
}

guint
as_yaml_node_get_value_as_uint (const AsNode *node)
{
	const gchar *tmp;
	gchar *endptr = NULL;
	guint64 value_tmp;

	tmp = as_yaml_node_get_value (node);
	if (tmp == NULL)
		return G_MAXUINT;
	value_tmp = g_ascii_strtoull (tmp, &endptr, 10);
	if (value_tmp == 0 && tmp == endptr)
		return G_MAXUINT;
	if (value_tmp > G_MAXUINT)
		return G_MAXUINT;
	return (guint) value_tmp;
}

static gboolean
as_node_yaml_destroy_node_cb (AsNode *node, gpointer data)
{
	AsYamlNode *ym = node->data;
	if (ym == NULL)
		return FALSE;
	if (ym->key != NULL)
		as_ref_string_unref (ym->key);
	if (ym->value != NULL)
		as_ref_string_unref (ym->value);
	g_slice_free (AsYamlNode, ym);
	return FALSE;
}

void
as_yaml_unref (AsNode *node)
{
	g_node_traverse (node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
			 as_node_yaml_destroy_node_cb, NULL);
	g_node_destroy (node);
}

static gboolean
as_yaml_to_string_cb (AsNode *node, gpointer data)
{
	AsYamlNode *ym = node->data;
	GString *str = (GString *) data;
	guint depth;
	guint i;

	depth = g_node_depth (node);
	if (depth >= 2) {
		for (i = 0; i < depth - 2; i++)
			g_string_append (str, " ");
	}
	if (ym == NULL)
		return FALSE;
	switch (ym->kind) {
	case AS_YAML_NODE_KIND_MAP:
		g_string_append (str, "[MAP]");
		break;
	case AS_YAML_NODE_KIND_SEQ:
		g_string_append (str, "[SEQ]");
		break;
	case AS_YAML_NODE_KIND_KEY:
		g_string_append (str, "[KEY]");
		break;
	case AS_YAML_NODE_KIND_KEY_VALUE:
		g_string_append (str, "[KVL]");
		break;
	case AS_YAML_NODE_KIND_UNKNOWN:
	default:
		g_string_append (str, "???: ");
		break;
	}

	if (ym->value != NULL) {
		g_string_append_printf (str, "%s=%s\n", ym->key, ym->value);
		return FALSE;
	}
	g_string_append_printf (str, "%s\n", ym->key);
	return FALSE;
}

GString *
as_yaml_to_string (AsNode *node)
{
	GString *str = g_string_new ("");
	g_node_traverse (node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
			 as_yaml_to_string_cb, str);
	return str;
}

#ifdef AS_BUILD_DEP11
static AsYamlNodeKind
as_yaml_node_get_kind (AsNode *node)
{
	AsYamlNode *ym;
	if (node == NULL)
		return AS_YAML_NODE_KIND_UNKNOWN;
	ym = node->data;
	if (ym == NULL)
		return AS_YAML_NODE_KIND_UNKNOWN;
	return ym->kind;
}

static AsYamlNode *
as_yaml_node_new (AsYamlNodeKind kind, AsRefString *id)
{
	AsYamlNode *ym;
	ym = g_slice_new0 (AsYamlNode);
	ym->kind = kind;
	if (id != NULL)
		ym->key = as_ref_string_ref (id);
	return ym;
}

static gchar *
as_yaml_mark_to_str (yaml_mark_t *mark)
{
	return g_strdup_printf ("ln:%" G_GSIZE_FORMAT " col:%" G_GSIZE_FORMAT,
				mark->line + 1,
				mark->column + 1);
}

static gboolean
as_yaml_parser_error_to_gerror (yaml_parser_t *parser, GError **error)
{
	g_autofree gchar *problem_str = NULL;
	g_autofree gchar *context_str = NULL;

	switch (parser->error) {
	case YAML_MEMORY_ERROR:
		g_set_error_literal (error,
				     AS_NODE_ERROR,
				     AS_NODE_ERROR_INVALID_MARKUP,
				     "not enough memory for parsing");
		break;

	case YAML_READER_ERROR:
		if (parser->problem_value != -1) {
			g_set_error (error,
				     AS_NODE_ERROR,
				     AS_NODE_ERROR_INVALID_MARKUP,
				     "reader error: %s: #%X at %" G_GSIZE_FORMAT "",
				     parser->problem,
				     (guint) parser->problem_value,
				     parser->problem_offset);
		} else {
			g_set_error (error,
				     AS_NODE_ERROR,
				     AS_NODE_ERROR_INVALID_MARKUP,
				     "reader error: %s at %" G_GSIZE_FORMAT "",
				     parser->problem,
				     parser->problem_offset);
		}
		break;

	case YAML_SCANNER_ERROR:
		problem_str = as_yaml_mark_to_str (&parser->problem_mark);
		if (parser->context) {
			context_str = as_yaml_mark_to_str (&parser->context_mark);
			g_set_error (error,
				     AS_NODE_ERROR,
				     AS_NODE_ERROR_INVALID_MARKUP,
				     "scanner error: %s at %s, %s at %s",
				     parser->context,
				     context_str,
				     parser->problem,
				     problem_str);
		} else {
			g_set_error (error,
				     AS_NODE_ERROR,
				     AS_NODE_ERROR_INVALID_MARKUP,
				     "scanner error: %s at %s ",
				     parser->problem,
				     problem_str);
		}
		break;
	case YAML_PARSER_ERROR:
		problem_str = as_yaml_mark_to_str (&parser->problem_mark);
		if (parser->context) {
			context_str = as_yaml_mark_to_str (&parser->context_mark);
			g_set_error (error,
				     AS_NODE_ERROR,
				     AS_NODE_ERROR_INVALID_MARKUP,
				     "parser error: %s at %s, %s at %s",
				     parser->context,
				     context_str,
				     parser->problem,
				     problem_str);
		} else {
			g_set_error (error,
				     AS_NODE_ERROR,
				     AS_NODE_ERROR_INVALID_MARKUP,
				     "parser error: %s at %s ",
				     parser->problem,
				     problem_str);
		}
		break;
	case YAML_COMPOSER_ERROR:
		problem_str = as_yaml_mark_to_str (&parser->problem_mark);
		if (parser->context) {
			context_str = as_yaml_mark_to_str (&parser->context_mark);
			g_set_error (error,
				     AS_NODE_ERROR,
				     AS_NODE_ERROR_INVALID_MARKUP,
				     "composer error: %s at %s, %s at %s",
				     parser->context,
				     context_str,
				     parser->problem,
				     problem_str);
		} else {
			g_set_error (error,
				     AS_NODE_ERROR,
				     AS_NODE_ERROR_INVALID_MARKUP,
				     "composer error: %s at %s ",
				     parser->problem, problem_str);
		}
		break;
	default:
		/* can't happen */
		g_set_error_literal (error,
				     AS_NODE_ERROR,
				     AS_NODE_ERROR_INVALID_MARKUP,
				     "internal error");
		break;
	}
	return FALSE;
}

typedef struct {
	AsYamlFromFlags		 flags;
	const gchar * const	*locales;
	yaml_parser_t		*parser;
} AsYamlContext;

static gboolean
as_yaml_node_valid (AsYamlContext *ctx, AsNode *parent, const gchar *key)
{
	const gchar *sections[] = { "Name", "Summary", "Description", NULL };
	AsYamlNode *ym = parent->data;

	/* if native filtering enabled */
	if ((ctx->flags & AS_YAML_FROM_FLAG_ONLY_NATIVE_LANGS) == 0)
		return TRUE;

	/* filter by translatable sections */
	if (!g_strv_contains (sections, ym->key))
		return TRUE;

	/* non-native language */
	if (g_strv_contains (ctx->locales, key))
		return TRUE;

	return FALSE;
}

static gboolean
as_node_yaml_process_layer (AsYamlContext *ctx, AsNode *parent, GError **error)
{
	AsYamlNode *ym;
	AsNode *last_scalar = NULL;
	AsNode *new;
	const gchar *tmp;
	gboolean valid = TRUE;
	yaml_event_t event;
	AsRefString *map_str = as_ref_string_new_static ("{");
	AsRefString *seq_str = as_ref_string_new_static ("[");

	while (valid) {

		/* process event */
		if (!yaml_parser_parse (ctx->parser, &event))
			return as_yaml_parser_error_to_gerror (ctx->parser, error);

		switch (event.type) {
		case YAML_SCALAR_EVENT:
			tmp = (const gchar *) event.data.scalar.value;
			if (as_yaml_node_get_kind (parent) != AS_YAML_NODE_KIND_SEQ &&
			    last_scalar != NULL) {
				ym = last_scalar->data;
				if (ym->key != NULL)
					ym->value = as_ref_string_new (tmp);
				ym->kind = AS_YAML_NODE_KIND_KEY_VALUE;
				last_scalar = NULL;
			} else {
				if (as_yaml_node_valid (ctx, parent, tmp)) {
					g_autoptr(AsRefString) rstr = as_ref_string_new (tmp);
					ym = as_yaml_node_new (AS_YAML_NODE_KIND_KEY, rstr);
				} else {
					ym = as_yaml_node_new (AS_YAML_NODE_KIND_KEY, NULL);
				}
				last_scalar = g_node_append_data (parent, ym);
			}
			break;
		case YAML_MAPPING_START_EVENT:
			if (last_scalar == NULL) {
				ym = as_yaml_node_new (AS_YAML_NODE_KIND_MAP, map_str);
				new = g_node_append_data (parent, ym);
			} else {
				ym = last_scalar->data;
				ym->kind = AS_YAML_NODE_KIND_MAP;
				new = last_scalar;
			}
			if (!as_node_yaml_process_layer (ctx, new, error))
				return FALSE;
			last_scalar = NULL;
			break;
		case YAML_SEQUENCE_START_EVENT:
			if (last_scalar == NULL) {
				ym = as_yaml_node_new (AS_YAML_NODE_KIND_SEQ, seq_str);
				new = g_node_append_data (parent, ym);
			} else {
				ym = last_scalar->data;
				ym->kind = AS_YAML_NODE_KIND_SEQ;
				new = last_scalar;
			}
			if (!as_node_yaml_process_layer (ctx, new, error))
				return FALSE;
			last_scalar = NULL;
			break;
		case YAML_STREAM_START_EVENT:
			break;
		case YAML_MAPPING_END_EVENT:
		case YAML_SEQUENCE_END_EVENT:
		case YAML_STREAM_END_EVENT:
			valid = FALSE;
			break;
		default:
			break;
		}
		yaml_event_delete (&event);
	}
	return TRUE;
}

typedef yaml_parser_t* AsYamlParser;
G_DEFINE_AUTO_CLEANUP_FREE_FUNC(AsYamlParser, yaml_parser_delete, NULL);

#endif

AsNode *
as_yaml_from_data (const gchar *data,
		   gssize data_len,
		   AsYamlFromFlags flags,
		   GError **error)
{
	g_autoptr(AsYaml) node = NULL;
#ifdef AS_BUILD_DEP11
	AsYamlContext ctx;
	yaml_parser_t parser;
	g_auto(AsYamlParser) parser_cleanup = NULL;

	/* parse */
	if (!yaml_parser_initialize (&parser)) {
		as_yaml_parser_error_to_gerror (&parser, error);
		return NULL;
	}
	parser_cleanup = &parser;
	g_assert (parser_cleanup != NULL);
	if (data_len < 0)
		data_len = (gssize) strlen (data);
	yaml_parser_set_input_string (&parser, (guchar *) data, (gsize) data_len);
	node = g_node_new (NULL);
	ctx.parser = &parser;
	ctx.flags = flags;
	ctx.locales = g_get_language_names ();
	if (!as_node_yaml_process_layer (&ctx, node, error))
		return NULL;
#else
	g_set_error_literal (error,
			     AS_NODE_ERROR,
			     AS_NODE_ERROR_NO_SUPPORT,
			     "No DEP-11 support, needs libyaml");
#endif
	return g_steal_pointer (&node);
}

#ifdef AS_BUILD_DEP11
static int
as_yaml_read_handler_cb (void *data,
			 unsigned char *buffer,
			 size_t size,
			 size_t *size_read)
{
	GInputStream *stream = G_INPUT_STREAM (data);
	gssize len = g_input_stream_read (stream,
					  buffer,
					  (gsize) size,
					  NULL,
					  NULL);
	if (len < 0)
		return 0;
	*size_read = (gsize) len;
	return 1;
}
#endif

AsNode *
as_yaml_from_file (GFile *file, AsYamlFromFlags flags, GCancellable *cancellable, GError **error)
{
	g_autoptr(AsYaml) node = NULL;
#ifdef AS_BUILD_DEP11
	const gchar *content_type = NULL;
	yaml_parser_t parser;
	g_auto(AsYamlParser) parser_cleanup = NULL;
	g_autofree gchar *data = NULL;
	g_autoptr(GConverter) conv = NULL;
	g_autoptr(GFileInfo) info = NULL;
	g_autoptr(GInputStream) file_stream = NULL;
	g_autoptr(GInputStream) stream_data = NULL;
	AsYamlContext ctx;

	/* what kind of file is this */
	info = g_file_query_info (file,
				  G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
				  G_FILE_QUERY_INFO_NONE,
				  cancellable,
				  error);
	if (info == NULL)
		return NULL;

	/* decompress if required */
	file_stream = G_INPUT_STREAM (g_file_read (file, cancellable, error));
	if (file_stream == NULL)
		return NULL;
	content_type = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);
	if (g_strcmp0 (content_type, "application/gzip") == 0 ||
	    g_strcmp0 (content_type, "application/x-gzip") == 0) {
		conv = G_CONVERTER (g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP));
		stream_data = g_converter_input_stream_new (file_stream, conv);
	} else if (g_strcmp0 (content_type, "application/x-yaml") == 0 ||
			   g_strcmp0 (content_type, "application/yaml") == 0) {
		stream_data = g_object_ref (file_stream);
	} else {
		g_set_error (error,
			     AS_NODE_ERROR,
			     AS_NODE_ERROR_FAILED,
			     "cannot process file of type %s",
			     content_type);
		return NULL;
	}

	/* parse */
	if (!yaml_parser_initialize (&parser)) {
		as_yaml_parser_error_to_gerror (&parser, error);
		return NULL;
	}
	parser_cleanup = &parser;
	g_assert (parser_cleanup != NULL);
	yaml_parser_set_input (&parser, as_yaml_read_handler_cb, stream_data);
	node = g_node_new (NULL);
	ctx.parser = &parser;
	ctx.flags = flags;
	ctx.locales = g_get_language_names ();
	if (!as_node_yaml_process_layer (&ctx, node, error))
		return NULL;
#else
	g_set_error_literal (error,
			     AS_NODE_ERROR,
			     AS_NODE_ERROR_NO_SUPPORT,
			     "No DEP-11 support, needs libyaml");
#endif
	return g_steal_pointer (&node);
}
