/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2011 Richard Hughes <richard@hughsie.com>
 *
 * Licensed under the GNU General Public License Version 2
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU 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 General Public License for more details.
 *
 * You should have received a copy of the GNU 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.
 */

#include "config.h"

#include <string.h>
#include <glib.h>
#include <glib-object.h>
#include <gusb.h>
#include <math.h>
#include <colord.h>

#include "ch-math.h"
#include "ch-hash.h"
#include "ch-device.h"
#include "ch-device-queue.h"

static void
ch_test_hash_func (void)
{
	ChSha1 sha1;
	gboolean ret;
	gchar *str;
	g_autoptr(GError) error = NULL;

	/* parse into structure */
	ret = ch_sha1_parse ("f18973b4ebaeab527dc15d5dd246debfbff20324",
			     &sha1, &error);
	g_assert_no_error (error);
	g_assert (ret);
	g_assert_cmpint (sha1.bytes[0], == ,241);
	g_assert_cmpint (sha1.bytes[1], ==, 137);

	/* print back to string */
	str = ch_sha1_to_string (&sha1);
	g_assert_cmpstr (str, ==, "f18973b4ebaeab527dc15d5dd246debfbff20324");
	g_free (str);
}

static guint device_failed_cnt = 0;
static guint progress_changed_cnt = 0;

static void
ch_test_device_queue_device_failed_cb (ChDeviceQueue	*device_queue,
				       GUsbDevice	*device,
				       const gchar	*error_message,
				       gpointer		 user_data)
{
	device_failed_cnt++;
	g_debug ("device %s down, error: %s",
		 g_usb_device_get_platform_id (device),
		 error_message);
}

static void
ch_test_device_queue_progress_changed_cb (ChDeviceQueue	*device_queue,
					  guint		 percentage,
					  gpointer	 user_data)
{
	progress_changed_cnt++;
	g_debug ("queue complete %u%%",
		 percentage);
}

static void
ch_test_device_queue_func (void)
{
	ChDeviceQueue *device_queue;
	gboolean ret;
	g_autoptr(GError) error = NULL;
	GPtrArray *devices;
	guint i;
	guint valid_devices = 0;
	GUsbContext *usb_ctx;
	GUsbDevice *device;

	/* try to find any ColorHug devices */
	usb_ctx = g_usb_context_new (NULL);
	if (usb_ctx == NULL)
		return;

	devices = g_usb_context_get_devices (usb_ctx);

	/* watch for any failed devices */
	device_queue = ch_device_queue_new ();
	g_signal_connect (device_queue,
			  "device-failed",
			  G_CALLBACK (ch_test_device_queue_device_failed_cb),
			  NULL);
	g_signal_connect (device_queue,
			  "progress-changed",
			  G_CALLBACK (ch_test_device_queue_progress_changed_cb),
			  NULL);
	for (i = 0; i < devices->len; i++) {
		device = g_ptr_array_index (devices, i);
		if (g_usb_device_get_vid (device) != CH_USB_VID)
			continue;
		if (g_usb_device_get_pid (device) != CH_USB_PID_FIRMWARE &&
		    g_usb_device_get_pid (device) != CH_USB_PID_FIRMWARE_PLUS)
			continue;

		valid_devices++;
		g_debug ("Found ColorHug device %s",
			 g_usb_device_get_platform_id (device));

		/* load device */
		ret = ch_device_open (device, &error);
		g_assert_no_error (error);
		g_assert (ret);

		/* set RED to queue */
		ch_device_queue_set_leds (device_queue,
				          device,
				          CH_STATUS_LED_RED,
				          50,
				          100,
				          5);

		/* set GREEN to queue */
		ch_device_queue_set_leds (device_queue,
				          device,
				          CH_STATUS_LED_GREEN,
				          50,
				          100,
				          5);

		/* do unknown command */
		ch_device_queue_add (device_queue,
				     device,
				     0xff,
				     NULL,
				     0,
				     NULL,
				     0);

		/* set ALL to queue */
		ch_device_queue_set_leds (device_queue,
				          device,
				          CH_STATUS_LED_RED |
				          CH_STATUS_LED_GREEN |
				          CH_STATUS_LED_BLUE,
				          50,
				          100,
				          5);
	}

	/* fix make check with no hardware attached */
	if (valid_devices == 0)
		goto out;

	/* process queue */
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONFATAL_ERRORS,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);

	/* check we failed both devices */
	g_assert_cmpint (device_failed_cnt, ==, valid_devices);

	/* check we got enough progress updates */
	if (valid_devices > 0)
		g_assert_cmpint (progress_changed_cnt, ==, valid_devices * 3 + 1);

	/* fail on unknown command */
	for (i = 0; i < devices->len; i++) {
		device = g_ptr_array_index (devices, i);
		if (g_usb_device_get_vid (device) != CH_USB_VID)
			continue;
		if (g_usb_device_get_pid (device) != CH_USB_PID_FIRMWARE)
			continue;
		ch_device_queue_add (device_queue,
				     device,
				     0xff,
				     NULL,
				     0,
				     NULL,
				     0);
	}

	/* process queue */
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_error (error, 1, 0);
	g_debug ("error was: %s", error->message);
	g_assert (!ret);
out:
	g_ptr_array_unref (devices);
	g_object_unref (device_queue);
	g_object_unref (usb_ctx);
}

static void
ch_test_math_convert_func (void)
{
	ChPackedFloat pf;
	gdouble value = 0.0f;

	/* test packing */
	g_assert_cmpint (sizeof (ChPackedFloat), ==, 4);

	/* test converting to packed struct */
	value = 3.1415927f;
	ch_double_to_packed_float (value, &pf);
	pf.raw = ch_packed_float_get_value (&pf);
	g_assert_cmpint (pf.offset, ==, 3);
	g_assert_cmpint (pf.fraction, <, 0x249f);
	g_assert_cmpint (pf.fraction, >, 0x240f);

	/* test converting to packed struct */
	value = -3.1415927f;
	ch_double_to_packed_float (value, &pf);
	pf.raw = ch_packed_float_get_value (&pf);
	g_assert_cmpint (pf.offset, ==, -4);
	g_assert_cmpint (pf.fraction, <, (0x240f ^ 0xffff));
	g_assert_cmpint (pf.fraction, >, (0x249f ^ 0xffff));

	/* test converting positive to float */
	pf.offset = 3;
	pf.fraction = 0x243c;
	pf.raw = ch_packed_float_get_value (&pf);
	ch_packed_float_to_double (&pf, &value);
	g_assert_cmpfloat (value, >, 3.1415);
	g_assert_cmpfloat (value, <, 3.1416);

	/* test converting negative to float */
	pf.offset = -4;
	pf.fraction = 0x243b ^ 0xffff;
	ch_packed_float_to_double (&pf, &value);
	pf.raw = ch_packed_float_get_value (&pf);
	g_assert_cmpfloat (value, >, -3.1416);
	g_assert_cmpfloat (value, <, -3.1415);

	/* test converting zero */
	value = 0.0f;
	ch_double_to_packed_float (value, &pf);
	pf.raw = ch_packed_float_get_value (&pf);
	g_assert_cmpint (pf.offset, ==, 0);
	g_assert_cmpint (pf.fraction, ==, 0);
	ch_packed_float_set_value (&pf, pf.raw);
	ch_packed_float_to_double (&pf, &value);
	g_assert_cmpfloat (value, >, -0.001f);
	g_assert_cmpfloat (value, <, +0.001f);

	/* test converting positive */
	value = +1.4f;
	ch_double_to_packed_float (value, &pf);
	pf.raw = ch_packed_float_get_value (&pf);
	g_assert_cmpint (pf.offset, ==, 1);
	g_assert_cmpint (pf.fraction, <, 0x6668);
	g_assert_cmpint (pf.fraction, >, 0x6663);
	ch_packed_float_set_value (&pf, pf.raw);
	ch_packed_float_to_double (&pf, &value);
	g_assert_cmpfloat (value, <, 1.41);
	g_assert_cmpfloat (value, >, 1.39);

	/* test converting negative */
	value = -1.4f;
	ch_double_to_packed_float (value, &pf);
	pf.raw = ch_packed_float_get_value (&pf);
	g_assert_cmpint (pf.offset, ==, -2);
	g_assert_cmpint (pf.fraction, <, (0x6662 ^ 0xffff));
	g_assert_cmpint (pf.fraction, >, (0x6668 ^ 0xffff));
	ch_packed_float_set_value (&pf, pf.raw);
	ch_packed_float_to_double (&pf, &value);
	g_assert_cmpfloat (value, <, -1.39);
	g_assert_cmpfloat (value, >, -1.41);

	/* test converting negative max */
	value = -0x7fff;
	ch_double_to_packed_float (value, &pf);
	pf.raw = ch_packed_float_get_value (&pf);
	g_assert_cmpint (pf.offset, ==, -32767);
	g_assert_cmpint (pf.fraction, ==, 0);
	ch_packed_float_set_value (&pf, pf.raw);
	ch_packed_float_to_double (&pf, &value);
	g_assert_cmpfloat (value, >, -32768.0001);
	g_assert_cmpfloat (value, <, +32767.9999);
}

static void
ch_test_math_add_func (void)
{
	ChPackedFloat pf;
	ChPackedFloat pf_tmp;
	ChPackedFloat pf_result;
	gdouble value = 0.0f;
	guint8 rc;

	/* test addition */
	ch_double_to_packed_float (3.90f, &pf);
	ch_double_to_packed_float (1.40f, &pf_tmp);
	rc = ch_packed_float_add (&pf, &pf_tmp, &pf_result);
	g_assert_cmpint (rc, ==, CH_ERROR_NONE);
	ch_packed_float_to_double (&pf_result, &value);
	g_assert_cmpfloat (value, >, 5.299);
	g_assert_cmpfloat (value, <, 5.310);

	/* test addition with both negative */
	ch_double_to_packed_float (-3.90f, &pf);
	ch_double_to_packed_float (-1.40f, &pf_tmp);
	rc = ch_packed_float_add (&pf, &pf_tmp, &pf_result);
	g_assert_cmpint (rc, ==, CH_ERROR_NONE);
	ch_packed_float_to_double (&pf_result, &value);
	g_assert_cmpfloat (value, >, -5.301);
	g_assert_cmpfloat (value, <, -5.299);

	/* test addition with negative */
	ch_double_to_packed_float (3.20f, &pf);
	ch_double_to_packed_float (-1.50f, &pf_tmp);
	rc = ch_packed_float_add (&pf, &pf_tmp, &pf_result);
	g_assert_cmpint (rc, ==, CH_ERROR_NONE);
	ch_packed_float_to_double (&pf_result, &value);
	g_assert_cmpfloat (value, <, 1.701);
	g_assert_cmpfloat (value, >, 1.699);

	/* test addition with negative */
	ch_double_to_packed_float (3.20f, &pf);
	ch_double_to_packed_float (-10.50f, &pf_tmp);
	rc = ch_packed_float_add (&pf, &pf_tmp, &pf_result);
	g_assert_cmpint (rc, ==, CH_ERROR_NONE);
	ch_packed_float_to_double (&pf_result, &value);
	g_assert_cmpfloat (value, >, -7.301);
	g_assert_cmpfloat (value, <, -7.299);

	/* test addition overflow */
	ch_double_to_packed_float (0x7fff, &pf);
	ch_double_to_packed_float (0x7fff, &pf_tmp);
	rc = ch_packed_float_add (&pf, &pf_tmp, &pf_result);
//	g_assert_cmpint (rc, ==, CH_ERROR_OVERFLOW_ADDITION);
}


static void
ch_test_math_multiply_func (void)
{
	ChPackedFloat pf;
	ChPackedFloat pf_tmp;
	ChPackedFloat pf_result;
	gdouble value = 0.0f;
	gdouble value1;
	gdouble value2;
	guint8 rc;

	/* test safe multiplication */
	ch_double_to_packed_float (0.25f, &pf);
	ch_double_to_packed_float (0.50f, &pf_tmp);
	rc = ch_packed_float_multiply (&pf, &pf_tmp, &pf_result);
	g_assert_cmpint (rc, ==, CH_ERROR_NONE);
	ch_packed_float_to_double (&pf_result, &value);
	g_assert_cmpfloat (value, >, 0.1249);
	g_assert_cmpfloat (value, <, 0.1251);

	/* test multiplication we have to scale */
	ch_double_to_packed_float (3.90f, &pf);
	ch_double_to_packed_float (1.40f, &pf_tmp);
	rc = ch_packed_float_multiply (&pf, &pf_tmp, &pf_result);
	g_assert_cmpint (rc, ==, CH_ERROR_NONE);
	ch_packed_float_to_double (&pf_result, &value);
	g_assert_cmpfloat (value, >, 5.45);
	g_assert_cmpfloat (value, <, 5.47);

	/* test multiplication we have to scale a lot */
	ch_double_to_packed_float (3.90f, &pf);
	ch_double_to_packed_float (200.0f, &pf_tmp);
	rc = ch_packed_float_multiply (&pf, &pf_tmp, &pf_result);
	g_assert_cmpint (rc, ==, CH_ERROR_NONE);
	ch_packed_float_to_double (&pf_result, &value);
	g_assert_cmpfloat (value, >, 778.9);
	g_assert_cmpfloat (value, <, 780.1);

	/* test multiplication of negative */
	ch_double_to_packed_float (3.90f, &pf);
	ch_double_to_packed_float (-1.4f, &pf_tmp);
	rc = ch_packed_float_multiply (&pf, &pf_tmp, &pf_result);
	g_assert_cmpint (rc, ==, CH_ERROR_NONE);
	ch_packed_float_to_double (&pf_result, &value);
	g_assert_cmpfloat (value, <, -5.45);
	g_assert_cmpfloat (value, >, -5.47);

	/* test multiplication of double negative */
	ch_double_to_packed_float (-3.90f, &pf);
	ch_double_to_packed_float (-1.4f, &pf_tmp);
	rc = ch_packed_float_multiply (&pf, &pf_tmp, &pf_result);
	g_assert_cmpint (rc, ==, CH_ERROR_NONE);
	ch_packed_float_to_double (&pf_result, &value);
	g_assert_cmpfloat (value, >, 5.45);
	g_assert_cmpfloat (value, <, 5.47);

	/* test multiplication of very different numbers */
	ch_double_to_packed_float (0.072587f, &pf);
	ch_double_to_packed_float (80.0f, &pf_tmp);
	rc = ch_packed_float_multiply (&pf, &pf_tmp, &pf_result);
	g_assert_cmpint (rc, ==, CH_ERROR_NONE);
	ch_packed_float_to_double (&pf_result, &value);
	g_assert_cmpfloat (value, >, 5.79);
	g_assert_cmpfloat (value, <, 5.81);

	/* be evil */
	for (value1 = -127; value1 < +127; value1 += 0.5f) {
		for (value2 = -127; value2 < +127; value2 += 0.5f) {
			ch_double_to_packed_float (value1, &pf);
			ch_double_to_packed_float (value2, &pf_tmp);
			rc = ch_packed_float_multiply (&pf, &pf_tmp, &pf_result);
			g_assert_cmpint (rc, ==, CH_ERROR_NONE);
			ch_packed_float_to_double (&pf_result, &value);
			g_assert_cmpfloat (value, >, (value1 * value2) - 0.01);
			g_assert_cmpfloat (value, <, (value1 * value2) + 0.01);
		}
	}

	/* test multiplication overflow */
	ch_double_to_packed_float (0x4fff, &pf);
	ch_double_to_packed_float (0x4, &pf_tmp);
	rc = ch_packed_float_multiply (&pf, &pf_tmp, &pf_result);
	g_assert_cmpint (rc, ==, CH_ERROR_OVERFLOW_MULTIPLY);
}

static GUsbDevice *
ch_client_get_default (GError **error)
{
	gboolean ret;
	GUsbContext *usb_ctx;
	GUsbDevice *device = NULL;

	g_return_val_if_fail (error == NULL || *error == NULL, NULL);

	/* try to find the ColorHug device */
	usb_ctx = g_usb_context_new (NULL);
	if (usb_ctx == NULL) {
		g_set_error(error,
			    G_USB_DEVICE_ERROR,
			    G_USB_DEVICE_ERROR_NO_DEVICE,
			    "No device found; USB initialisation failed");
		return NULL;
	}
	device = g_usb_context_find_by_vid_pid (usb_ctx,
						CH_USB_VID,
						CH_USB_PID_FIRMWARE_ALS_SENSOR_HID,
						NULL);
	if (device == NULL) {
		device = g_usb_context_find_by_vid_pid (usb_ctx,
							CH_USB_VID,
							CH_USB_PID_FIRMWARE,
							error);
	}
	if (device == NULL)
		goto out;
	g_debug ("Found ColorHug device %s",
		 g_usb_device_get_platform_id (device));
	ret = ch_device_open (device, error);
	if (!ret)
		goto out;
out:
	g_object_unref (usb_ctx);
	return device;
}

static void
ch_test_state_func (void)
{
	ChColorSelect color_select = 0;
	ChDeviceQueue *device_queue;
	ChFreqScale multiplier = 0;
	gboolean ret;
	gdouble elapsed;
	g_autoptr(GError) error = NULL;
	GTimer *timer;
	guint16 integral_time = 0;
	guint8 leds = 0;
	guint i;
	GUsbDevice *device;

	/* load the device */
	device = ch_client_get_default (&error);
	if (device == NULL && g_error_matches (error,
					       G_USB_DEVICE_ERROR,
					       G_USB_DEVICE_ERROR_NO_DEVICE)) {
		g_debug ("no device, skipping tests");
		return;
	}
	g_assert_no_error (error);
	g_assert (device != NULL);

	/* verify LEDs */
	device_queue = ch_device_queue_new ();
	ch_device_queue_set_leds (device_queue,
				  device,
				  1,
				  0,
				  0x00,
				  0x00);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	ch_device_queue_get_leds (device_queue,
				  device,
				  &leds);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	g_assert_cmpint (leds, ==, 1);

	/* verify color select */
	if (ch_device_get_mode (device) == CH_DEVICE_MODE_FIRMWARE) {
		ch_device_queue_set_color_select (device_queue,
						  device,
						  CH_COLOR_SELECT_BLUE);
		ret = ch_device_queue_process (device_queue,
					       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
					       NULL,
					       &error);
		g_assert_no_error (error);
		g_assert (ret);
		ch_device_queue_get_color_select (device_queue,
						  device,
						  &color_select);
		ret = ch_device_queue_process (device_queue,
					       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
					       NULL,
					       &error);
		g_assert_no_error (error);
		g_assert (ret);
		g_assert_cmpint (color_select, ==, CH_COLOR_SELECT_BLUE);

		/* verify multiplier */
		ch_device_queue_set_multiplier (device_queue,
						device,
						CH_FREQ_SCALE_2);
		ret = ch_device_queue_process (device_queue,
					       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
					       NULL,
					       &error);
		g_assert_no_error (error);
		g_assert (ret);
		ch_device_queue_get_multiplier (device_queue,
						device,
						&multiplier);
		ret = ch_device_queue_process (device_queue,
					       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
					       NULL,
					       &error);
		g_assert_no_error (error);
		g_assert (ret);
		g_assert_cmpint (multiplier, ==, CH_FREQ_SCALE_2);
	}

	/* verify integral */
	ch_device_queue_set_integral_time (device_queue,
					   device,
					   100);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	ch_device_queue_get_integral_time (device_queue,
					   device,
					   &integral_time);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	g_assert_cmpint (integral_time, ==, 100);

	/* verify sram access time */
	if (ch_device_get_mode (device) == CH_DEVICE_MODE_FIRMWARE_PLUS) {
		guint8 data[3500*2];
		for (i = 0; i < sizeof(data); i++)
			data[i] = i;

		/* test writing */
		timer = g_timer_new ();
		ch_device_queue_write_sram (device_queue,
					    device,
					    0x0000,
					    data,
					    sizeof(data));
		ret = ch_device_queue_process (device_queue,
					       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
					       NULL,
					       &error);
		g_assert_no_error (error);
		g_assert (ret);
		elapsed = g_timer_elapsed (timer, NULL);
		g_debug ("%" G_GSIZE_FORMAT " writes in %.1fms",
			 sizeof(data) / 60, elapsed * 1000);
		g_assert_cmpfloat (elapsed, <, 0.75);

		/* test reading */
		g_timer_reset (timer);
		ch_device_queue_read_sram (device_queue,
					   device,
					   0x0000,
					   data,
					   sizeof(data));
		ret = ch_device_queue_process (device_queue,
					       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
					       NULL,
					       &error);
		g_assert_no_error (error);
		g_assert (ret);
		elapsed = g_timer_elapsed (timer, NULL);
		g_debug ("%" G_GSIZE_FORMAT " reads in %.1fms",
			 sizeof(data) / 60, elapsed * 1000);
		g_assert_cmpfloat (elapsed, <, 0.75);
		g_timer_destroy (timer);
	}

	/* close */
	ret = ch_device_close (device, &error);
	g_assert_no_error (error);
	g_assert (ret);

	g_object_unref (device_queue);
}

static void
ch_test_eeprom_func (void)
{
	gboolean ret;
	g_autoptr(GError) error = NULL;
	guint16 major = 0;
	guint16 micro = 0;
	guint16 minor = 0;
	guint8 types = 0;
	CdColorRGB value;
	gdouble post_scale = 0;
	gdouble post_scale_tmp = 0;
	gdouble pre_scale = 0;
	gdouble pre_scale_tmp = 0;
	guint32 serial_number = 0;
	CdMat3x3 calibration;
	CdMat3x3 calibration_tmp;
	gchar desc[24];
	GUsbDevice *device;
	ChDeviceQueue *device_queue;

	/* load the device */
	device = ch_client_get_default (&error);
	if (device != NULL && g_usb_device_get_pid (device) != CH_USB_PID_FIRMWARE) {
		ret = ch_device_close (device, &error);
		g_assert_no_error (error);
		g_assert (ret);
		g_debug ("not capable device, skipping tests");
		return;
	}
	if (device == NULL && g_error_matches (error,
					       G_USB_DEVICE_ERROR,
					       G_USB_DEVICE_ERROR_NO_DEVICE)) {
		g_debug ("no device, skipping tests");
		return;
	}
	g_assert_no_error (error);
	g_assert (device != NULL);

	/* only run the destructive tests on a device that is blank */
	device_queue = ch_device_queue_new ();
	ch_device_queue_get_serial_number (device_queue,
					   device,
					   &serial_number);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	if (serial_number != 0) {
		g_debug ("not resetting device as bad serial, skipping tests");
		return;
	}

	/* write eeprom with wrong code */
	ch_device_queue_write_eeprom (device_queue,
				      device,
				     "hello dave");
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_error (error, 1, 0);
	g_assert (!ret);
	g_clear_error (&error);

	/* verify serial number */
	ch_device_queue_set_serial_number (device_queue,
					   device,
					   12345678);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	ch_device_queue_get_serial_number (device_queue,
					   device,
					   &serial_number);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	g_assert_cmpint (serial_number, ==, 12345678);

	/* verify firmware */
	ch_device_queue_get_firmware_ver (device_queue,
					  device,
					  &major,
					  &minor,
					  &micro);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	g_assert_cmpint (major, ==, 1);
	g_assert_cmpint (minor, ==, 0);
	g_assert_cmpint (micro, >, 0);

	/* verify dark offsets */
	value.R = 0.12;
	value.G = 0.34;
	value.B = 0.56;
	ch_device_queue_set_dark_offsets (device_queue,
					  device,
					  &value);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	ch_device_queue_get_dark_offsets (device_queue,
					  device,
					  &value);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	g_assert_cmpint (value.R, ==, 0.12);
	g_assert_cmpint (value.G, ==, 0.34);
	g_assert_cmpint (value.B, ==, 0.56);

	/* verify calibration */
	calibration.m00 = 1.0f;
	calibration.m01 = 2.0f;
	calibration.m02 = 3.0f;
	calibration.m10 = 4.0f;
	calibration.m11 = 5.0f;
	calibration.m12 = 6.0f;
	calibration.m20 = 7.0f;
	calibration.m21 = 8.0f;
	calibration.m22 = 9.0f;
	ch_device_queue_set_calibration (device_queue,
					 device,
					 60,
					 &calibration,
					 CH_CALIBRATION_TYPE_CRT,
					 "test0");
	ch_device_queue_set_calibration (device_queue,
					 device,
					 61,
					 &calibration,
					 CH_CALIBRATION_TYPE_PROJECTOR,
					 "test1");
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	ch_device_queue_set_calibration (device_queue,
					 device,
					 60,
					 &calibration,
					 CH_CALIBRATION_TYPE_CRT,
					 "test0");
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);

	/* read back data */
	ch_device_queue_get_calibration (device_queue,
					 device,
					 60,
					 &calibration_tmp,
					 &types,
					 desc);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	g_assert (memcmp (&calibration_tmp,
			  &calibration,
			  sizeof (gfloat) * 9) == 0);
	g_assert_cmpint (types, ==, CH_CALIBRATION_TYPE_CRT);
	g_assert_cmpstr (desc, ==, "test0");
	ch_device_queue_get_calibration (device_queue,
					 device,
					 61,
					 &calibration_tmp,
					 &types,
					 desc);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	g_assert (memcmp (&calibration_tmp,
			  &calibration,
			  sizeof (gfloat) * 9) == 0);
	g_assert_cmpint (types, ==, CH_CALIBRATION_TYPE_PROJECTOR);
	g_assert_cmpstr (desc, ==, "test1");

	/* verify post scale */
	post_scale = 127.8f;
	ch_device_queue_set_post_scale (device_queue,
					device,
					post_scale);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);

	ch_device_queue_get_post_scale (device_queue,
					device,
					&post_scale_tmp);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	g_assert_cmpfloat (fabs (post_scale - post_scale_tmp), <, 0.0001);

	/* verify pre scale */
	pre_scale = 1.23f;
	ch_device_queue_set_pre_scale (device_queue,
				       device,
				       pre_scale);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);

	ch_device_queue_get_pre_scale (device_queue,
				       device,
				       &pre_scale_tmp);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	g_assert_cmpfloat (fabs (pre_scale - pre_scale_tmp), <, 0.0001);

#if 0
	/* write eeprom */
	ch_device_queue_write_eeprom (device_queue,
				      device,
				      CH_WRITE_EEPROM_MAGIC);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
#endif

	ret = ch_device_close (device, &error);
	g_assert_no_error (error);
	g_assert (ret);
	g_object_unref (device);
	g_object_unref (device_queue);
}

static void
ch_test_reading_func (void)
{
	gboolean ret;
	g_autoptr(GError) error = NULL;
	guint32 take_reading = 0;
	GUsbDevice *device;
	ChDeviceQueue *device_queue;

	/* load the device */
	device = ch_client_get_default (&error);
	if (device == NULL && g_error_matches (error,
					       G_USB_DEVICE_ERROR,
					       G_USB_DEVICE_ERROR_NO_DEVICE)) {
		g_debug ("no device, skipping tests");
		return;
	}
	g_assert_no_error (error);
	g_assert (device != NULL);

	/* set color select */
	device_queue = ch_device_queue_new ();
	if (ch_device_get_mode (device) == CH_DEVICE_MODE_FIRMWARE) {
		ch_device_queue_set_color_select (device_queue,
						  device,
						  CH_COLOR_SELECT_WHITE);
		ret = ch_device_queue_process (device_queue,
					       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
					       NULL,
					       &error);
		g_assert_no_error (error);
		g_assert (ret);

		/* set multiplier */
		ch_device_queue_set_multiplier (device_queue,
						device,
						CH_FREQ_SCALE_100);
		ret = ch_device_queue_process (device_queue,
					       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
					       NULL,
					       &error);
		g_assert_no_error (error);
		g_assert (ret);
	}

	/* set integral and take a reading from the hardware */
	ch_device_queue_set_integral_time (device_queue,
					   device,
					   0xffff);
	ch_device_queue_take_reading_raw (device_queue,
					  device,
					  &take_reading);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	g_assert_cmpint (take_reading, >, 0);

	/* close */
	ret = ch_device_close (device, &error);
	g_assert_no_error (error);
	g_assert (ret);

	g_object_unref (device);
	g_object_unref (device_queue);
}

static void
ch_test_reading_xyz_func (void)
{
	gboolean ret;
	CdMat3x3 calibration;
	CdColorXYZ reading1;
	CdColorXYZ reading2;
	gdouble scaling_factor_actual;
	g_autoptr(GError) error = NULL;
	guint16 calibration_map[6];
	guint16 post_scale;
	guint i;
	GUsbDevice *device;
	CdColorRGB value;
	ChDeviceQueue *device_queue;

	/* load the device */
	device = ch_client_get_default (&error);
	if (device != NULL && g_usb_device_get_pid (device) != CH_USB_PID_FIRMWARE) {
		ret = ch_device_close (device, &error);
		g_assert_no_error (error);
		g_assert (ret);
		g_debug ("not capable device, skipping tests");
		return;
	}
	if (device == NULL && g_error_matches (error,
					       G_USB_DEVICE_ERROR,
					       G_USB_DEVICE_ERROR_NO_DEVICE)) {
		g_debug ("no device, skipping tests");
		return;
	}
	g_assert_no_error (error);
	g_assert (device != NULL);

	/* set unity calibration */
	device_queue = ch_device_queue_new ();
	cd_mat33_set_identity (&calibration);
	ch_device_queue_set_calibration (device_queue,
					 device,
					 60,
					 &calibration,
					 CH_CALIBRATION_TYPE_ALL,
					 "test0");
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);

	/* set everything to use the unity values */
	for (i = 0; i < 6; i++)
		calibration_map[i] = 60;
	ch_device_queue_set_calibration_map (device_queue,
					     device,
					     calibration_map);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);

	/* set dark offsets and scale */
	value.R = 0.0f;
	value.G = 0.0f;
	value.B = 0.0f;
	ch_device_queue_set_dark_offsets (device_queue,
					  device,
					  &value);
	ch_device_queue_set_pre_scale (device_queue,
				       device,
				       5.0f);
	ch_device_queue_set_post_scale (device_queue,
					device,
					1.0f);

	/* take a reading from the hardware */
	ch_device_queue_take_readings_xyz (device_queue,
					   device,
					   0,
					   &reading1);
	ret = ch_device_queue_process (device_queue,
				       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
				       NULL,
				       &error);
	g_assert_no_error (error);
	g_assert (ret);
	g_assert_cmpfloat (reading1.X, >, 0.0f);
	g_assert_cmpfloat (reading1.Y, >, 0.0f);
	g_assert_cmpfloat (reading1.Z, >, 0.0f);

	/* set post scale much higher */
	for (post_scale = 1; post_scale < 2000; post_scale *= 2) {
		g_debug ("Setting post-scale %" G_GUINT16_FORMAT, post_scale);
		ch_device_queue_set_post_scale (device_queue,
						device,
						post_scale);
		ch_device_queue_take_readings_xyz (device_queue,
						   device,
						   0,
						   &reading2);
		ret = ch_device_queue_process (device_queue,
					       CH_DEVICE_QUEUE_PROCESS_FLAGS_NONE,
					       NULL,
					       &error);
		g_assert_no_error (error);
		g_assert (ret);

		/* X */
		scaling_factor_actual = reading2.X / reading1.X;
		g_debug ("scale %i: %f, scale 1: %f so effective %f",
			 post_scale, reading2.X, reading1.X,
			 scaling_factor_actual);
		g_assert_cmpfloat (scaling_factor_actual, >, 0.9);
		g_assert_cmpfloat (scaling_factor_actual, <, 1.1);
		reading1.X = reading2.X * 2;

		/* Y */
		scaling_factor_actual = reading2.Y / reading1.Y;
		g_debug ("scale %i: %f, scale 1: %f so effective %f",
			 post_scale, reading2.Y, reading1.Y,
			 scaling_factor_actual);
		g_assert_cmpfloat (scaling_factor_actual, >, 0.9);
		g_assert_cmpfloat (scaling_factor_actual, <, 1.1);
		reading1.Y = reading2.Y * 2;

		/* Z */
		scaling_factor_actual = reading2.Z / reading1.Z;
		g_debug ("scale %i: %f, scale 1: %f so effective %f",
			 post_scale, reading2.Z, reading1.Z,
			 scaling_factor_actual);
		g_assert_cmpfloat (scaling_factor_actual, >, 0.9);
		g_assert_cmpfloat (scaling_factor_actual, <, 1.1);
		reading1.Z = reading2.Z * 2;
	}

	/* close */
	ret = ch_device_close (device, &error);
	g_assert_no_error (error);
	g_assert (ret);

	g_object_unref (device);
	g_object_unref (device_queue);
}

/**
 * ch_test_incomplete_request_func:
 *
 * This tests what happens when we do request,request,read on the device
 * rather than just request,read. With new firmware versions we should
 * return a %CH_ERROR_INCOMPLETE_REQUEST error value and the original
 * command ID rather than just the device re-enumerating on the USB bus.
 */
static void
ch_test_incomplete_request_func (void)
{
	gboolean ret;
	g_autoptr(GError) error = NULL;
	guint8 buffer[CH_USB_HID_EP_SIZE];
	GUsbDevice *device = NULL;


	/* load the device */
	device = ch_client_get_default (&error);
	if (device != NULL && g_usb_device_get_pid (device) != CH_USB_PID_FIRMWARE) {
		ret = ch_device_close (device, &error);
		g_assert_no_error (error);
		g_assert (ret);
		g_debug ("not capable device, skipping tests");
		return;
	}
	if (device == NULL && g_error_matches (error,
					       G_USB_DEVICE_ERROR,
					       G_USB_DEVICE_ERROR_NO_DEVICE)) {
		g_debug ("no device, skipping tests");
		return;
	}
	g_assert_no_error (error);
	g_assert (device != NULL);

	/* sending first tx packet */
	memset (buffer, 0x00, CH_USB_HID_EP_SIZE);
	buffer[0] = CH_CMD_GET_FIRMWARE_VERSION;
	ret = g_usb_device_interrupt_transfer (device,
					       CH_USB_HID_EP_OUT,
					       buffer,
					       CH_USB_HID_EP_SIZE,
					       NULL,
					       CH_DEVICE_USB_TIMEOUT,
					       NULL,
					       &error);
	if (!ret) {
		g_warning ("Error: %s", error->message);
		goto out;
	}

	/* oops, the calling program crashed */
	g_usleep (G_USEC_PER_SEC);

	/* sending second tx packet */
	memset (buffer, 0x00, CH_USB_HID_EP_SIZE);
	buffer[0] = CH_CMD_GET_CALIBRATION;
	ret = g_usb_device_interrupt_transfer (device,
					       CH_USB_HID_EP_OUT,
					       buffer,
					       CH_USB_HID_EP_SIZE,
					       NULL,
					       CH_DEVICE_USB_TIMEOUT,
					       NULL,
					       &error);
	if (!ret) {
		g_warning ("Error: %s", error->message);
		goto out;
	}

	/* get rx packet */
	memset (buffer, 0x00, CH_USB_HID_EP_SIZE);
	ret = g_usb_device_interrupt_transfer (device,
					       CH_USB_HID_EP_IN,
					       buffer,
					       CH_USB_HID_EP_SIZE,
					       NULL,
					       CH_DEVICE_USB_TIMEOUT,
					       NULL,
					       &error);
	if (!ret) {
		g_warning ("Error: %s", error->message);
		goto out;
	}
	g_assert_cmpint (buffer[0], ==, CH_ERROR_INCOMPLETE_REQUEST);
	g_assert_cmpint (buffer[1], ==, CH_CMD_GET_FIRMWARE_VERSION);
out:
	if (device != NULL) {
		ret = ch_device_close (device, &error);
		g_assert_no_error (error);
		g_assert (ret);
		g_object_unref (device);
	}
}

static void
ch_test_firmware_func (void)
{
	ChDeviceMode device_mode;
	guint8 firmware_data[1024];
	guint i;

	/* set to something predictable */
	for (i = 0; i < sizeof(firmware_data); i++)
		firmware_data[i] = i % 255;

	/* test with no firmware ID */
	device_mode = ch_device_mode_from_firmware (firmware_data, 1024);
	g_assert_cmpint (device_mode, ==, CH_DEVICE_MODE_UNKNOWN);

	/* test with V1 signature */
	memcpy (firmware_data + 64, CH_FIRMWARE_ID_TOKEN1, 8);
	device_mode = ch_device_mode_from_firmware (firmware_data, 1024);
	g_assert_cmpint (device_mode, ==, CH_DEVICE_MODE_FIRMWARE);

	/* test with V2 signature */
	memcpy (firmware_data + 64, CH_FIRMWARE_ID_TOKEN2, 8);
	device_mode = ch_device_mode_from_firmware (firmware_data, 1024);
	g_assert_cmpint (device_mode, ==, CH_DEVICE_MODE_FIRMWARE2);
}

int
main (int argc, char **argv)
{
	g_test_init (&argc, &argv, NULL);

	/* only critical and error are fatal */
	g_log_set_fatal_mask (NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL);

	/* tests go here */
	g_test_add_func ("/ColorHug/hash", ch_test_hash_func);
	g_test_add_func ("/ColorHug/device-queue", ch_test_device_queue_func);
	g_test_add_func ("/ColorHug/math-convert", ch_test_math_convert_func);
	g_test_add_func ("/ColorHug/math-add", ch_test_math_add_func);
	g_test_add_func ("/ColorHug/math-multiply", ch_test_math_multiply_func);
	g_test_add_func ("/ColorHug/state", ch_test_state_func);
	g_test_add_func ("/ColorHug/eeprom", ch_test_eeprom_func);
	g_test_add_func ("/ColorHug/reading", ch_test_reading_func);
	g_test_add_func ("/ColorHug/reading-xyz", ch_test_reading_xyz_func);
	g_test_add_func ("/ColorHug/device-incomplete-request", ch_test_incomplete_request_func);
	g_test_add_func ("/ColorHug/firmware", ch_test_firmware_func);

	return g_test_run ();
}

