/**************************************************************************
 *
 * Copyright 2009 Younes Manton.
 * All Rights Reserved.
 *
 * 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, sub license, 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 NON-INFRINGEMENT.
 * IN NO EVENT SHALL VMWARE AND/OR ITS SUPPLIERS 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.
 *
 **************************************************************************/

/* Force assertions, even on release builds. */
#undef NDEBUG
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "testlib.h"

#define BLOCK_WIDTH			8
#define BLOCK_HEIGHT			8
#define BLOCK_SIZE			(BLOCK_WIDTH * BLOCK_HEIGHT)
#define MACROBLOCK_WIDTH		16
#define MACROBLOCK_HEIGHT		16
#define MACROBLOCK_WIDTH_IN_BLOCKS	(MACROBLOCK_WIDTH / BLOCK_WIDTH)
#define MACROBLOCK_HEIGHT_IN_BLOCKS	(MACROBLOCK_HEIGHT / BLOCK_HEIGHT)
#define BLOCKS_PER_MACROBLOCK		6

#define INPUT_WIDTH			64
#define INPUT_HEIGHT			64
#define INPUT_WIDTH_IN_MACROBLOCKS	(INPUT_WIDTH / MACROBLOCK_WIDTH)
#define INPUT_HEIGHT_IN_MACROBLOCKS	(INPUT_HEIGHT / MACROBLOCK_HEIGHT)
#define NUM_MACROBLOCKS			(INPUT_WIDTH_IN_MACROBLOCKS * INPUT_HEIGHT_IN_MACROBLOCKS)

#define DEFAULT_OUTPUT_WIDTH		INPUT_WIDTH
#define DEFAULT_OUTPUT_HEIGHT		INPUT_HEIGHT
#define DEFAULT_ACCEPTABLE_ERR		0.01

static void ParseArgs(int argc, char **argv, unsigned int *output_width, unsigned int *output_height, double *acceptable_error, int *prompt)
{
	int fail = 0;
	int i;

	*output_width = DEFAULT_OUTPUT_WIDTH;
	*output_height = DEFAULT_OUTPUT_HEIGHT;
	*acceptable_error = DEFAULT_ACCEPTABLE_ERR;
	*prompt = 0;

	for (i = 1; i < argc && !fail; ++i)
	{
		if (!strcmp(argv[i], "-w"))
		{
			if (sscanf(argv[++i], "%u", output_width) != 1)
				fail = 1;
		}
		else if (!strcmp(argv[i], "-h"))
		{
			if (sscanf(argv[++i], "%u", output_height) != 1)
				fail = 1;
		}
		else if (!strcmp(argv[i], "-e"))
		{
			if (sscanf(argv[++i], "%lf", acceptable_error) != 1)
				fail = 1;
		}
		else if (!strcmp(argv[i], "-p"))
			*prompt = 1;
		else
			fail = 1;
	}

	if (fail)
	{
		fprintf(
			stderr,
			"Bad argument.\n"
			"\n"
			"Usage: %s [options]\n"
			"\t-w <width>\tOutput width\n"
			"\t-h <height>\tOutput height\n"
			"\t-e <error>\tAcceptable margin of error per pixel, from 0 to 1\n"
			"\t-p\tPrompt for quit\n",
			argv[0]
		);
		exit(1);
	}
}

static void Gradient(short *block, unsigned int start, unsigned int stop, int horizontal, unsigned int intra_unsigned)
{
	unsigned int x, y;
	unsigned int range = stop - start;

	if (horizontal)
	{
		for (y = 0; y < BLOCK_HEIGHT; ++y)
			for (x = 0; x < BLOCK_WIDTH; ++x) {
				*block = (short)(start + range * (x / (float)(BLOCK_WIDTH - 1)));
				if (intra_unsigned)
					*block += 1 << 10;
				block++;
			}
	}
	else
	{
		for (y = 0; y < BLOCK_HEIGHT; ++y)
			for (x = 0; x < BLOCK_WIDTH; ++x) {
				*block = (short)(start + range * (y / (float)(BLOCK_WIDTH - 1)));
				if (intra_unsigned)
					*block += 1 << 10;
				block++;
			}
	}
}

int main(int argc, char **argv)
{
	unsigned int		output_width;
	unsigned int		output_height;
	double			acceptable_error;
	int			prompt;
	Display			*display;
	Window			root, window;
	const unsigned int	mc_types[] = {XVMC_MOCOMP | XVMC_MPEG_2};
	XvPortID		port_num;
	int			surface_type_id;
	unsigned int		is_overlay, intra_unsigned;
	int			colorkey;
	XvMCContext		context;
	XvMCSurface		surface;
	XvMCBlockArray		block_array;
	XvMCMacroBlockArray	mb_array;
	int			mbx, mby, bx, by;
	XvMCMacroBlock		*mb;
	short			*blocks;
	int			quit = 0;

	ParseArgs(argc, argv, &output_width, &output_height, &acceptable_error, &prompt);

	display = XOpenDisplay(NULL);

	if (!GetPort
	(
		display,
		INPUT_WIDTH,
		INPUT_HEIGHT,
		XVMC_CHROMA_FORMAT_420,
		mc_types,
		sizeof(mc_types)/sizeof(*mc_types),
		&port_num,
		&surface_type_id,
		&is_overlay,
		&intra_unsigned
	))
	{
		XCloseDisplay(display);
		fprintf(stderr, "Error, unable to find a good port.\n");
		exit(1);
	}

	if (is_overlay)
	{
		Atom xv_colorkey = XInternAtom(display, "XV_COLORKEY", 0);
		XvGetPortAttribute(display, port_num, xv_colorkey, &colorkey);
	}

	root = XDefaultRootWindow(display);
	window = XCreateSimpleWindow(display, root, 0, 0, output_width, output_height, 0, 0, colorkey);

	assert(XvMCCreateContext(display, port_num, surface_type_id, INPUT_WIDTH, INPUT_HEIGHT, XVMC_DIRECT, &context) == Success);
	assert(XvMCCreateSurface(display, &context, &surface) == Success);
	assert(XvMCCreateBlocks(display, &context, NUM_MACROBLOCKS * BLOCKS_PER_MACROBLOCK, &block_array) == Success);
	assert(XvMCCreateMacroBlocks(display, &context, NUM_MACROBLOCKS, &mb_array) == Success);

	mb = mb_array.macro_blocks;
	blocks = block_array.blocks;

	for (mby = 0; mby < INPUT_HEIGHT_IN_MACROBLOCKS; ++mby)
		for (mbx = 0; mbx < INPUT_WIDTH_IN_MACROBLOCKS; ++mbx)
		{
			mb->x = mbx;
			mb->y = mby;
			mb->macroblock_type = XVMC_MB_TYPE_INTRA;
			/*mb->motion_type = ;*/
			/*mb->motion_vertical_field_select = ;*/
			mb->dct_type = XVMC_DCT_TYPE_FRAME;
			/*mb->PMV[0][0][0] = ;
			mb->PMV[0][0][1] = ;
			mb->PMV[0][1][0] = ;
			mb->PMV[0][1][1] = ;
			mb->PMV[1][0][0] = ;
			mb->PMV[1][0][1] = ;
			mb->PMV[1][1][0] = ;
			mb->PMV[1][1][1] = ;*/
			mb->index = (mby * INPUT_WIDTH_IN_MACROBLOCKS + mbx) * BLOCKS_PER_MACROBLOCK;
			mb->coded_block_pattern = 0x3F;

			mb++;

			for (by = 0; by < MACROBLOCK_HEIGHT_IN_BLOCKS; ++by)
				for (bx = 0; bx < MACROBLOCK_WIDTH_IN_BLOCKS; ++bx)
				{
					const int start = 16, stop = 235, range = stop - start;

					Gradient
					(
						blocks,
						(short)(start + range * ((mbx * MACROBLOCK_WIDTH + bx * BLOCK_WIDTH) / (float)(INPUT_WIDTH - 1))),
						(short)(start + range * ((mbx * MACROBLOCK_WIDTH + bx * BLOCK_WIDTH + BLOCK_WIDTH - 1) / (float)(INPUT_WIDTH - 1))),
						1,
						intra_unsigned
					);

					blocks += BLOCK_SIZE;
				}

			for (by = 0; by < MACROBLOCK_HEIGHT_IN_BLOCKS / 2; ++by)
				for (bx = 0; bx < MACROBLOCK_WIDTH_IN_BLOCKS / 2; ++bx)
				{
					const int start = 16, stop = 240, range = stop - start;

					Gradient
					(
						blocks,
						(short)(start + range * ((mbx * MACROBLOCK_WIDTH + bx * BLOCK_WIDTH) / (float)(INPUT_WIDTH - 1))),
						(short)(start + range * ((mbx * MACROBLOCK_WIDTH + bx * BLOCK_WIDTH + BLOCK_WIDTH - 1) / (float)(INPUT_WIDTH - 1))),
						1,
						intra_unsigned
					);

					blocks += BLOCK_SIZE;

					Gradient
					(
						blocks,
						(short)(start + range * ((mbx * MACROBLOCK_WIDTH + bx * BLOCK_WIDTH) / (float)(INPUT_WIDTH - 1))),
						(short)(start + range * ((mbx * MACROBLOCK_WIDTH + bx * BLOCK_WIDTH + BLOCK_WIDTH - 1) / (float)(INPUT_WIDTH - 1))),
						1,
						intra_unsigned
					);

					blocks += BLOCK_SIZE;
				}
		}

	XSelectInput(display, window, ExposureMask | KeyPressMask);
	XMapWindow(display, window);
	XSync(display, 0);

	/* Test NULL context */
	assert(XvMCRenderSurface(display, NULL, XVMC_FRAME_PICTURE, &surface, NULL, NULL, 0, NUM_MACROBLOCKS, 0, &mb_array, &block_array) == XvMCBadContext);
	/* Test NULL surface */
	assert(XvMCRenderSurface(display, &context, XVMC_FRAME_PICTURE, NULL, NULL, NULL, 0, NUM_MACROBLOCKS, 0, &mb_array, &block_array) == XvMCBadSurface);
	/* Test bad picture structure */
	assert(XvMCRenderSurface(display, &context, 0, &surface, NULL, NULL, 0, NUM_MACROBLOCKS, 0, &mb_array, &block_array) == BadValue);
	/* Test valid params */
	assert(XvMCRenderSurface(display, &context, XVMC_FRAME_PICTURE, &surface, NULL, NULL, 0, NUM_MACROBLOCKS, 0, &mb_array, &block_array) == Success);

	/* Test NULL surface */
	assert(XvMCPutSurface(display, NULL, window, 0, 0, INPUT_WIDTH, INPUT_HEIGHT, 0, 0, output_width, output_height, XVMC_FRAME_PICTURE) == XvMCBadSurface);
	/* Test bad window */
	/* XXX: X halts with a bad drawable for some reason, doesn't return BadDrawable as expected */
	/*assert(XvMCPutSurface(display, &surface, 0, 0, 0, width, height, 0, 0, width, height, XVMC_FRAME_PICTURE) == BadDrawable);*/

	if (prompt)
	{
		puts("Press any button to quit...");

		while (!quit)
		{
			if (XPending(display) > 0)
			{
				XEvent event;

				XNextEvent(display, &event);

				switch (event.type)
				{
					case Expose:
					{
						/* Test valid params */
						assert
						(
							XvMCPutSurface
							(
								display, &surface, window,
								0, 0, INPUT_WIDTH, INPUT_HEIGHT,
								0, 0, output_width, output_height,
								XVMC_FRAME_PICTURE
							) == Success
						);
						break;
					}
					case KeyPress:
					{
						quit = 1;
						break;
					}
				}
			}
		}
	}

	assert(XvMCDestroyBlocks(display, &block_array) == Success);
	assert(XvMCDestroyMacroBlocks(display, &mb_array) == Success);
	assert(XvMCDestroySurface(display, &surface) == Success);
	assert(XvMCDestroyContext(display, &context) == Success);

	XvUngrabPort(display, port_num, CurrentTime);
	XDestroyWindow(display, window);
	XCloseDisplay(display);

	return 0;
}
