/*
 * Copyright © 2017 Intel Corporation
 *
 * 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 (including the next
 * paragraph) 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.
 *
 * Authors:
 *  Paul Kocialkowski <paul.kocialkowski@linux.intel.com>
 */

#include "config.h"

#include <fcntl.h>
#include <pixman.h>
#include <cairo.h>
#include <gsl/gsl_statistics_double.h>
#include <gsl/gsl_fit.h>

#include "igt_frame.h"
#include "igt_core.h"

/**
 * SECTION:igt_frame
 * @short_description: Library for frame-related tests
 * @title: Frame
 * @include: igt_frame.h
 *
 * This library contains helpers for frame-related tests. This includes common
 * frame dumping as well as frame comparison helpers.
 */

/**
 * igt_frame_dump_is_enabled:
 *
 * Get whether frame dumping is enabled.
 *
 * Returns: A boolean indicating whether frame dumping is enabled
 */
bool igt_frame_dump_is_enabled(void)
{
	return igt_frame_dump_path != NULL;
}

static void igt_write_frame_to_png(cairo_surface_t *surface, int fd,
				   const char *qualifier, const char *suffix)
{
	char path[PATH_MAX];
	const char *test_name;
	const char *subtest_name;
	cairo_status_t status;
	int index;

	test_name = igt_test_name();
	subtest_name = igt_subtest_name();

	if (suffix)
		snprintf(path, PATH_MAX, "%s/frame-%s-%s-%s-%s.png",
			 igt_frame_dump_path, test_name, subtest_name, qualifier,
			 suffix);
	else
		snprintf(path, PATH_MAX, "%s/frame-%s-%s-%s.png",
			 igt_frame_dump_path, test_name, subtest_name, qualifier);

	igt_debug("Dumping %s frame to %s...\n", qualifier, path);

	status = cairo_surface_write_to_png(surface, path);

	igt_assert_eq(status, CAIRO_STATUS_SUCCESS);

	index = strlen(path);

	if (fd >= 0 && index < (PATH_MAX - 1)) {
		path[index++] = '\n';
		path[index] = '\0';

		write(fd, path, strlen(path));
	}
}

/**
 * igt_write_compared_frames_to_png:
 * @reference: The reference cairo surface
 * @capture: The captured cairo surface
 * @reference_suffix: The suffix to give to the reference png file
 * @capture_suffix: The suffix to give to the capture png file
 *
 * Write previously compared frames to png files.
 */
void igt_write_compared_frames_to_png(cairo_surface_t *reference,
				      cairo_surface_t *capture,
				      const char *reference_suffix,
				      const char *capture_suffix)
{
	char *id;
	const char *test_name;
	const char *subtest_name;
	char path[PATH_MAX];
	int fd = -1;

	if (!igt_frame_dump_is_enabled())
		return;

	id = getenv("IGT_FRAME_DUMP_ID");

	test_name = igt_test_name();
	subtest_name = igt_subtest_name();

	if (id)
		snprintf(path, PATH_MAX, "%s/frame-%s-%s-%s.txt",
			 igt_frame_dump_path, test_name, subtest_name, id);
	else
		snprintf(path, PATH_MAX, "%s/frame-%s-%s.txt",
			 igt_frame_dump_path, test_name, subtest_name);

	fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
	igt_assert(fd >= 0);

	igt_debug("Writing dump report to %s...\n", path);

	igt_write_frame_to_png(reference, fd, "reference", reference_suffix);
	igt_write_frame_to_png(capture, fd, "capture", capture_suffix);

	close(fd);
}

/**
 * igt_check_analog_frame_match:
 * @reference: The reference cairo surface
 * @capture: The captured cairo surface
 *
 * Checks that the analog image contained in the chamelium frame dump matches
 * the given framebuffer.
 *
 * In order to determine whether the frame matches the reference, the following
 * reasoning is implemented:
 * 1. The absolute error for each color value of the reference is collected.
 * 2. The average absolute error is calculated for each color value of the
 *    reference and must not go above 60 (23.5 % of the total range).
 * 3. A linear fit for the average absolute error from the pixel value is
 *    calculated, as a DAC-ADC chain is expected to have a linear error curve.
 * 4. The linear fit is correlated with the actual average absolute error for
 *    the frame and the correlation coefficient is checked to be > 0.985,
 *    indicating a match with the expected error trend.
 *
 * Most errors (e.g. due to scaling, rotation, color space, etc) can be
 * reliably detected this way, with a minimized number of false-positives.
 * However, the brightest values (250 and up) are ignored as the error trend
 * is often not linear there in practice due to clamping.
 *
 * Returns: a boolean indicating whether the frames match
 */

bool igt_check_analog_frame_match(cairo_surface_t *reference,
				  cairo_surface_t *capture)
{
	pixman_image_t *reference_src, *capture_src;
	int w, h;
	int error_count[3][256][2] = { 0 };
	double error_average[4][250];
	double error_trend[250];
	double c0, c1, cov00, cov01, cov11, sumsq;
	double correlation;
	unsigned char *reference_pixels, *capture_pixels;
	unsigned char *p;
	unsigned char *q;
	bool match = true;
	int diff;
	int x, y;
	int i, j;

	w = cairo_image_surface_get_width(reference);
	h = cairo_image_surface_get_height(reference);

	reference_src = pixman_image_create_bits(
	    PIXMAN_x8r8g8b8, w, h,
	    (void*)cairo_image_surface_get_data(reference),
	    cairo_image_surface_get_stride(reference));
	reference_pixels = (unsigned char *) pixman_image_get_data(reference_src);

	capture_src = pixman_image_create_bits(
	    PIXMAN_x8r8g8b8, w, h,
	    (void*)cairo_image_surface_get_data(capture),
	    cairo_image_surface_get_stride(capture));
	capture_pixels = (unsigned char *) pixman_image_get_data(capture_src);

	/* Collect the absolute error for each color value */
	for (x = 0; x < w; x++) {
		for (y = 0; y < h; y++) {
			p = &capture_pixels[(x + y * w) * 4];
			q = &reference_pixels[(x + y * w) * 4];

			for (i = 0; i < 3; i++) {
				diff = (int) p[i] - q[i];
				if (diff < 0)
					diff = -diff;

				error_count[i][q[i]][0] += diff;
				error_count[i][q[i]][1]++;
			}
		}
	}

	/* Calculate the average absolute error for each color value */
	for (i = 0; i < 250; i++) {
		error_average[0][i] = i;

		for (j = 1; j < 4; j++) {
			error_average[j][i] = (double) error_count[j-1][i][0] /
					      error_count[j-1][i][1];

			if (error_average[j][i] > 60) {
				igt_warn("Error average too high (%f)\n",
					 error_average[j][i]);

				match = false;
				goto complete;
			}
		}
	}

	/*
	 * Calculate error trend from linear fit.
	 * A DAC-ADC chain is expected to have a linear absolute error on
	 * most of its range
	 */
	for (i = 1; i < 4; i++) {
		gsl_fit_linear((const double *) &error_average[0], 1,
			       (const double *) &error_average[i], 1, 250,
			       &c0, &c1, &cov00, &cov01, &cov11, &sumsq);

		for (j = 0; j < 250; j++)
			error_trend[j] = c0 + j * c1;

		correlation = gsl_stats_correlation((const double *) &error_trend,
						    1,
						    (const double *) &error_average[i],
						    1, 250);

		if (correlation < 0.985) {
			igt_warn("Error with reference not correlated (%f)\n",
				 correlation);

			match = false;
			goto complete;
		}
	}

complete:
	pixman_image_unref(reference_src);
	pixman_image_unref(capture_src);

	return match;
}
