/* reorder.c ... manage reorder reordering
 *
 * 11/1/17
 * 	- first version
 */

/*

	This file is part of VIPS.

	VIPS 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 of the License, or
	(at your option) any later version.

	This program 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 program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
	02110-1301  USA

 */

/*

	These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk

 */

/*
#define DEBUG
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /*HAVE_CONFIG_H*/
#include <glib/gi18n-lib.h>

#include <vips/vips.h>
#include <vips/internal.h>
#include <vips/debug.h>

/* Have one of these on every image, identified by a quark.
 */
typedef struct _VipsReorder {
	/* The image we are attached to.
	 */
	VipsImage *image;

	/* The direct inputs to this image, so a copy of the array that is
	 * passed to vips_image_pipeline_array(), and in the same order.
	 * NULL-terminated.
	 *
	 * Score is the priority we give to the inputs as we de-dupe the source
	 * arrays.
	 *
	 * The recomp order is the order we prepare regions in ... just make a
	 * range then sort by score.
	 */
	int n_inputs;
	VipsImage **input;
	int *score;
	int *recomp_order;

	/* Source images are images with no input images, so file load,
	 * vips_black(), etc. NULL-terminated array.
	 *
	 * The cumulative margin is the total margin that has been added to
	 * each source image up to this point in the pipeline.
	 */
	int n_sources;
	VipsImage **source;
	int *cumulative_margin;

} VipsReorder;

GQuark vips__image_reorder_quark = 0;

#ifdef DEBUG
static void
vips_reorder_print(VipsReorder *reorder)
{
	int i;

	printf("vips_reorder_print: ");
	vips_object_print_name(VIPS_OBJECT(reorder->image));
	printf("\n");

	printf("n_inputs = %d\n", reorder->n_inputs);
	printf(" n      score       order\n");
	for (i = 0; i < reorder->n_inputs; i++) {
		printf("%2d - %8d, %8d, ",
			i, reorder->score[i], reorder->recomp_order[i]);
		vips_object_print_name(VIPS_OBJECT(reorder->input[i]));
		printf("\n");
	}

	printf("n_sources = %d\n", reorder->n_sources);
	printf(" n     margin\n");
	for (i = 0; i < reorder->n_sources; i++) {
		printf("%2d - %8d, ",
			i, reorder->cumulative_margin[i]);
		vips_object_print_name(VIPS_OBJECT(reorder->source[i]));
		printf("\n");
	}
}
#endif /*DEBUG*/

static void
vips_reorder_free(VipsReorder *reorder)
{
	/* We free explicitly, rather than using VIPS_ARRAY(image, ...), since
	 * we need to make sure these pointers are valid to this point in the
	 * close cycle.
	 */
	VIPS_FREE(reorder->input);
	VIPS_FREE(reorder->score);
	VIPS_FREE(reorder->recomp_order);
	VIPS_FREE(reorder->source);
	VIPS_FREE(reorder->cumulative_margin);
}

static void
vips_reorder_destroy(VipsReorder *reorder)
{
	vips_reorder_free(reorder);
	VIPS_FREE(reorder);
}

static VipsReorder *
vips_reorder_get(VipsImage *image)
{
	VipsReorder *reorder;

	if ((reorder = g_object_get_qdata(G_OBJECT(image),
			 vips__image_reorder_quark)))
		return reorder;

	reorder = VIPS_NEW(NULL, VipsReorder);
	reorder->image = image;
	reorder->n_inputs = 0;
	reorder->input = NULL;
	reorder->score = NULL;
	reorder->recomp_order = NULL;
	reorder->n_sources = 0;
	reorder->source = NULL;
	reorder->cumulative_margin = NULL;

	g_object_set_qdata_full(G_OBJECT(image), vips__image_reorder_quark,
		reorder, (GDestroyNotify) vips_reorder_destroy);

	return reorder;
}

static int
vips_reorder_compare_score(const void *a, const void *b, void *arg)
{
	int i1 = *((int *) a);
	int i2 = *((int *) b);
	VipsReorder *reorder = (VipsReorder *) arg;

	return reorder->score[i2] - reorder->score[i1];
}

int
vips__reorder_set_input(VipsImage *image, VipsImage **in)
{
	VipsReorder *reorder = vips_reorder_get(image);

	int i;
	int total;

	/* We have to support being called more than once on the same image.
	 * Two cases:
	 *
	 * 1. in the first call, no images were set ... we throw away
	 * everything from the first call and try again. foreign can do this.
	 *
	 * 2. warn if the args were different and do nothing.
	 */
	if (reorder->source) {
		if (reorder->n_inputs == 0) {
			reorder->n_sources = 0;
			vips_reorder_free(reorder);
		}
		else {
			for (i = 0; in[i]; i++)
				if (i >= reorder->n_inputs ||
					in[i] != reorder->input[i]) {
					/* Should never happen.
					 */
					g_warning("vips__reorder_set_input: args differ\n");
					break;
				}

			return 0;
		}
	}

	/* Make a copy of the input array.
	 */
	for (i = 0; in[i]; i++)
		;
	reorder->n_inputs = i;
	reorder->input = VIPS_ARRAY(NULL, reorder->n_inputs + 1, VipsImage *);
	reorder->score = VIPS_ARRAY(NULL, reorder->n_inputs, int);
	reorder->recomp_order = VIPS_ARRAY(NULL, reorder->n_inputs, int);
	if (!reorder->input)
		return -1;
	if (reorder->n_inputs &&
		(!reorder->score ||
			!reorder->recomp_order))
		return -1;

	for (i = 0; i < reorder->n_inputs; i++) {
		reorder->input[i] = in[i];
		reorder->score[i] = 0;
		reorder->recomp_order[i] = i;
	}
	reorder->input[i] = NULL;

	/* Find the total number of source images -- this gives an upper bound
	 * to the size of the unique source image array we will need.
	 */
	total = 0;
	for (i = 0; i < reorder->n_inputs; i++)
		total += vips_reorder_get(reorder->input[i])->n_sources;

	/* No source images means this must itself be a source image, so it has
	 * a source image of itself.
	 */
	total = VIPS_MAX(1, total);

	reorder->source = VIPS_ARRAY(NULL, total + 1, VipsImage *);
	reorder->cumulative_margin = VIPS_ARRAY(NULL, total, int);
	if (!reorder->source ||
		!reorder->cumulative_margin)
		return -1;

	/* Copy source images over, removing duplicates. If we find a
	 * duplicate, we have a reordering opportunity, and we adjust the
	 * scores of the two images containing the dupe.
	 */
	for (i = 0; i < reorder->n_inputs; i++) {
		VipsReorder *input = vips_reorder_get(reorder->input[i]);

		int j;

		for (j = 0; j < input->n_sources; j++) {
			int k;

			/* Search for dupe.
			 */
			for (k = 0; k < reorder->n_sources; k++)
				if (reorder->source[k] == input->source[j])
					break;

			if (k < reorder->n_sources) {
				/* Found a dupe. Does this new use of
				 * input->source[j] have a larger or smaller
				 * margin? Adjust the score to reflect the
				 * change, note the new max.
				 */
				reorder->score[i] +=
					input->cumulative_margin[j] -
					reorder->cumulative_margin[k];

				reorder->cumulative_margin[k] = VIPS_MAX(
					reorder->cumulative_margin[k],
					input->cumulative_margin[j]);
			}
			else {
				/* No dupe, just add to the table.
				 */
				reorder->source[reorder->n_sources] =
					input->source[j];
				reorder->cumulative_margin[reorder->n_sources] =
					input->cumulative_margin[j];
				reorder->n_sources += 1;
			}
		}
	}

	/* Sort recomp_order by score. qsort_r() is a GNU libc thing, don't use
	 * it.
	 */
	if (reorder->n_inputs > 1)
#if GLIB_CHECK_VERSION(2, 82, 0)
		g_sort_array
#else
		g_qsort_with_data
#endif
			(reorder->recomp_order,
				reorder->n_inputs,
				sizeof(int),
				vips_reorder_compare_score, reorder);

	/* No sources ... make one, us!
	 */
	if (reorder->n_inputs == 0) {
		reorder->source[0] = image;
		reorder->cumulative_margin[0] = 0;
		reorder->n_sources = 1;
	}

#ifdef DEBUG
	vips_reorder_print(reorder);
#endif /*DEBUG*/

	return 0;
}

/**
 * vips_reorder_prepare_many: (method)
 * @image: the image that's being written
 * @regions: (array): the set of regions to prepare
 * @r: the [struct@Rect] to prepare on each region
 *
 * [method@Image.reorder_prepare_many] runs [method@Region.prepare] on each
 * region in @regions, requesting the pixels in @r.
 *
 * It tries to request the regions in the order which will cause least
 * recomputation. This can give a large speedup, in some cases.
 *
 * ::: seealso
 *     [method@Region.prepare], [method@Image.reorder_margin_hint].
 *
 * Returns: 0 on success, or -1 on error.
 */
int
vips_reorder_prepare_many(VipsImage *image, VipsRegion **regions, VipsRect *r)
{
	VipsReorder *reorder = vips_reorder_get(image);

	int i;

	for (i = 0; i < reorder->n_inputs; i++) {
		g_assert(regions[i]);

		if (vips_region_prepare(
				regions[reorder->recomp_order[i]], r))
			return -1;
	}

	return 0;
}

/**
 * vips_reorder_margin_hint: (method)
 * @image: the image to hint on
 * @margin: the size of the margin this operation has added
 *
 * [method@Image.reorder_margin_hint] sets a hint that @image contains a
 * margin, that is, that each [method@Region.prepare] on @image will request
 * a slightly larger region from it's inputs. A good value for @margin is
 * (width * height) for the window the operation uses.
 *
 * This information is used by [method@Image.reorder_prepare_many] to attempt to
 * reorder computations to minimise recomputation.
 *
 * ::: seealso
 *     [method@Image.reorder_prepare_many].
 */
void
vips_reorder_margin_hint(VipsImage *image, int margin)
{
	VipsReorder *reorder = vips_reorder_get(image);

	int i;

	for (i = 0; i < reorder->n_sources; i++)
		reorder->cumulative_margin[i] += margin;
}

void
vips__reorder_clear(VipsImage *image)
{
	g_object_set_qdata(G_OBJECT(image),
		vips__image_reorder_quark, NULL);
}

void
vips__reorder_init(void)
{
	if (!vips__image_reorder_quark)
		vips__image_reorder_quark =
			g_quark_from_static_string("vips-image-reorder");
}
