/*
 * Copyright © 2012 Marek Olšák <maraeo@gmail.com>
 *
 * 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.
 */

#include "piglit-util-gl.h"

PIGLIT_GL_TEST_CONFIG_BEGIN

	config.supports_gl_compat_version = 10;

	config.window_visual = PIGLIT_GL_VISUAL_RGBA |
			       PIGLIT_GL_VISUAL_DOUBLE;
	config.khr_no_error_support = PIGLIT_NO_ERRORS;

PIGLIT_GL_TEST_CONFIG_END

enum piglit_result
piglit_display(void)
{
	/* UNREACHABLE */
	return PIGLIT_FAIL;
}

#define IMAGE_WIDTH 32
#define IMAGE_HEIGHT 32
#define IMAGE_SIZE (IMAGE_WIDTH * IMAGE_HEIGHT * 4)

static void
init_layer_data(GLubyte *layer_data, int num_layers)
{
	int x, y, z, i, j;

	for (z = 0; z < num_layers; z++) {
		GLubyte *data = layer_data + IMAGE_SIZE * z;

		for (x = 0; x < IMAGE_WIDTH; x += 4) {
			for (y = 0; y < IMAGE_HEIGHT; y += 4) {
				int r = (x + 1) * 255 / (IMAGE_WIDTH - 1);
				int g = (y + 1) * 255 / (IMAGE_HEIGHT - 1);
				int b = (z + 1) * 255 / (num_layers - 1);
				int a = x ^ y ^ z;

				/* each 4x4 block contains only one color (for S3TC) */
				for (i = 0; i < 4; i++) {
					for (j = 0; j < 4; j++) {
						data[((y + j) * IMAGE_WIDTH + x
						      + i) * 4 + 0] = r;
						data[((y + j) * IMAGE_WIDTH + x
						      + i) * 4 + 1] = g;
						data[((y + j) * IMAGE_WIDTH + x
						      + i) * 4 + 2] = b;
						data[((y + j) * IMAGE_WIDTH + x
						      + i) * 4 + 3] = a;
					}
				}
			}
		}
	}
}

static bool
compare_layer(int layer, int num_elements, int tolerance,
			  GLubyte *data, GLubyte *expected)
{
	int i;

	for (i = 0; i < num_elements; ++i) {
		if (abs((int)data[i] - (int)expected[i]) > tolerance) {
			printf("GetTexImage() returns incorrect data in byte %i for layer %i\n",
			       i, layer);
			printf("    corresponding to (%i,%i), channel %i\n",
			       (i / 4) % IMAGE_WIDTH, (i / 4) / IMAGE_WIDTH, i % 4);
			printf("    expected: %i\n", expected[i]);
			printf("    got: %i\n", data[i]);
			return false;
		}
	}
	return true;
}

static bool
getTexImage(bool doPBO, GLenum target, GLubyte data[][IMAGE_SIZE],
	    GLenum internalformat, int tolerance)
{
	int i;
	int num_layers=1, num_faces=1, layer_size;
	GLubyte data2[18][IMAGE_SIZE];
	GLubyte *dataGet;
	GLuint tex, packPBO;
	bool pass = true;

	glGenTextures(1, &tex);
	glBindTexture(target, tex);

	switch (target) {
	case GL_TEXTURE_1D:
		glTexImage1D(GL_TEXTURE_1D, 0, internalformat, IMAGE_WIDTH, 0,
			     GL_RGBA, GL_UNSIGNED_BYTE, data);
		layer_size = IMAGE_WIDTH * 4;
		break;

	case GL_TEXTURE_2D:
	case GL_TEXTURE_RECTANGLE:
		glTexImage2D(target, 0, internalformat, IMAGE_WIDTH,
			     IMAGE_HEIGHT, 0,
			     GL_RGBA, GL_UNSIGNED_BYTE, data);
		layer_size = IMAGE_SIZE;
		break;

	case GL_TEXTURE_3D:
		num_layers = 16;
		glTexImage3D(GL_TEXTURE_3D, 0, internalformat,
			     IMAGE_WIDTH, IMAGE_HEIGHT, num_layers, 0, GL_RGBA,
			     GL_UNSIGNED_BYTE, data);
		layer_size = IMAGE_SIZE;
		break;

	case GL_TEXTURE_CUBE_MAP:
		num_faces = 6;
		for (i = 0; i < num_faces; i++) {
			glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0,
				     internalformat, IMAGE_WIDTH, IMAGE_HEIGHT,
				     0, GL_RGBA,
				     GL_UNSIGNED_BYTE, data[i]);
		}
		target = GL_TEXTURE_CUBE_MAP_POSITIVE_X;
		layer_size = IMAGE_SIZE;
		break;

	case GL_TEXTURE_1D_ARRAY:
		num_layers = 7;
		glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, internalformat,
			     IMAGE_WIDTH, num_layers, 0,
			     GL_RGBA, GL_UNSIGNED_BYTE, data);
		// test as a single layer 2D image
		layer_size = IMAGE_WIDTH * 4 * num_layers;
		num_layers = 1;
		break;

	case GL_TEXTURE_2D_ARRAY:
		num_layers = 7;
		glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, internalformat,
			     IMAGE_WIDTH, IMAGE_HEIGHT, num_layers, 0,
			     GL_RGBA, GL_UNSIGNED_BYTE, data);
		layer_size = IMAGE_SIZE;
		break;

	case GL_TEXTURE_CUBE_MAP_ARRAY:
		num_layers = 6 * 3;
		glTexImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, internalformat,
			     IMAGE_WIDTH, IMAGE_HEIGHT, num_layers, 0, GL_RGBA,
			     GL_UNSIGNED_BYTE, data);
		layer_size = IMAGE_SIZE;
		break;

	default:
		puts("Invalid texture target.");
		return false;

	}

	/* Setup the PBO or data array to read into from glGetTexImage */
	if (doPBO) {
		glGenBuffers(1, &packPBO);
		glBindBuffer(GL_PIXEL_PACK_BUFFER, packPBO);
		glBufferData(GL_PIXEL_PACK_BUFFER,
				     layer_size * MAX2(num_faces,num_layers),
				     NULL, GL_STREAM_READ);
	} else {
		glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
		memset(data2, 123, sizeof(data2));
	}
	pass = piglit_check_gl_error(GL_NO_ERROR) && pass;
	assert(num_layers * num_faces * layer_size <= sizeof(data2));

	for (i = 0; i < num_faces; i++) {
		if (doPBO) {
			glGetTexImage(target + i, 0, GL_RGBA, GL_UNSIGNED_BYTE,
				      (GLvoid *) (long) (i * layer_size));
		} else {
			glGetTexImage(target + i, 0, GL_RGBA, GL_UNSIGNED_BYTE,
				      data2[i]);
		}
		pass = piglit_check_gl_error(GL_NO_ERROR) && pass;

	}
	if (doPBO)
		dataGet = (GLubyte *) glMapBufferRange(
					       GL_PIXEL_PACK_BUFFER, 0,
					       layer_size *
					       MAX2(num_faces,num_layers),
					       GL_MAP_READ_BIT);
	else
		dataGet = data2[0];

	for (i = 0; i < MAX2(num_faces,num_layers); i++) {
		pass = compare_layer(i, layer_size, tolerance, dataGet,
				     data[i]) && pass;
		dataGet += layer_size;
	}

	if (doPBO) {
		glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
		glDeleteBuffers(1, &packPBO);
	}

	glDeleteTextures(1, &tex);
	return pass;
}

NORETURN void
print_usage_and_exit(char *prog_name)
{
	printf("Usage: %s <target> <compression>\n"
	       "  where <target> is one of:\n"
	       "    1D\n"
	       "    2D\n"
	       "    3D\n"
	       "    RECT\n"
	       "    CUBE\n"
	       "    1D_ARRAY\n"
	       "    2D_ARRAY\n"
	       "    CUBE_ARRAY\n"
	       "  where <compression> is optional and can be one of:\n"
	       "    S3TC: Valid only with 2D* and CUBE* targets\n",
	       prog_name);
	piglit_report_result(PIGLIT_FAIL);
}

void
piglit_init(int argc, char **argv)
{
	GLenum target;
	bool pass = true;
	GLenum internalformat = GL_RGBA8;
	GLubyte data[18][IMAGE_SIZE];
	int tolerance = 0;

	if (argc < 2 || argc > 3)
		print_usage_and_exit(argv[0]);

	if (strcmp(argv[1], "1D") == 0) {
		target = GL_TEXTURE_1D;
	}
	else if (strcmp(argv[1], "2D") == 0) {
		target = GL_TEXTURE_2D;
	}
	else if (strcmp(argv[1], "3D") == 0) {
		target = GL_TEXTURE_3D;
		piglit_require_gl_version(12);
	}
	else if (strcmp(argv[1], "RECT") == 0) {
		target = GL_TEXTURE_RECTANGLE;
		piglit_require_extension("GL_ARB_texture_rectangle");
	}
	else if (strcmp(argv[1], "CUBE") == 0) {
		target = GL_TEXTURE_CUBE_MAP;
		piglit_require_extension("GL_ARB_texture_cube_map");
	}
	else if (strcmp(argv[1], "1D_ARRAY") == 0) {
		target = GL_TEXTURE_1D_ARRAY;
		piglit_require_extension("GL_EXT_texture_array");
	}
	else if (strcmp(argv[1], "2D_ARRAY") == 0) {
		target = GL_TEXTURE_2D_ARRAY;
		piglit_require_extension("GL_EXT_texture_array");
	}
	else if (strcmp(argv[1], "CUBE_ARRAY") == 0) {
		target = GL_TEXTURE_CUBE_MAP_ARRAY;
		piglit_require_extension("GL_ARB_texture_cube_map_array");
	}
	else {
		print_usage_and_exit(argv[0]);
	}

	if (argc == 3 && strcmp(argv[2], "S3TC") == 0 &&
	    (target == GL_TEXTURE_2D ||
	     target == GL_TEXTURE_2D_ARRAY ||
	     target == GL_TEXTURE_CUBE_MAP ||
	     target == GL_TEXTURE_CUBE_MAP_ARRAY)) {
		internalformat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
		tolerance = 8;
		if (!piglit_is_extension_supported("GL_EXT_texture_compression_s3tc")
		    && !piglit_is_extension_supported("GL_ANGLE_texture_compression_dxt5")) {
			fprintf(stderr,
				"S3TC testing requires either "
				"GL_EXT_texture_compression_s3tc or "
				"GL_ANGLE_texture_compression_dxt5 "
				"extension be supported.\n");
			piglit_report_result(PIGLIT_SKIP);
		}
		puts("Testing S3TC.");
	} else if (argc == 3) {
		print_usage_and_exit(argv[0]);
	}

	init_layer_data(data[0], 18);

	printf("Testing %s into PBO\n", piglit_get_gl_enum_name(target));
	pass = getTexImage(true, target, data, internalformat, tolerance) &&
				pass;

	printf("Testing %s into client array\n", piglit_get_gl_enum_name(target));
	pass = getTexImage(false, target, data, internalformat, tolerance) &&
				pass;

	pass = piglit_check_gl_error(GL_NO_ERROR) && pass;
	if (pass)
		piglit_report_result(PIGLIT_PASS);
	else
		piglit_report_result(PIGLIT_FAIL);
}
