/*
 * Copyright 2018 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#define G_LOG_DOMAIN "XbSilo"

#include "config.h"

#include <gio/gio.h>
#include <string.h>

#include "xb-builder-fixup-private.h"
#include "xb-builder-source-ctx-private.h"
#include "xb-builder-source-private.h"
#ifdef HAVE_LZMA
#include "xb-lzma-decompressor.h"
#endif
#ifdef HAVE_ZSTD
#include "xb-zstd-decompressor.h"
#endif
typedef struct {
	GInputStream *istream;
	GFile *file;
	GPtrArray *fixups;   /* of XbBuilderFixup */
	GPtrArray *adapters; /* of XbBuilderSourceAdapter */
	XbBuilderNode *info;
	gchar *guid;
	gchar *prefix;
	gchar *content_type;
	XbBuilderSourceFlags flags;
} XbBuilderSourcePrivate;

G_DEFINE_TYPE_WITH_PRIVATE(XbBuilderSource, xb_builder_source, G_TYPE_OBJECT)
#define GET_PRIVATE(o) (xb_builder_source_get_instance_private(o))

typedef struct {
	gchar *content_type;
	XbBuilderSourceAdapterFunc func_adapter;
	gpointer user_data;
	GDestroyNotify user_data_free;
	gboolean is_simple;
} XbBuilderSourceAdapter;

static XbBuilderSourceAdapter *
xb_builder_source_get_adapter_by_mime(XbBuilderSource *self, const gchar *content_type)
{
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);
	for (guint i = 0; i < priv->adapters->len; i++) {
		XbBuilderSourceAdapter *item = g_ptr_array_index(priv->adapters, i);
		if (item->func_adapter == NULL)
			continue;
		if (g_strcmp0(item->content_type, content_type) == 0)
			return item;
	}
	return NULL;
}

/**
 * xb_builder_source_load_file:
 * @self: a #XbBuilderSource
 * @file: a #GFile
 * @flags: some #XbBuilderSourceFlags, e.g. %XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT
 * @cancellable: a #GCancellable, or %NULL
 * @error: the #GError, or %NULL
 *
 * Loads an optionally compressed XML file to build a #XbSilo.
 *
 * Returns: %TRUE for success
 *
 * Since: 0.1.1
 **/
gboolean
xb_builder_source_load_file(XbBuilderSource *self,
			    GFile *file,
			    XbBuilderSourceFlags flags,
			    GCancellable *cancellable,
			    GError **error)
{
	const gchar *content_type = NULL;
	guint32 ctime_usec;
	guint64 ctime;
	g_autofree gchar *fn = NULL;
	g_autoptr(GFileInfo) fileinfo = NULL;
	g_autoptr(GString) guid = NULL;
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);

	g_return_val_if_fail(XB_IS_BUILDER_SOURCE(self), FALSE);
	g_return_val_if_fail(G_IS_FILE(file), FALSE);
	g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* what kind of file is this */
	fileinfo = g_file_query_info(file,
				     G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE
				     "," G_FILE_ATTRIBUTE_TIME_CHANGED
				     "," G_FILE_ATTRIBUTE_TIME_CHANGED_USEC,
				     G_FILE_QUERY_INFO_NONE,
				     cancellable,
				     error);
	if (fileinfo == NULL)
		return FALSE;

	/* add data to GUID */
	fn = g_file_get_path(file);
	guid = g_string_new(fn);
	ctime = g_file_info_get_attribute_uint64(fileinfo, G_FILE_ATTRIBUTE_TIME_CHANGED);
	if (ctime != 0)
		g_string_append_printf(guid, ":ctime=%" G_GUINT64_FORMAT, ctime);
	ctime_usec = g_file_info_get_attribute_uint32(fileinfo, G_FILE_ATTRIBUTE_TIME_CHANGED_USEC);
	if (ctime_usec != 0)
		g_string_append_printf(guid, ".%" G_GUINT32_FORMAT, ctime_usec);
	priv->guid = g_string_free(g_steal_pointer(&guid), FALSE);

	/* check content type of file */
	content_type =
	    g_file_info_get_attribute_string(fileinfo, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);
	if (content_type == NULL) {
		g_set_error_literal(error,
				    G_IO_ERROR,
				    G_IO_ERROR_NOT_SUPPORTED,
				    "cannot get content type for file");
		return FALSE;
	}

	/* success */
	priv->flags = flags;
	priv->content_type = g_strdup(content_type);
	priv->file = g_object_ref(file);
	return TRUE;
}

/**
 * xb_builder_source_set_info:
 * @self: a #XbBuilderSource
 * @info: (allow-none): a #XbBuilderNode
 *
 * Sets an optional information metadata node on the root node.
 *
 * Since: 0.1.0
 **/
void
xb_builder_source_set_info(XbBuilderSource *self, XbBuilderNode *info)
{
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);
	g_return_if_fail(XB_IS_BUILDER_SOURCE(self));
	g_set_object(&priv->info, info);
}

/**
 * xb_builder_source_set_prefix:
 * @self: a #XbBuilderSource
 * @prefix: (allow-none): an XPath prefix, e.g. `installed`
 *
 * Sets an optional prefix on the root node. This makes any nodes added
 * using this source reside under a common shared parent node.
 *
 * Since: 0.1.0
 **/
void
xb_builder_source_set_prefix(XbBuilderSource *self, const gchar *prefix)
{
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);
	g_return_if_fail(XB_IS_BUILDER_SOURCE(self));
	g_free(priv->prefix);
	priv->prefix = g_strdup(prefix);
}

/**
 * xb_builder_source_load_xml:
 * @self: a #XbBuilderSource
 * @xml: XML data
 * @flags: some #XbBuilderSourceFlags, e.g. %XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT
 * @error: the #GError, or %NULL
 *
 * Loads XML data and begins to build a #XbSilo.
 *
 * Returns: %TRUE for success
 *
 * Since: 0.1.1
 **/
gboolean
xb_builder_source_load_xml(XbBuilderSource *self,
			   const gchar *xml,
			   XbBuilderSourceFlags flags,
			   GError **error)
{
	g_autoptr(GBytes) blob = NULL;
	g_autoptr(GChecksum) csum = g_checksum_new(G_CHECKSUM_SHA1);
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);

	g_return_val_if_fail(XB_IS_BUILDER_SOURCE(self), FALSE);
	g_return_val_if_fail(xml != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* add a GUID of the SHA1 hash of the entire string */
	g_checksum_update(csum, (const guchar *)xml, -1);
	priv->guid = g_strdup(g_checksum_get_string(csum));

	/* create input stream */
	blob = g_bytes_new(xml, strlen(xml));
	priv->istream = g_memory_input_stream_new_from_bytes(blob);
	if (priv->istream == NULL)
		return FALSE;

	/* success */
	priv->flags = flags;
	return TRUE;
}

/**
 * xb_builder_source_load_bytes:
 * @self: a #XbBuilderSource
 * @bytes: a #GBytes
 * @flags: some #XbBuilderSourceFlags, e.g. %XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT
 * @error: the #GError, or %NULL
 *
 * Loads XML data and begins to build a #XbSilo.
 *
 * Returns: %TRUE for success
 *
 * Since: 0.1.2
 **/
gboolean
xb_builder_source_load_bytes(XbBuilderSource *self,
			     GBytes *bytes,
			     XbBuilderSourceFlags flags,
			     GError **error)
{
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);
	g_autoptr(GChecksum) csum = g_checksum_new(G_CHECKSUM_SHA1);

	g_return_val_if_fail(XB_IS_BUILDER_SOURCE(self), FALSE);
	g_return_val_if_fail(bytes != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* add a GUID of the SHA1 hash of the entire blob */
	g_checksum_update(csum,
			  (const guchar *)g_bytes_get_data(bytes, NULL),
			  (gssize)g_bytes_get_size(bytes));
	priv->guid = g_strdup(g_checksum_get_string(csum));

	/* create input stream */
	priv->istream = g_memory_input_stream_new_from_bytes(bytes);
	if (priv->istream == NULL)
		return FALSE;

	/* success */
	priv->flags = flags;
	return TRUE;
}

/**
 * xb_builder_source_add_fixup:
 * @self: a #XbBuilderSource
 * @fixup: a #XbBuilderFixup
 *
 * Adds a function that will get run on every #XbBuilderNode compile creates
 * with this source.
 *
 * Since: 0.1.3
 **/
void
xb_builder_source_add_fixup(XbBuilderSource *self, XbBuilderFixup *fixup)
{
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);
	g_return_if_fail(XB_IS_BUILDER_SOURCE(self));
	g_return_if_fail(XB_IS_BUILDER_FIXUP(fixup));
	g_ptr_array_add(priv->fixups, g_object_ref(fixup));
}

static void
xb_builder_source_init_adapter(XbBuilderSource *self,
			       const gchar *content_types,
			       XbBuilderSourceAdapterFunc func,
			       gpointer user_data,
			       GDestroyNotify user_data_free,
			       gboolean is_simple)
{
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);
	g_auto(GStrv) split = NULL;

	g_return_if_fail(XB_IS_BUILDER_SOURCE(self));
	g_return_if_fail(content_types != NULL);
	g_return_if_fail(func != NULL);

	/* add each */
	split = g_strsplit(content_types, ",", -1);
	for (guint i = 0; split[i] != NULL; i++) {
		XbBuilderSourceAdapter *item;
		item = g_slice_new0(XbBuilderSourceAdapter);
		item->content_type = g_strdup(split[i]);
		item->func_adapter = func;
		item->user_data = user_data;
		item->user_data_free = user_data_free;
		item->is_simple = is_simple;
		g_ptr_array_add(priv->adapters, item);
	}
}

/**
 * xb_builder_source_add_adapter:
 * @self: a #XbBuilderSource
 * @content_types: mimetypes, e.g. `application/x-desktop,application/gzip`
 * @func: a callback, or %NULL
 * @user_data: user pointer to pass to @func, or %NULL
 * @user_data_free: a function which gets called to free @user_data, or %NULL
 *
 * Adds a function that can be used to convert streams loaded with
 * xb_builder_source_load_xml().
 *
 * This will decompress multiple layers of content, for instance decompressing a gzip stream into a
 * different content type that can then be parsed. Use xb_builder_source_add_simple_adapter() when
 * this recursive behaviour is not desired.
 *
 * Since: 0.1.7
 **/
void
xb_builder_source_add_adapter(XbBuilderSource *self,
			      const gchar *content_types,
			      XbBuilderSourceAdapterFunc func,
			      gpointer user_data,
			      GDestroyNotify user_data_free)
{
	xb_builder_source_init_adapter(self, content_types, func, user_data, user_data_free, FALSE);
}

/**
 * xb_builder_source_add_simple_adapter:
 * @self: a #XbBuilderSource
 * @content_types: mimetypes, e.g. `application/x-desktop,application/gzip`
 * @func: a callback, or %NULL
 * @user_data: user pointer to pass to @func, or %NULL
 * @user_data_free: a function which gets called to free @user_data, or %NULL
 *
 * Adds a function that can be used to convert streams loaded with
 * xb_builder_source_load_xml().
 *
 * This function is similar to xb_builder_source_add_adapter() but is limited to one "layer" of
 * content, for instance handling application/xml or a single simple type added using
 * xb_builder_source_add_adapter().
 *
 * Since: 0.1.15
 **/
void
xb_builder_source_add_simple_adapter(XbBuilderSource *self,
				     const gchar *content_types,
				     XbBuilderSourceAdapterFunc func,
				     gpointer user_data,
				     GDestroyNotify user_data_free)
{
	xb_builder_source_init_adapter(self, content_types, func, user_data, user_data_free, TRUE);
}

gboolean
xb_builder_source_fixup(XbBuilderSource *self, XbBuilderNode *bn, GError **error)
{
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);
	for (guint i = 0; i < priv->fixups->len; i++) {
		XbBuilderFixup *fixup = g_ptr_array_index(priv->fixups, i);
		if (!xb_builder_fixup_node(fixup, bn, error))
			return FALSE;
	}
	return TRUE;
}

static gboolean
xb_builder_source_info_guid_cb(XbBuilderNode *bn, gpointer data)
{
	GString *str = (GString *)data;
	if (xb_builder_node_get_text(bn) != NULL) {
		g_string_append_printf(str,
				       ":%s=%s",
				       xb_builder_node_get_element(bn),
				       xb_builder_node_get_text(bn));
	}
	return FALSE;
}

gchar *
xb_builder_source_get_guid(XbBuilderSource *self)
{
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);
	g_autoptr(GString) str = g_string_new(priv->guid);

	g_return_val_if_fail(XB_IS_BUILDER_SOURCE(self), NULL);

	/* append function IDs */
	for (guint i = 0; i < priv->fixups->len; i++) {
		XbBuilderFixup *fixup = g_ptr_array_index(priv->fixups, i);
		g_autofree gchar *tmp = xb_builder_fixup_get_guid(fixup);
		g_string_append_printf(str, ":%s", tmp);
	}

	/* append any info */
	if (priv->info != NULL) {
		xb_builder_node_traverse(priv->info,
					 G_PRE_ORDER,
					 G_TRAVERSE_ALL,
					 -1,
					 xb_builder_source_info_guid_cb,
					 str);
	}

	/* append prefix */
	if (priv->prefix != NULL)
		g_string_append_printf(str, ":prefix=%s", priv->prefix);
	return g_string_free(g_steal_pointer(&str), FALSE);
}

const gchar *
xb_builder_source_get_prefix(XbBuilderSource *self)
{
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(XB_IS_BUILDER_SOURCE(self), NULL);
	return priv->prefix;
}

XbBuilderNode *
xb_builder_source_get_info(XbBuilderSource *self)
{
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(XB_IS_BUILDER_SOURCE(self), NULL);
	return priv->info;
}

/* converts foo.xml.gz to foo.xml */
static void
xb_builder_source_remove_last_extension(gchar *basename)
{
	gchar *tmp = g_strrstr(basename, ".");
	if (tmp != NULL)
		*tmp = '\0';
}

GInputStream *
xb_builder_source_get_istream(XbBuilderSource *self, GCancellable *cancellable, GError **error)
{
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);
	g_autofree gchar *basename = NULL;
	g_autoptr(GInputStream) istream = NULL;
	GFile *file;

	g_return_val_if_fail(XB_IS_BUILDER_SOURCE(self), NULL);

	/* nothing required */
	if (priv->istream != NULL)
		return g_object_ref(priv->istream);

	/* convert the file to a GFileInputStream */
	istream = G_INPUT_STREAM(g_file_read(priv->file, cancellable, error));
	if (istream == NULL)
		return NULL;

	/* run the content type handlers until we get application/xml */
	basename = g_file_get_basename(priv->file);
	file = priv->file;

	do {
		XbBuilderSourceAdapter *item;
		g_autofree gchar *content_type = NULL;
		g_autoptr(GInputStream) istream_tmp = NULL;
		g_autoptr(XbBuilderSourceCtx) ctx = xb_builder_source_ctx_new(file, istream);

		/* get the content type of the stream */
		xb_builder_source_ctx_set_filename(ctx, basename);
		content_type = xb_builder_source_ctx_get_content_type(ctx, cancellable, error);
		g_debug("detected content type of %s to be %s", basename, content_type);
		if (content_type == NULL)
			return NULL;
		if (g_strcmp0(content_type, "application/xml") == 0)
			break;
		/* Also accept the text/xml alias, just in case the user’s content-type database is
		 * slightly broken (application/xml should normally be what’s used): */
		if (g_strcmp0(content_type, "text/xml") == 0)
			break;

		/* convert the stream */
		item = xb_builder_source_get_adapter_by_mime(self, content_type);
		if (item == NULL || item->func_adapter == NULL) {
			g_set_error(error,
				    G_IO_ERROR,
				    G_IO_ERROR_NOT_SUPPORTED,
				    "cannot process content type %s",
				    content_type);
			return NULL;
		}
		istream_tmp = item->func_adapter(self, ctx, item->user_data, cancellable, error);
		if (istream_tmp == NULL)
			return NULL;
		xb_builder_source_remove_last_extension(basename);
		g_set_object(&istream, istream_tmp);

		/* the #GFile is only useful for the outermost input stream,
		 * for example it points to the .tar.gz file, while inner input
		 * streams are the .xml output of decompressing the .gz in
		 * memory and can’t be represented as a #GFile */
		file = NULL;

		if (item->is_simple)
			break;
	} while (TRUE);
	return g_steal_pointer(&istream);
}

GFile *
xb_builder_source_get_file(XbBuilderSource *self)
{
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(XB_IS_BUILDER_SOURCE(self), NULL);
	return priv->file;
}

XbBuilderSourceFlags
xb_builder_source_get_flags(XbBuilderSource *self)
{
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);
	g_return_val_if_fail(XB_IS_BUILDER_SOURCE(self), 0);
	return priv->flags;
}

static GInputStream *
xb_builder_source_load_gzip_cb(XbBuilderSource *self,
			       XbBuilderSourceCtx *ctx,
			       gpointer user_data,
			       GCancellable *cancellable,
			       GError **error)
{
	GInputStream *istream = xb_builder_source_ctx_get_stream(ctx);
	g_autoptr(GConverter) conv = NULL;
	conv = G_CONVERTER(g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_GZIP));
	return g_converter_input_stream_new(istream, conv);
}

#ifdef HAVE_LZMA
static GInputStream *
xb_builder_source_load_lzma_cb(XbBuilderSource *self,
			       XbBuilderSourceCtx *ctx,
			       gpointer user_data,
			       GCancellable *cancellable,
			       GError **error)
{
	GInputStream *istream = xb_builder_source_ctx_get_stream(ctx);
	g_autoptr(GConverter) conv = G_CONVERTER(xb_lzma_decompressor_new());
	return g_converter_input_stream_new(istream, conv);
}
#endif

#ifdef HAVE_ZSTD
static GInputStream *
xb_builder_source_load_zstd_cb(XbBuilderSource *self,
			       XbBuilderSourceCtx *ctx,
			       gpointer user_data,
			       GCancellable *cancellable,
			       GError **error)
{
	GInputStream *istream = xb_builder_source_ctx_get_stream(ctx);
	g_autoptr(GConverter) conv = G_CONVERTER(xb_zstd_decompressor_new());
	return g_converter_input_stream_new(istream, conv);
}
#endif
static void
xb_builder_source_adapter_free(XbBuilderSourceAdapter *item)
{
	if (item->user_data_free != NULL)
		item->user_data_free(item->user_data);
	g_free(item->content_type);
	g_slice_free(XbBuilderSourceAdapter, item);
}

static void
xb_builder_source_finalize(GObject *obj)
{
	XbBuilderSource *self = XB_BUILDER_SOURCE(obj);
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);

	if (priv->istream != NULL)
		g_object_unref(priv->istream);
	if (priv->info != NULL)
		g_object_unref(priv->info);
	if (priv->file != NULL)
		g_object_unref(priv->file);
	g_ptr_array_unref(priv->fixups);
	g_ptr_array_unref(priv->adapters);
	g_free(priv->guid);
	g_free(priv->prefix);
	g_free(priv->content_type);

	G_OBJECT_CLASS(xb_builder_source_parent_class)->finalize(obj);
}

static void
xb_builder_source_class_init(XbBuilderSourceClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	object_class->finalize = xb_builder_source_finalize;
}

static void
xb_builder_source_init(XbBuilderSource *self)
{
	XbBuilderSourcePrivate *priv = GET_PRIVATE(self);
	priv->fixups = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
	priv->adapters =
	    g_ptr_array_new_with_free_func((GDestroyNotify)xb_builder_source_adapter_free);
	xb_builder_source_add_adapter(self,
				      "application/gzip,application/x-gzip,org.gnu.gnu-zip-archive",
				      xb_builder_source_load_gzip_cb,
				      NULL,
				      NULL);
#ifdef HAVE_LZMA
	xb_builder_source_add_adapter(self,
				      "application/x-xz,org.tukaani.xz-archive",
				      xb_builder_source_load_lzma_cb,
				      NULL,
				      NULL);
#endif
#ifdef HAVE_ZSTD
	xb_builder_source_add_adapter(self,
				      "application/zstd",
				      xb_builder_source_load_zstd_cb,
				      NULL,
				      NULL);
#endif
}

/**
 * xb_builder_source_new:
 *
 * Creates a new builder source.
 *
 * Returns: a new #XbBuilderSource
 *
 * Since: 0.1.1
 **/
XbBuilderSource *
xb_builder_source_new(void)
{
	return g_object_new(XB_TYPE_BUILDER_SOURCE, NULL);
}
