/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2014-2015 Richard Hughes <richard@hughsie.com>
 *
 * Licensed under the GNU Lesser General Public License Version 2.1
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

/**
 * SECTION:asb-context
 * @short_description: High level interface for creating metadata.
 * @stability: Unstable
 *
 * This high level object can be used to build metadata given some package
 * filenames.
 */

#include "config.h"

#include <stdlib.h>
#include <appstream-glib.h>

#include "asb-context.h"
#include "asb-context-private.h"
#include "asb-plugin.h"
#include "asb-plugin-loader.h"
#include "asb-task.h"
#include "asb-utils.h"

#ifdef HAVE_RPM
#include "asb-package-rpm.h"
#endif
#ifdef HAVE_ALPM
#include "asb-package-alpm.h"
#endif
#include "asb-package-cab.h"
#include "asb-package-deb.h"

typedef struct
{
	AsStore			*store_failed;
	AsStore			*store_ignore;
	AsStore			*store_old;
	GList			*apps;			/* of AsbApp */
	GMutex			 apps_mutex;		/* for ->apps */
	GPtrArray		*file_globs;		/* of AsbPackage */
	GPtrArray		*packages;		/* of AsbPackage */
	AsbPluginLoader		*plugin_loader;
	AsbContextFlags		 flags;
	guint			 max_threads;
	guint			 min_icon_size;
	gdouble			 api_version;
	gchar			*old_metadata;
	gchar			*log_dir;
	gchar			*cache_dir;
	gchar			*temp_dir;
	gchar			*output_dir;
	gchar			*icons_dir;
	gchar			*basename;
	gchar			*origin;
} AsbContextPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (AsbContext, asb_context, G_TYPE_OBJECT)

#define GET_PRIVATE(o) (asb_context_get_instance_private (o))

static gchar *
asb_context_realpath (const gchar *path)
{
	char *temp;
	gchar *real;

	/* don't trust realpath one little bit */
	if (path == NULL)
		return NULL;

	/* glibc allocates us a buffer to try and fix some brain damage */
	temp = realpath (path, NULL);
	if (temp == NULL)
		return NULL;
	real = g_strdup (temp);
	free (temp);
	return real;
}

/**
 * asb_context_set_api_version:
 * @ctx: A #AsbContext
 * @api_version: the AppStream API version
 *
 * Sets the version of the metadata to write.
 *
 * Since: 0.1.0
 **/
void
asb_context_set_api_version (AsbContext *ctx, gdouble api_version)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	priv->api_version = api_version;
}

/**
 * asb_context_set_flags:
 * @ctx: A #AsbContext
 * @flags: #AsbContextFlags, e.g. %ASB_CONTEXT_FLAG_ADD_CACHE_ID
 *
 * Sets if the cache id should be included in the metadata.
 *
 * Since: 0.3.5
 **/
void
asb_context_set_flags (AsbContext *ctx, AsbContextFlags flags)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	if (flags & ASB_CONTEXT_FLAG_ADD_CACHE_ID) {
		as_store_add_metadata_index (priv->store_failed, "X-CacheID");
		as_store_add_metadata_index (priv->store_ignore, "X-CacheID");
		as_store_add_metadata_index (priv->store_old, "X-CacheID");
	}
	priv->flags = flags;
}

/**
 * asb_context_set_max_threads:
 * @ctx: A #AsbContext
 * @max_threads: integer
 *
 * Sets the maximum number of threads to use when processing packages.
 *
 * Since: 0.1.0
 **/
void
asb_context_set_max_threads (AsbContext *ctx, guint max_threads)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	priv->max_threads = max_threads;
}

/**
 * asb_context_set_min_icon_size:
 * @ctx: A #AsbContext
 * @min_icon_size: integer
 *
 * Sets the smallest icon size in pixels supported.
 *
 * Since: 0.3.1
 **/
void
asb_context_set_min_icon_size (AsbContext *ctx, guint min_icon_size)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	priv->min_icon_size = min_icon_size;
}

/**
 * asb_context_get_min_icon_size:
 * @ctx: A #AsbContext
 *
 * Gets the minimum icon size in pixels.
 *
 * Returns: size
 *
 * Since: 0.3.1
 **/
guint
asb_context_get_min_icon_size (AsbContext *ctx)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	return priv->min_icon_size;
}

/**
 * asb_context_set_old_metadata:
 * @ctx: A #AsbContext
 * @old_metadata: filename, or %NULL
 *
 * Sets the filename location of the old metadata file.
 * Note: the old metadata must have been built with cache-ids to be useful.
 *
 * Since: 0.1.0
 **/
void
asb_context_set_old_metadata (AsbContext *ctx, const gchar *old_metadata)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	priv->old_metadata = asb_context_realpath (old_metadata);
}

/**
 * asb_context_set_log_dir:
 * @ctx: A #AsbContext
 * @log_dir: directory
 *
 * Sets the log directory to use when building metadata.
 *
 * Since: 0.1.0
 **/
void
asb_context_set_log_dir (AsbContext *ctx, const gchar *log_dir)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	priv->log_dir = asb_context_realpath (log_dir);
}

/**
 * asb_context_set_cache_dir:
 * @ctx: A #AsbContext
 * @cache_dir: directory
 *
 * Sets the cache directory to use when building metadata.
 *
 * Since: 0.1.0
 **/
void
asb_context_set_cache_dir (AsbContext *ctx, const gchar *cache_dir)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	priv->cache_dir = g_strdup (cache_dir);
}

/**
 * asb_context_set_temp_dir:
 * @ctx: A #AsbContext
 * @temp_dir: directory
 *
 * Sets the temporary directory to use when building metadata.
 *
 * Since: 0.1.0
 **/
void
asb_context_set_temp_dir (AsbContext *ctx, const gchar *temp_dir)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	priv->temp_dir = g_strdup (temp_dir);
}

/**
 * asb_context_set_output_dir:
 * @ctx: A #AsbContext
 * @output_dir: directory
 *
 * Sets the output directory to use when building metadata.
 *
 * Since: 0.1.0
 **/
void
asb_context_set_output_dir (AsbContext *ctx, const gchar *output_dir)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	priv->output_dir = g_strdup (output_dir);
}

/**
 * asb_context_set_icons_dir:
 * @ctx: A #AsbContext
 * @icons_dir: directory
 *
 * Sets the icons directory to use when building metadata.
 *
 * Since: 0.3.5
 **/
void
asb_context_set_icons_dir (AsbContext *ctx, const gchar *icons_dir)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	priv->icons_dir = g_strdup (icons_dir);
}

/**
 * asb_context_set_basename:
 * @ctx: A #AsbContext
 * @basename: AppStream file basename, e.g. "appstream"
 *
 * Sets the basename for the two metadata files.
 *
 * Since: 0.1.0
 **/
void
asb_context_set_basename (AsbContext *ctx, const gchar *basename)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	priv->basename = g_strdup (basename);
}

/**
 * asb_context_set_origin:
 * @ctx: A #AsbContext
 * @origin: AppStream origin, e.g. "fedora-21"
 *
 * Sets the origin for the two metadata files.
 *
 * Since: 0.3.4
 **/
void
asb_context_set_origin (AsbContext *ctx, const gchar *origin)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	priv->origin = g_strdup (origin);
}

/**
 * asb_context_get_flags:
 * @ctx: A #AsbContext
 *
 * Gets the build flags.
 *
 * Returns: #AsbContextFlags
 *
 * Since: 0.3.5
 **/
AsbContextFlags
asb_context_get_flags (AsbContext *ctx)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	return priv->flags;
}

/**
 * asb_context_get_flag:
 * @ctx: A #AsbContext
 * @flag: A #AsbContextFlags
 *
 * Gets one specific build flag.
 *
 * Returns: %TRUE if the flag was set
 *
 * Since: 0.3.5
 **/
gboolean
asb_context_get_flag (AsbContext *ctx, AsbContextFlags flag)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	return (priv->flags & flag) > 0;
}

/**
 * asb_context_get_api_version:
 * @ctx: A #AsbContext
 *
 * Gets the target metadata API version.
 *
 * Returns: floating point
 *
 * Since: 0.1.0
 **/
gdouble
asb_context_get_api_version (AsbContext *ctx)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	return priv->api_version;
}

/**
 * asb_context_get_temp_dir:
 * @ctx: A #AsbContext
 *
 * Gets the temporary directory to use
 *
 * Returns: directory
 *
 * Since: 0.1.0
 **/
const gchar *
asb_context_get_temp_dir (AsbContext *ctx)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	return priv->temp_dir;
}

/**
 * asb_context_get_cache_dir:
 * @ctx: A #AsbContext
 *
 * Gets the screenshot directory to use
 *
 * Returns: directory
 *
 * Since: 0.3.6
 **/
const gchar *
asb_context_get_cache_dir (AsbContext *ctx)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	return priv->cache_dir;
}

/**
 * asb_context_get_plugin_loader:
 * @ctx: A #AsbContext
 *
 * Gets the plugins available to the metadata extractor.
 *
 * Returns: (transfer none): the plugin loader in use
 *
 * Since: 0.1.0
 **/
AsbPluginLoader *
asb_context_get_plugin_loader (AsbContext *ctx)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	return priv->plugin_loader;
}

/**
 * asb_context_get_packages:
 * @ctx: A #AsbContext
 *
 * Returns the packages already added to the context.
 *
 * Returns: (transfer none) (element-type AsbPackage): array of packages
 *
 * Since: 0.1.0
 **/
GPtrArray *
asb_context_get_packages (AsbContext *ctx)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	return priv->packages;
}

/**
 * asb_context_add_package:
 * @ctx: A #AsbContext
 * @pkg: A #AsbPackage
 *
 * Adds a package to the list of packages to be processed
 *
 * Since: 0.3.5
 **/
void
asb_context_add_package (AsbContext *ctx, AsbPackage *pkg)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	g_ptr_array_add (priv->packages, g_object_ref (pkg));
}

/**
 * asb_context_add_filename:
 * @ctx: A #AsbContext
 * @filename: package filename
 * @error: A #GError or %NULL
 *
 * Adds a filename to the list of packages to be processed
 *
 * Returns: %TRUE for success, %FALSE otherwise
 *
 * Since: 0.1.0
 **/
gboolean
asb_context_add_filename (AsbContext *ctx, const gchar *filename, GError **error)
{
	g_autoptr(AsbPackage) pkg = NULL;

	/* can find in existing metadata */
	if (asb_context_find_in_cache (ctx, filename)) {
		g_debug ("Found %s in old metadata", filename);
		return TRUE;
	}

	/* open */
#ifdef HAVE_RPM
	if (g_str_has_suffix (filename, ".rpm"))
		pkg = asb_package_rpm_new ();
#endif
#ifdef HAVE_ALPM
	if (g_str_has_suffix (filename, ".pkg.tar.xz"))
		pkg = asb_package_alpm_new ();
#endif
	if (g_str_has_suffix (filename, ".cab"))
		pkg = asb_package_cab_new ();
	if (g_str_has_suffix (filename, ".deb"))
		pkg = asb_package_deb_new ();
	if (pkg == NULL) {
		g_set_error (error,
			     ASB_PLUGIN_ERROR,
			     ASB_PLUGIN_ERROR_FAILED,
			     "No idea how to handle %s",
			     filename);
		return FALSE;
	}

	/* add to array */
	asb_package_set_filename (pkg, filename);

	/* failed to guess the nevra */
	if (asb_package_get_name (pkg) == NULL) {
		if (!asb_package_open (pkg, filename, error))
			return FALSE;
	}

	asb_context_add_package (ctx, pkg);
	return TRUE;
}

/**
 * asb_context_get_file_globs:
 * @ctx: A #AsbContext
 *
 * Gets the list of file globs added by plugins.
 *
 * Returns: file globs
 *
 * Since: 0.1.0
 **/
GPtrArray *
asb_context_get_file_globs (AsbContext *ctx)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	return priv->file_globs;
}

/**
 * asb_context_setup:
 * @ctx: A #AsbContext
 * @error: A #GError or %NULL
 *
 * Sets up the context ready for use.
 *
 * Returns: %TRUE for success, %FALSE otherwise
 *
 * Since: 0.1.0
 **/
gboolean
asb_context_setup (AsbContext *ctx, GError **error)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	g_autofree gchar *icons_dir = NULL;
	g_autofree gchar *screenshot_dir1 = NULL;
	g_autofree gchar *screenshot_dir2 = NULL;

	/* required stuff set */
	if (priv->origin == NULL) {
		g_set_error_literal (error,
				     ASB_PLUGIN_ERROR,
				     ASB_PLUGIN_ERROR_FAILED,
				     "origin not set!");
		return FALSE;
	}
	if (priv->output_dir == NULL) {
		g_set_error_literal (error,
				     ASB_PLUGIN_ERROR,
				     ASB_PLUGIN_ERROR_FAILED,
				     "output_dir not set!");
		return FALSE;
	}
	if (priv->icons_dir == NULL) {
		g_set_error_literal (error,
				     ASB_PLUGIN_ERROR,
				     ASB_PLUGIN_ERROR_FAILED,
				     "icons_dir not set!");
		return FALSE;
	}
	if (priv->temp_dir == NULL) {
		g_set_error_literal (error,
				     ASB_PLUGIN_ERROR,
				     ASB_PLUGIN_ERROR_FAILED,
				     "temp_dir not set!");
		return FALSE;
	}
	if (priv->cache_dir == NULL) {
		g_set_error_literal (error,
				     ASB_PLUGIN_ERROR,
				     ASB_PLUGIN_ERROR_FAILED,
				     "cache_dir not set!");
		return FALSE;
	}

	/* create temp space */
	if (!asb_utils_ensure_exists (priv->output_dir, error))
		return FALSE;
	screenshot_dir1 = g_build_filename (priv->temp_dir, "screenshots", NULL);
	if (!asb_utils_ensure_exists_and_empty (screenshot_dir1, error))
		return FALSE;
	screenshot_dir2 = g_build_filename (priv->cache_dir, "screenshots", NULL);
	if (!asb_utils_ensure_exists (screenshot_dir2, error))
		return FALSE;
	if (priv->log_dir != NULL) {
		if (!asb_utils_ensure_exists (priv->log_dir, error))
			return FALSE;
	}

	/* icons is nuked; we can re-decompress from the -icons.tar.gz */
	if (!asb_utils_ensure_exists (priv->icons_dir, error))
		return FALSE;
	if (priv->flags & ASB_CONTEXT_FLAG_HIDPI_ICONS) {
		g_autofree gchar *icons_dir_hidpi = NULL;
		g_autofree gchar *icons_dir_lodpi = NULL;
		icons_dir_lodpi = g_build_filename (priv->icons_dir, "64x64", NULL);
		if (!asb_utils_ensure_exists (icons_dir_lodpi, error))
			return FALSE;
		icons_dir_hidpi = g_build_filename (priv->icons_dir, "128x128", NULL);
		if (!asb_utils_ensure_exists (icons_dir_hidpi, error))
			return FALSE;
	}

	/* decompress the icons */
	if (priv->old_metadata != NULL) {
		g_autofree gchar *icons_fn = NULL;
		icons_fn = g_strdup_printf ("%s/%s-icons.tar.gz",
					    priv->old_metadata,
					    priv->basename);
		if (g_file_test (icons_fn, G_FILE_TEST_EXISTS)) {
			if (!asb_utils_explode (icons_fn,
						priv->icons_dir,
						NULL,
						error))
				return FALSE;
		}
	}

	/* load plugins */
	if (!asb_plugin_loader_setup (priv->plugin_loader, error))
		return FALSE;

	/* get a cache of the file globs */
	priv->file_globs = asb_plugin_loader_get_globs (priv->plugin_loader);

	/* add old metadata */
	if (priv->old_metadata != NULL) {
		g_autofree gchar *builder_id = NULL;
		g_autofree gchar *fn_failed = NULL;
		g_autofree gchar *fn_ignore = NULL;
		g_autofree gchar *fn_old = NULL;
		g_autoptr(GFile) file_failed = NULL;
		g_autoptr(GFile) file_ignore = NULL;
		g_autoptr(GFile) file_old = NULL;

		builder_id = asb_utils_get_builder_id ();
		fn_old = g_strdup_printf ("%s/%s.xml.gz",
					  priv->old_metadata,
					  priv->basename);
		file_old = g_file_new_for_path (fn_old);
		if (g_file_query_exists (file_old, NULL)) {
			if (!as_store_from_file (priv->store_old, file_old,
						 NULL, NULL, error))
				return FALSE;
			/* check builder-id matches */
			if (g_strcmp0 (as_store_get_builder_id (priv->store_old),
				       builder_id) != 0) {
				g_debug ("builder ID does not match: %s:%s",
					 as_store_get_builder_id (priv->store_old),
					 builder_id);
				as_store_remove_all (priv->store_old);
			}
		}
		fn_ignore = g_strdup_printf ("%s/%s-ignore.xml.gz",
					     priv->old_metadata,
					     priv->basename);
		file_ignore = g_file_new_for_path (fn_ignore);
		if (g_file_query_exists (file_ignore, NULL)) {
			if (!as_store_from_file (priv->store_ignore, file_ignore,
						 NULL, NULL, error))
				return FALSE;
			/* check builder-id matches */
			if (g_strcmp0 (as_store_get_builder_id (priv->store_ignore),
				       builder_id) != 0) {
				g_debug ("builder ID does not match: %s:%s",
					 as_store_get_builder_id (priv->store_ignore),
					 builder_id);
				as_store_remove_all (priv->store_ignore);
			}
		}
		fn_failed = g_strdup_printf ("%s/%s-failed.xml.gz",
					     priv->old_metadata,
					     priv->basename);
		file_failed = g_file_new_for_path (fn_failed);
		if (g_file_query_exists (file_failed, NULL)) {
			if (!as_store_from_file (priv->store_failed, file_failed,
						 NULL, NULL, error))
				return FALSE;
			/* check builder-id matches */
			if (g_strcmp0 (as_store_get_builder_id (priv->store_failed),
				       builder_id) != 0) {
				g_debug ("builder ID does not match: %s:%s",
					 as_store_get_builder_id (priv->store_failed),
					 builder_id);
				as_store_remove_all (priv->store_failed);
			}
		}
	}

	return TRUE;
}

static void
asb_task_process_func (gpointer data, gpointer user_data)
{
	AsbTask *task = (AsbTask *) data;
	g_autoptr(GError) error = NULL;

	/* just run the task */
	if (!asb_task_process (task, &error))
		g_warning ("Failed to run task: %s", error->message);
}

static gboolean
asb_context_write_icons (AsbContext *ctx,
			 const gchar *temp_dir,
			 GError **error)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	g_autofree gchar *filename = NULL;

	/* not enabled */
	if (priv->flags & ASB_CONTEXT_FLAG_UNCOMPRESSED_ICONS)
		return TRUE;

	if (!g_file_test (priv->icons_dir, G_FILE_TEST_EXISTS))
		return TRUE;
	filename = g_strdup_printf ("%s/%s-icons.tar.gz",
				    priv->output_dir, priv->basename);
	g_print ("Writing %s...\n", filename);
	return asb_utils_write_archive_dir (filename, priv->icons_dir, error);
}

static gboolean
asb_context_write_screenshots (AsbContext *ctx,
			       const gchar *temp_dir,
			       GError **error)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	g_autofree gchar *filename = NULL;
	g_autofree gchar *screenshot_dir = NULL;

	/* not enabled */
	if (priv->flags & ASB_CONTEXT_FLAG_UNCOMPRESSED_ICONS)
		return TRUE;

	screenshot_dir = g_build_filename (temp_dir, "screenshots", NULL);
	if (!g_file_test (screenshot_dir, G_FILE_TEST_EXISTS))
		return TRUE;
	filename = g_strdup_printf ("%s/%s-screenshots.tar",
				    priv->output_dir, priv->basename);
	g_print ("Writing %s...\n", filename);
	return asb_utils_write_archive_dir (filename, screenshot_dir, error);
}

static gboolean
asb_context_write_xml (AsbContext *ctx, GError **error)
{
	AsApp *app;
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	GList *l;
	g_autofree gchar *filename = NULL;
	g_autoptr(AsStore) store = NULL;
	g_autoptr(GFile) file = NULL;

	/* convert any vetod applications into dummy components */
	for (l = priv->apps; l != NULL; l = l->next) {
		app = AS_APP (l->data);
		if (!ASB_IS_APP (app))
			continue;
		if (as_app_get_vetos(app)->len == 0)
			continue;
		asb_context_add_app_ignore (ctx, asb_app_get_package (ASB_APP (app)));
	}

	/* add any non-vetoed applications */
	store = as_store_new ();
	for (l = priv->apps; l != NULL; l = l->next) {
		app = AS_APP (l->data);
		if (as_app_get_vetos(app)->len > 0)
			continue;
		as_store_add_app (store, app);
		as_store_remove_app (priv->store_failed, app);

		/* remove from the ignore list if the application was useful */
		if (ASB_IS_APP (app)) {
			AsbPackage *pkg = asb_app_get_package (ASB_APP (app));
			g_autofree gchar *name_arch = NULL;
			name_arch = g_strdup_printf ("%s.%s",
						     asb_package_get_name (pkg),
						     asb_package_get_arch (pkg));
			as_store_remove_app_by_id (priv->store_ignore, name_arch);
		}

	}
	filename = g_strdup_printf ("%s/%s.xml.gz",
				    priv->output_dir,
				    priv->basename);
	file = g_file_new_for_path (filename);

	g_print ("Writing %s...\n", filename);
	as_store_set_origin (store, priv->origin);
	as_store_set_api_version (store, priv->api_version);
	if (priv->flags & ASB_CONTEXT_FLAG_ADD_CACHE_ID) {
		g_autofree gchar *builder_id = asb_utils_get_builder_id ();
		as_store_set_builder_id (store, builder_id);
	}
	return as_store_to_file (store,
				 file,
				 AS_NODE_TO_XML_FLAG_ADD_HEADER |
				 AS_NODE_TO_XML_FLAG_FORMAT_INDENT |
				 AS_NODE_TO_XML_FLAG_FORMAT_MULTILINE,
				 NULL, error);
}

static gboolean
asb_context_convert_icons (AsbContext *ctx, GError **error)
{
	AsApp *app;
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	GList *l;

	/* not enabled */
	if ((priv->flags & ASB_CONTEXT_FLAG_EMBEDDED_ICONS) == 0)
		return TRUE;

	/* convert each one before saving resources */
	for (l = priv->apps; l != NULL; l = l->next) {
		app = AS_APP (l->data);
		if (as_app_get_vetos(app)->len > 0)
			continue;
		if (!as_app_convert_icons (app, AS_ICON_KIND_EMBEDDED, error))
			return FALSE;
	}
	return TRUE;
}

static gboolean
asb_context_save_resources (AsbContext *ctx, GError **error)
{
	AsApp *app;
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	GList *l;

	for (l = priv->apps; l != NULL; l = l->next) {
		app = AS_APP (l->data);

		/* save icon */
		if (as_app_get_vetos(app)->len > 0)
			continue;
		if (!ASB_IS_APP (app))
			continue;
		if (!asb_app_save_resources (ASB_APP (app),
					     ASB_APP_SAVE_FLAG_ICONS,
					     error))
			return FALSE;
	}
	return TRUE;
}

static gboolean
asb_context_detect_pkgname_dups (AsbContext *ctx, GError **error)
{
	AsApp *app;
	AsApp *found;
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	GList *l;
	const gchar *pkgname;
	g_autoptr(GHashTable) hash = NULL;

	hash = g_hash_table_new (g_str_hash, g_str_equal);
	for (l = priv->apps; l != NULL; l = l->next) {
		app = AS_APP (l->data);
		pkgname = as_app_get_pkgname_default (app);
		if (pkgname == NULL)
			continue;
		if (ASB_IS_APP (app) &&  as_app_get_vetos(app)->len > 0)
			continue;
		found = g_hash_table_lookup (hash, pkgname);
		if (found != NULL) {
			g_print ("WARNING: %s and %s share the package '%s'\n",
				 as_app_get_id (app),
				 as_app_get_id (found), pkgname);
			continue;
		}
		g_hash_table_insert (hash, (gpointer) pkgname, app);
	}
	return TRUE;
}

static void
asb_context_write_app_xml (AsbContext *ctx)
{
	AsbApp *app;
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	GList *l;

	/* log the XML in the log file */
	for (l = priv->apps; l != NULL; l = l->next) {
		g_autoptr(GString) xml = NULL;
		g_autoptr(AsStore) store = NULL;

		/* we have an open log file? */
		if (!ASB_IS_APP (l->data))
			continue;

		/* just log raw XML */
		app = ASB_APP (l->data);
		store = as_store_new ();
		as_store_set_api_version (store, 1.0f);
		as_store_add_app (store, AS_APP (app));
		xml = as_store_to_xml (store,
				       AS_NODE_TO_XML_FLAG_FORMAT_INDENT |
				       AS_NODE_TO_XML_FLAG_FORMAT_MULTILINE);
		asb_package_log (asb_app_get_package (app),
				 ASB_PACKAGE_LOG_LEVEL_NONE, "%s", xml->str);
	}
}

static gboolean
asb_context_detect_missing_data (AsbContext *ctx, GError **error)
{
	AsApp *app;
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	GList *l;

	/* look for the thing that an addon extends */
	for (l = priv->apps; l != NULL; l = l->next) {
		app = AS_APP (l->data);
		if (as_app_get_name (AS_APP (app), "C") == NULL)
			as_app_add_veto (AS_APP (app), "No <name> in AppData");
		if (as_app_get_comment (AS_APP (app), "C") == NULL)
			as_app_add_veto (AS_APP (app), "No <summary> in AppData");
		switch (as_app_get_kind (AS_APP (app))) {
		case AS_APP_KIND_ADDON:
		case AS_APP_KIND_FIRMWARE:
		case AS_APP_KIND_DRIVER:
		case AS_APP_KIND_GENERIC:
		case AS_APP_KIND_LOCALIZATION:
			break;
		default:
			if (as_app_get_icon_default (AS_APP (app)) == NULL)
				as_app_add_veto (AS_APP (app), "Has no Icon");
			break;
		}
	}
	return TRUE;
}

static gboolean
asb_context_detect_missing_parents (AsbContext *ctx, GError **error)
{
	AsApp *app;
	AsApp *found;
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	GList *l;
	const gchar *tmp;
	g_autoptr(GHashTable) hash = NULL;

	/* add all desktop apps to the hash */
	hash = g_hash_table_new (g_str_hash, g_str_equal);
	for (l = priv->apps; l != NULL; l = l->next) {
		app = AS_APP (l->data);
		if (!ASB_IS_APP (app))
			continue;
		if (as_app_get_pkgname_default (app) == NULL)
			continue;
		if (as_app_get_kind (app) != AS_APP_KIND_DESKTOP)
			continue;
		g_hash_table_insert (hash,
				     (gpointer) as_app_get_id (app),
				     app);
	}

	/* look for the thing that an addon extends */
	for (l = priv->apps; l != NULL; l = l->next) {
		app = AS_APP (l->data);
		if (!ASB_IS_APP (app))
			continue;
		if (as_app_get_pkgname_default (app) == NULL)
			continue;
		if (as_app_get_kind (app) != AS_APP_KIND_ADDON)
			continue;
		if (as_app_get_extends(app)->len == 0)
			continue;
		tmp = g_ptr_array_index (as_app_get_extends(app), 0);
		found = g_hash_table_lookup (hash, tmp);
		if (found != NULL)
			continue;
		found = as_store_get_app_by_id (priv->store_old, tmp);
		if (found != NULL)
			continue;

		/* do not add the addon */
		as_app_add_veto (app, "%s has no parent of '%s'",
				 as_app_get_id (app), tmp);
		g_print ("WARNING: %s has no parent of '%s'\n",
			 as_app_get_id (app), tmp);
	}
	return TRUE;
}

static gboolean
asb_context_write_xml_fail (AsbContext *ctx, GError **error)
{
	AsApp *app;
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	GList *l;
	g_autofree gchar *basename_failed = NULL;
	g_autofree gchar *filename = NULL;
	g_autoptr(GFile) file = NULL;

	/* no need to create */
	if ((priv->flags & ASB_CONTEXT_FLAG_INCLUDE_FAILED) == 0)
		return TRUE;

	for (l = priv->apps; l != NULL; l = l->next) {
		app = AS_APP (l->data);
		if (!ASB_IS_APP (app))
			continue;
		if (as_app_get_vetos(app)->len == 0)
			continue;
		if (as_store_get_app_by_id (priv->store_failed,
					    as_app_get_id (app)) != NULL)
			continue;
		as_store_add_app (priv->store_failed, app);
	}
	filename = g_strdup_printf ("%s/%s-failed.xml.gz",
				    priv->output_dir, priv->basename);
	file = g_file_new_for_path (filename);

	g_print ("Writing %s...\n", filename);
	basename_failed = g_strdup_printf ("%s-failed", priv->origin);
	as_store_set_origin (priv->store_failed, basename_failed);
	as_store_set_api_version (priv->store_failed, priv->api_version);
	if (priv->flags & ASB_CONTEXT_FLAG_ADD_CACHE_ID) {
		g_autofree gchar *builder_id = asb_utils_get_builder_id ();
		as_store_set_builder_id (priv->store_failed, builder_id);
	}
	return as_store_to_file (priv->store_failed,
				 file,
				 AS_NODE_TO_XML_FLAG_ADD_HEADER |
				 AS_NODE_TO_XML_FLAG_FORMAT_INDENT |
				 AS_NODE_TO_XML_FLAG_FORMAT_MULTILINE,
				 NULL, error);
}

static gboolean
asb_context_write_xml_ignore (AsbContext *ctx, GError **error)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	g_autofree gchar *basename_cache = NULL;
	g_autofree gchar *filename = NULL;
	g_autoptr(GFile) file = NULL;

	/* no need to create */
	if ((priv->flags & ASB_CONTEXT_FLAG_ADD_CACHE_ID) == 0)
		return TRUE;

	/* the store is already populated */
	filename = g_strdup_printf ("%s/%s-ignore.xml.gz",
				    priv->output_dir, priv->basename);
	file = g_file_new_for_path (filename);

	g_print ("Writing %s...\n", filename);
	basename_cache = g_strdup_printf ("%s-ignore", priv->origin);
	as_store_set_origin (priv->store_ignore, basename_cache);
	as_store_set_api_version (priv->store_ignore, priv->api_version);
	if (priv->flags & ASB_CONTEXT_FLAG_ADD_CACHE_ID) {
		g_autofree gchar *builder_id = asb_utils_get_builder_id ();
		as_store_set_builder_id (priv->store_ignore, builder_id);
	}
	return as_store_to_file (priv->store_ignore,
				 file,
				 AS_NODE_TO_XML_FLAG_ADD_HEADER |
				 AS_NODE_TO_XML_FLAG_FORMAT_INDENT |
				 AS_NODE_TO_XML_FLAG_FORMAT_MULTILINE,
				 NULL, error);
}

static void
asb_context_disable_older_pkgs (AsbContext *ctx)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	AsbPackage *found;
	AsbPackage *pkg;
	const gchar *key;
	guint i;
	g_autoptr(GHashTable) newest = NULL;

	newest = g_hash_table_new_full (g_str_hash, g_str_equal,
					g_free, (GDestroyNotify) g_object_unref);
	for (i = 0; i < priv->packages->len; i++) {
		pkg = ASB_PACKAGE (g_ptr_array_index (priv->packages, i));
		if (!asb_package_get_enabled (pkg))
			continue;
		key = asb_package_get_name (pkg);
		if (key == NULL)
			continue;
		switch (asb_package_get_kind (pkg)) {
		case ASB_PACKAGE_KIND_DEFAULT:
		case ASB_PACKAGE_KIND_BUNDLE:
			found = g_hash_table_lookup (newest, key);
			if (found != NULL) {
				if (asb_package_compare (pkg, found) <= 0) {
					asb_package_set_enabled (pkg, FALSE);
					continue;
				}
				asb_package_set_enabled (found, FALSE);
			}
			break;
		default:
			/* allow multiple versions */
			break;
		}
		g_hash_table_insert (newest, g_strdup (key), g_object_ref (pkg));
	}
}

/* return the first package in the repo that matches the name and arch */
static AsbPackage *
asb_context_get_package_by_name_arch (AsbContext *ctx,
				      const gchar *name,
				      const gchar *arch)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	guint i;
	for (i = 0; i < priv->packages->len; i++) {
		AsbPackage *pkg = ASB_PACKAGE (g_ptr_array_index (priv->packages, i));
		if (g_strcmp0 (asb_package_get_name (pkg), name) == 0 &&
		    g_strcmp0 (asb_package_get_arch (pkg), arch) == 0) {
			return pkg;
		}
	}
	return NULL;
}

static void
asb_context_disable_multiarch_pkgs (AsbContext *ctx)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	guint i;

	/* are there any non 64-bit packages in the repo with 64-bit versions */
	for (i = 0; i < priv->packages->len; i++) {
		AsbPackage *pkg = ASB_PACKAGE (g_ptr_array_index (priv->packages, i));
		AsbPackage *pkg64;
		const gchar *arch = asb_package_get_arch (pkg);
		const gchar *name = asb_package_get_name (pkg);
		if (arch == NULL)
			continue;
		if (g_strcmp0 (arch, "x86_64") == 0)
			continue;
		if (g_strcmp0 (arch, "noarch") == 0)
			continue;
		pkg64 = asb_context_get_package_by_name_arch (ctx,
							      name,
							      "x86_64");
		if (pkg64 == NULL)
			continue;
		g_debug ("disabling alternate-arch %s as native exists %s",
			 asb_package_get_filename (pkg),
			 asb_package_get_filename (pkg64));
		asb_package_set_enabled (pkg, FALSE);
	}
}

/**
 * asb_context_process:
 * @ctx: A #AsbContext
 * @error: A #GError or %NULL
 *
 * Processes all the packages that have been added to the context.
 *
 * Returns: %TRUE for success, %FALSE otherwise
 *
 * Since: 0.1.0
 **/
gboolean
asb_context_process (AsbContext *ctx, GError **error)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	AsbPackage *pkg;
	AsbTask *task;
	GThreadPool *pool;
	gboolean ret;
	guint i;
	g_autoptr(GPtrArray) tasks = NULL;

	/* only process the newest packages */
	asb_context_disable_multiarch_pkgs (ctx);
	asb_context_disable_older_pkgs (ctx);

	/* create thread pool */
	pool = g_thread_pool_new (asb_task_process_func,
				  ctx,
				  (gint) priv->max_threads,
				  TRUE,
				  error);
	if (pool == NULL)
		return FALSE;

	/* add each package */
	g_print ("Processing packages...\n");
	tasks = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	for (i = 0; i < priv->packages->len; i++) {
		pkg = g_ptr_array_index (priv->packages, i);
		if (!asb_package_get_enabled (pkg)) {
			asb_package_log (pkg,
					 ASB_PACKAGE_LOG_LEVEL_DEBUG,
					 "%s is not enabled",
					 asb_package_get_nevr (pkg));
			asb_context_add_app_ignore (ctx, pkg);
			asb_package_log_flush (pkg, NULL);
			continue;
		}

		/* set locations of external resources */
		asb_package_set_config (pkg, "LogDir", priv->log_dir);
		asb_package_set_config (pkg, "TempDir", priv->temp_dir);
		asb_package_set_config (pkg, "IconsDir", priv->icons_dir);
		asb_package_set_config (pkg, "OutputDir", priv->output_dir);

		/* create task */
		task = asb_task_new (ctx);
		asb_task_set_package (task, pkg);
		g_ptr_array_add (tasks, task);

		/* run the task */
		if (priv->max_threads == 1) {
			if (!asb_task_process (task, error))
				return FALSE;
		} else {
			if (!g_thread_pool_push (pool, task, error))
				return FALSE;
		}
	}

	/* wait for them to finish */
	g_thread_pool_free (pool, FALSE, TRUE);

	/* merge */
	g_print ("Merging applications...\n");
	asb_plugin_loader_merge (priv->plugin_loader, priv->apps);

	/* print any warnings */
	if ((priv->flags & ASB_CONTEXT_FLAG_IGNORE_MISSING_INFO) == 0) {
		if (!asb_context_detect_missing_data (ctx, error))
			return FALSE;
	}
	if ((priv->flags & ASB_CONTEXT_FLAG_IGNORE_MISSING_PARENTS) == 0) {
		if (!asb_context_detect_missing_parents (ctx, error))
			return FALSE;
	}
	if (!asb_context_detect_pkgname_dups (ctx, error))
		return FALSE;
	if (!asb_context_convert_icons (ctx, error))
		return FALSE;
	if (!asb_context_save_resources (ctx, error))
		return FALSE;

	/* write the application XML to the log file */
	asb_context_write_app_xml (ctx);

	/* write XML file */
	ret = asb_context_write_xml (ctx, error);
	if (!ret)
		return FALSE;

	/* write XML file */
	ret = asb_context_write_xml_fail (ctx, error);
	if (!ret)
		return FALSE;

	/* write XML file */
	ret = asb_context_write_xml_ignore (ctx, error);
	if (!ret)
		return FALSE;

	/* write icons archive */
	ret = asb_context_write_icons (ctx,
				       priv->temp_dir,
				       error);
	if (!ret)
		return FALSE;

	/* write screenshots archive */
	ret = asb_context_write_screenshots (ctx,
					     priv->temp_dir,
					     error);
	if (!ret)
		return FALSE;

	/* ensure all packages are flushed */
	for (i = 0; i < priv->packages->len; i++) {
		pkg = g_ptr_array_index (priv->packages, i);
		if (!asb_package_log_flush (pkg, error))
			return FALSE;
	}
	return TRUE;
}

/**
 * asb_context_find_in_cache:
 * @ctx: A #AsbContext
 * @filename: cache-id
 *
 * Finds an application in the cache. This will only return results if
 * asb_context_set_old_metadata() has been used.
 *
 * Returns: %TRUE to skip exploding the package
 *
 * Since: 0.1.0
 **/
gboolean
asb_context_find_in_cache (AsbContext *ctx, const gchar *filename)
{
	AsApp *app;
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	guint i;
	g_autofree gchar *cache_id = NULL;
	g_autofree gchar *builder_id = NULL;
	g_autoptr(GPtrArray) apps = NULL;
	g_autoptr(GPtrArray) apps_ignore = NULL;

	/* the package was successfully parsed last time */
	cache_id = asb_utils_get_cache_id_for_filename (filename);
	apps = as_store_get_apps_by_metadata (priv->store_old,
					      "X-CacheID",
					      cache_id);
	if (apps->len > 0) {
		for (i = 0; i < apps->len; i++) {
			app = g_ptr_array_index (apps, i);
			asb_context_add_app (ctx, (AsbApp *) app);
		}
		return TRUE;
	}

	/* the package was ignored last time */
	apps_ignore = as_store_get_apps_by_metadata (priv->store_ignore,
						     "X-CacheID",
						     cache_id);
	if (apps_ignore->len > 0)
		return TRUE;

	return FALSE;
}

/**
 * asb_context_find_by_pkgname:
 * @ctx: A #AsbContext
 * @pkgname: a package name
 *
 * Find a package from its name.
 *
 * Returns: (transfer none): a #AsbPackage, or %NULL for not found.
 *
 * Since: 0.1.0
 **/
AsbPackage *
asb_context_find_by_pkgname (AsbContext *ctx, const gchar *pkgname)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	AsbPackage *pkg;
	guint i;

	for (i = 0; i < priv->packages->len; i++) {
		pkg = g_ptr_array_index (priv->packages, i);
		if (!asb_package_get_enabled (pkg))
			continue;
		if (g_strcmp0 (asb_package_get_name (pkg), pkgname) == 0)
			return pkg;
	}
	return NULL;
}

/**
 * asb_context_add_app:
 * @ctx: A #AsbContext
 * @app: A #AsbApp
 *
 * Adds an application to the context.
 *
 * Since: 0.1.0
 **/
void
asb_context_add_app (AsbContext *ctx, AsbApp *app)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	g_mutex_lock (&priv->apps_mutex);
	asb_plugin_add_app (&priv->apps, AS_APP (app));
	g_mutex_unlock (&priv->apps_mutex);
}

void
asb_context_add_app_ignore (AsbContext *ctx, AsbPackage *pkg)
{
	AsApp *app_tmp;
	AsbContextPrivate *priv = GET_PRIVATE (ctx);
	g_autofree gchar *name_arch = NULL;
	g_autoptr(AsApp) app = NULL;
	g_autoptr(GPtrArray) apps = NULL;

	/* only do this when we are using a cache-id */
	if ((priv->flags & ASB_CONTEXT_FLAG_ADD_CACHE_ID) == 0)
		return;

	/* check not already added a dummy application for this package */
	apps = as_store_get_apps_by_metadata (priv->store_ignore,
					      "X-CacheID",
					      asb_package_get_basename (pkg));
	if (apps->len > 0) {
		g_debug ("already found CacheID of %s",
			 asb_package_get_basename (pkg));
		return;
	}

	/* package name already exists, but with a different CacheID */
	name_arch = g_strdup_printf ("%s.%s",
				     asb_package_get_name (pkg),
				     asb_package_get_arch (pkg));
	app_tmp = as_store_get_app_by_id (priv->store_ignore, name_arch);
	if (app_tmp != NULL) {
		as_app_add_metadata (AS_APP (app_tmp), "X-CacheID",
				     asb_package_get_basename (pkg));
		return;
	}

	/* never encountered before, so add */
	app = as_app_new ();
	as_app_set_id (app, name_arch);
	as_app_add_pkgname (app, asb_package_get_name (pkg));
	as_app_add_metadata (app, "X-CacheID",
			     asb_package_get_basename (pkg));
	as_store_add_app (priv->store_ignore, app);
}

static void
asb_context_finalize (GObject *object)
{
	AsbContext *ctx = ASB_CONTEXT (object);
	AsbContextPrivate *priv = GET_PRIVATE (ctx);

	g_object_unref (priv->store_failed);
	g_object_unref (priv->store_ignore);
	g_object_unref (priv->store_old);
	g_object_unref (priv->plugin_loader);
	g_ptr_array_unref (priv->packages);
	g_list_foreach (priv->apps, (GFunc) g_object_unref, NULL);
	g_list_free (priv->apps);
	if (priv->file_globs != NULL)
		g_ptr_array_unref (priv->file_globs);
	g_mutex_clear (&priv->apps_mutex);
	g_free (priv->old_metadata);
	g_free (priv->log_dir);
	g_free (priv->cache_dir);
	g_free (priv->temp_dir);
	g_free (priv->output_dir);
	g_free (priv->icons_dir);
	g_free (priv->basename);
	g_free (priv->origin);

	G_OBJECT_CLASS (asb_context_parent_class)->finalize (object);
}

static void
asb_context_init (AsbContext *ctx)
{
	AsbContextPrivate *priv = GET_PRIVATE (ctx);

	priv->plugin_loader = asb_plugin_loader_new (ctx);
	priv->packages = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
	g_mutex_init (&priv->apps_mutex);
	priv->store_failed = as_store_new ();
	priv->store_ignore = as_store_new ();
	priv->store_old = as_store_new ();
	priv->max_threads = 1;
	priv->min_icon_size = 32;
}

static void
asb_context_class_init (AsbContextClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	object_class->finalize = asb_context_finalize;
}

/**
 * asb_context_new:
 *
 * Creates a new high-level instance.
 *
 * Returns: a #AsbContext
 *
 * Since: 0.1.0
 **/
AsbContext *
asb_context_new (void)
{
	AsbContext *context;
	context = g_object_new (ASB_TYPE_CONTEXT, NULL);
	return ASB_CONTEXT (context);
}
