/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2012 Richard Hughes <richard@hughsie.com>
 *
 * Licensed under the GNU Lesser General Public License Version 2.1
 *
 * This library 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.1 of the License, or (at your option) any later version.
 *
 * This library 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
 */

/**
 * SECTION:cd-it8
 * @short_description: Read and write IT8 color sample exchange files
 *
 * This object represents .ti1 and .ti3 files which can contain raw
 * or normalized sample data.
 */

#include "config.h"

#include <glib.h>
#include <lcms2.h>
#include <stdlib.h>

#include "cd-it8.h"
#include "cd-color.h"

static void	cd_it8_class_init	(CdIt8Class	*klass);
static void	cd_it8_init		(CdIt8		*it8);
static void	cd_it8_finalize		(GObject	*object);

#define CD_IT8_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), CD_TYPE_IT8, CdIt8Private))

/**
 * CdIt8Private:
 *
 * Private #CdIt8 data
 **/
struct _CdIt8Private
{
	CdIt8Kind		 kind;
	CdMat3x3		 matrix;
	gboolean		 normalized;
	gboolean		 spectral;
	gchar			*instrument;
	gchar			*reference;
	gchar			*originator;
	gchar			*title;
	GPtrArray		*array_rgb;
	GPtrArray		*array_xyz;
	GPtrArray		*options;
};

enum {
	PROP_0,
	PROP_KIND,
	PROP_INSTRUMENT,
	PROP_REFERENCE,
	PROP_NORMALIZED,
	PROP_ORIGINATOR,
	PROP_TITLE,
	PROP_SPECTRAL,
	PROP_LAST
};

G_DEFINE_TYPE (CdIt8, cd_it8, G_TYPE_OBJECT)

/**
 * cd_it8_error_quark:
 *
 * Return value: An error quark.
 *
 * Since: 0.1.0
 **/
GQuark
cd_it8_error_quark (void)
{
	static GQuark quark = 0;
	if (!quark) {
		quark = g_quark_from_static_string ("cd_it8_error");
	}
	return quark;
}

/**
 * cd_it8_set_matrix:
 * @it8: a #CdIt8 instance.
 * @matrix: a #CdMat3x3.
 *
 * Set the calibration matrix in the it8 file.
 *
 * Since: 0.1.20
 **/
void
cd_it8_set_matrix (CdIt8 *it8, const CdMat3x3 *matrix)
{
	g_return_if_fail (CD_IS_IT8 (it8));
	cd_mat33_copy (matrix, &it8->priv->matrix);
}

/**
 * cd_it8_get_matrix:
 * @it8: a #CdIt8 instance.
 *
 * Gets the calibration matrix in the it8 file.
 *
 * Return value: a #CdMat3x3.
 *
 * Since: 0.1.20
 **/
const CdMat3x3 *
cd_it8_get_matrix (CdIt8 *it8)
{
	g_return_val_if_fail (CD_IS_IT8 (it8), NULL);
	return &it8->priv->matrix;
}

/**
 * cd_it8_set_kind:
 * @it8: a #CdIt8 instance.
 * @kind: a #CdIt8Kind, e.g %CD_IT8_KIND_TI3.
 *
 * Set the kind of IT8 file.
 *
 * Since: 0.1.20
 **/
void
cd_it8_set_kind (CdIt8 *it8, CdIt8Kind kind)
{
	g_return_if_fail (CD_IS_IT8 (it8));
	it8->priv->kind = kind;
}

/**
 * cd_it8_get_kind:
 * @it8: a #CdIt8 instance.
 *
 * Gets the kind of IT8 file.
 *
 * Return value: a #CdIt8Kind, e.g %CD_IT8_KIND_TI3.
 *
 * Since: 0.1.20
 **/
CdIt8Kind
cd_it8_get_kind (CdIt8 *it8)
{
	g_return_val_if_fail (CD_IS_IT8 (it8), 0);
	return it8->priv->kind;
}

/**
 * cd_it8_lcms_error_cb:
 **/
static void
cd_it8_lcms_error_cb (cmsContext ContextID,
		      cmsUInt32Number error_code,
		      const char *text)
{
	g_warning ("LCMS: %s", text);
}

/**
 * cd_it8_parse_luminance:
 **/
static gboolean
cd_it8_parse_luminance (const gchar *text, CdColorXYZ *xyz, GError **error)
{
	gboolean ret = TRUE;
	gchar **split;

	split = g_strsplit (text, " ", -1);
	if (g_strv_length (split) != 3) {
		ret = FALSE;
		g_set_error (error,
			     1, 0,
			     "LUMINANCE_XYZ_CDM2 format invalid: %s",
			     text);
		goto out;
	}
	xyz->X = atof (split[0]);
	xyz->Y = atof (split[1]);
	xyz->Z = atof (split[2]);
out:
	g_strfreev (split);
	return ret;
}

/**
 * cd_it8_get_originator:
 * @it8: a #CdIt8 instance.
 *
 * Gets the file orginator.
 *
 * Return value: The originator, or %NULL if unset
 *
 * Since: 0.1.20
 **/
const gchar *
cd_it8_get_originator (CdIt8 *it8)
{
	g_return_val_if_fail (CD_IS_IT8 (it8), NULL);
	return it8->priv->originator;
}

/**
 * cd_it8_get_title:
 * @it8: a #CdIt8 instance.
 *
 * Gets the file title.
 *
 * Return value: The title, or %NULL if unset
 *
 * Since: 0.1.20
 **/
const gchar *
cd_it8_get_title (CdIt8 *it8)
{
	g_return_val_if_fail (CD_IS_IT8 (it8), NULL);
	return it8->priv->title;
}

/**
 * cd_it8_get_instrument:
 * @it8: a #CdIt8 instance.
 *
 * Gets the instrument the file was created by.
 *
 * Return value: The instrument, or %NULL if unset
 *
 * Since: 0.1.20
 **/
const gchar *
cd_it8_get_instrument (CdIt8 *it8)
{
	g_return_val_if_fail (CD_IS_IT8 (it8), NULL);
	return it8->priv->instrument;
}

/**
 * cd_it8_get_reference:
 * @it8: a #CdIt8 instance.
 *
 * Gets the reference the file was created against.
 *
 * Return value: The reference, or %NULL if unset
 *
 * Since: 0.1.20
 **/
const gchar *
cd_it8_get_reference (CdIt8 *it8)
{
	g_return_val_if_fail (CD_IS_IT8 (it8), NULL);
	return it8->priv->reference;
}

/**
 * cd_it8_get_normalized:
 * @it8: a #CdIt8 instance.
 *
 * Gets if the data should be written normlaised to y=100.
 *
 * Return value: %TRUE if the data should be normalised.
 *
 * Since: 0.1.20
 **/
gboolean
cd_it8_get_normalized (CdIt8 *it8)
{
	g_return_val_if_fail (CD_IS_IT8 (it8), FALSE);
	return it8->priv->normalized;
}

/**
 * cd_it8_get_spectral:
 * @it8: a #CdIt8 instance.
 *
 * Gets if the data is spectral or XYZ.
 *
 * Return value: %TRUE if the data is in spectral bands.
 *
 * Since: 0.1.20
 **/
gboolean
cd_it8_get_spectral (CdIt8 *it8)
{
	g_return_val_if_fail (CD_IS_IT8 (it8), FALSE);
	return it8->priv->spectral;
}

/**
 * cd_it8_load_ti1:
 **/
static gboolean
cd_it8_load_ti1 (CdIt8 *it8, cmsHANDLE it8_lcms, GError **error)
{
	CdColorRGB *rgb;
	CdColorXYZ *xyz;
	const gchar *tmp;
	gboolean ret = TRUE;
	guint i;
	guint number_of_sets = 0;

	tmp = cmsIT8GetProperty (it8_lcms, "COLOR_REP");
	if (g_strcmp0 (tmp, "RGB") != 0) {
		ret = FALSE;
		g_set_error (error, 1, 0, "Invalid data format: %s", tmp);
		goto out;
	}

	/* copy out data entries */
	number_of_sets = cmsIT8GetPropertyDbl (it8_lcms, "NUMBER_OF_SETS");
	for (i = 0; i < number_of_sets; i++) {
		rgb = cd_color_rgb_new ();
		rgb->R = cmsIT8GetDataRowColDbl(it8_lcms, i, 1);
		rgb->G = cmsIT8GetDataRowColDbl(it8_lcms, i, 2);
		rgb->B = cmsIT8GetDataRowColDbl(it8_lcms, i, 3);
		g_ptr_array_add (it8->priv->array_rgb, rgb);
		xyz = cd_color_xyz_new ();
		cd_color_set_xyz (xyz, 0.0, 0.0, 0.0);
		g_ptr_array_add (it8->priv->array_xyz, xyz);
	}
out:
	return ret;
}

/**
 * cd_it8_load_ti3:
 **/
static gboolean
cd_it8_load_ti3 (CdIt8 *it8, cmsHANDLE it8_lcms, GError **error)
{
	CdColorRGB *rgb;
	CdColorXYZ luminance;
	CdColorXYZ *xyz;
	const gchar *tmp;
	gboolean ret = TRUE;
	gboolean scaled_to_y100 = FALSE;
	guint i;
	guint number_of_sets = 0;

	tmp = cmsIT8GetProperty (it8_lcms, "COLOR_REP");
	if (g_strcmp0 (tmp, "RGB_XYZ") != 0) {
		ret = FALSE;
		g_set_error (error, 1, 0, "Invalid data format: %s", tmp);
		goto out;
	}

	/* if normalized, then scale back up */
	tmp = cmsIT8GetProperty (it8_lcms, "NORMALIZED_TO_Y_100");
	if (g_strcmp0 (tmp, "YES") == 0) {
		scaled_to_y100 = TRUE;
		tmp = cmsIT8GetProperty (it8_lcms, "LUMINANCE_XYZ_CDM2");
		ret = cd_it8_parse_luminance (tmp, &luminance, error);
		if (!ret)
			goto out;
	}

	/* set spectral flag */
	tmp = cmsIT8GetProperty (it8_lcms, "INSTRUMENT_TYPE_SPECTRAL");
	cd_it8_set_spectral (it8, g_strcmp0 (tmp, "YES") == 0);

	/* set instrument */
	cd_it8_set_instrument (it8, cmsIT8GetProperty (it8_lcms, "TARGET_INSTRUMENT"));

	/* copy out data entries */
	number_of_sets = cmsIT8GetPropertyDbl (it8_lcms, "NUMBER_OF_SETS");
	for (i = 0; i < number_of_sets; i++) {
		rgb = cd_color_rgb_new ();
		rgb->R = cmsIT8GetDataRowColDbl(it8_lcms, i, 1);
		rgb->G = cmsIT8GetDataRowColDbl(it8_lcms, i, 2);
		rgb->B = cmsIT8GetDataRowColDbl(it8_lcms, i, 3);
		if (scaled_to_y100) {
			rgb->R /= 100.0f;
			rgb->G /= 100.0f;
			rgb->B /= 100.0f;
		}
		g_ptr_array_add (it8->priv->array_rgb, rgb);
		xyz = cd_color_xyz_new ();
		xyz->X = cmsIT8GetDataRowColDbl(it8_lcms, i, 4);
		xyz->Y = cmsIT8GetDataRowColDbl(it8_lcms, i, 5);
		xyz->Z = cmsIT8GetDataRowColDbl(it8_lcms, i, 6);
		if (scaled_to_y100) {
			xyz->X /= 100.0f;
			xyz->Y /= 100.0f;
			xyz->Z /= 100.0f;
			xyz->X *= luminance.X;
			xyz->Y *= luminance.Y;
			xyz->Z *= luminance.Z;
		}
		g_ptr_array_add (it8->priv->array_xyz, xyz);
	}
out:
	return ret;
}

/**
 * cd_it8_load_ccmx:
 **/
static gboolean
cd_it8_load_ccmx (CdIt8 *it8, cmsHANDLE it8_lcms, GError **error)
{
	gboolean ret = TRUE;
	const gchar *tmp;

	/* check color format */
	tmp = cmsIT8GetProperty (it8_lcms, "COLOR_REP");
	if (g_strcmp0 (tmp, "XYZ") != 0) {
		ret = FALSE;
		g_set_error (error, 1, 0, "Invalid CCMX data format: %s", tmp);
		goto out;
	}

	/* set instrument */
	cd_it8_set_instrument (it8, cmsIT8GetProperty (it8_lcms, "INSTRUMENT"));

	/* just load the matrix */
	it8->priv->matrix.m00 = cmsIT8GetDataRowColDbl(it8_lcms, 0, 0);
	it8->priv->matrix.m01 = cmsIT8GetDataRowColDbl(it8_lcms, 0, 1);
	it8->priv->matrix.m02 = cmsIT8GetDataRowColDbl(it8_lcms, 0, 2);
	it8->priv->matrix.m10 = cmsIT8GetDataRowColDbl(it8_lcms, 1, 0);
	it8->priv->matrix.m11 = cmsIT8GetDataRowColDbl(it8_lcms, 1, 1);
	it8->priv->matrix.m12 = cmsIT8GetDataRowColDbl(it8_lcms, 1, 2);
	it8->priv->matrix.m20 = cmsIT8GetDataRowColDbl(it8_lcms, 2, 0);
	it8->priv->matrix.m21 = cmsIT8GetDataRowColDbl(it8_lcms, 2, 1);
	it8->priv->matrix.m22 = cmsIT8GetDataRowColDbl(it8_lcms, 2, 2);
out:
	return ret;
}

/**
 * cd_it8_has_option:
 * @it8: a #CdIt8 instance.
 * @option: a option, e.g. "TYPE_CRT"
 *
 * Finds an option in the file.
 *
 * Return value: %TRUE if the option is set
 *
 * Since: 0.1.20
 **/
gboolean
cd_it8_has_option (CdIt8 *it8, const gchar *option)
{
	const gchar *tmp;
	guint i;

	g_return_val_if_fail (CD_IS_IT8 (it8), FALSE);
	g_return_val_if_fail (option != NULL, FALSE);

	for (i = 0; i < it8->priv->options->len; i++) {
		tmp = g_ptr_array_index (it8->priv->options, i);
		if (g_strcmp0 (tmp, option) == 0)
			return TRUE;
	}
	return FALSE;
}

/**
 * cd_it8_load_from_data:
 * @it8: a #CdIt8 instance.
 * @data: text data
 * @size: the size of text data
 * @error: a #GError, or %NULL
 *
 * Loads a it8 file from data.
 *
 * Return value: %TRUE if a valid it8 file was read.
 *
 * Since: 0.1.20
 **/
gboolean
cd_it8_load_from_data (CdIt8 *it8,
		       const gchar *data,
		       gsize size,
		       GError **error)
{
	cmsHANDLE it8_lcms = NULL;
	const gchar *tmp;
	gboolean ret = TRUE;
	gchar **props = NULL;
	guint i;

	g_return_val_if_fail (CD_IS_IT8 (it8), FALSE);
	g_return_val_if_fail (data != NULL, FALSE);
	g_return_val_if_fail (size > 0, FALSE);

	/* clear old data */
	g_ptr_array_set_size (it8->priv->array_rgb, 0);
	g_ptr_array_set_size (it8->priv->array_xyz, 0);
	g_ptr_array_set_size (it8->priv->options, 0);
	cd_mat33_clear (&it8->priv->matrix);

	/* load the it8 data */
	cmsSetLogErrorHandler (cd_it8_lcms_error_cb);
	it8_lcms = cmsIT8LoadFromMem (NULL, (void *) data, size);
	if (it8_lcms == NULL) {
		ret = FALSE;
		g_set_error_literal (error, 1, 0,
				     "Cannot open CCMX file");
		goto out;
	}

	/* add options */
	cmsIT8EnumProperties (it8_lcms, &props);
	for (i = 0; props[i] != NULL; i++) {
		if (g_str_has_prefix (props[i], "TYPE_"))
			cd_it8_add_option (it8, props[i]);
	}

	/* get sheet type */
	tmp = cmsIT8GetSheetType (it8_lcms);
	if (g_str_has_prefix (tmp, "CTI1")) {
		cd_it8_set_kind (it8, CD_IT8_KIND_TI1);
	} else if (g_str_has_prefix (tmp, "CTI3")) {
		cd_it8_set_kind (it8, CD_IT8_KIND_TI3);
	} else if (g_str_has_prefix (tmp, "CCMX")) {
		cd_it8_set_kind (it8, CD_IT8_KIND_CCMX);
	} else {
		ret = FALSE;
		g_set_error (error, 1, 0, "Invalid sheet type: %s", tmp);
		goto out;
	}

	/* get ti1 and ti3 specific data */
	if (it8->priv->kind == CD_IT8_KIND_TI1) {
		ret = cd_it8_load_ti1 (it8, it8_lcms, error);
		if (!ret)
			goto out;
	} else if (it8->priv->kind == CD_IT8_KIND_TI3) {
		ret = cd_it8_load_ti3 (it8, it8_lcms, error);
		if (!ret)
			goto out;
	} else if (it8->priv->kind == CD_IT8_KIND_CCMX) {
		ret = cd_it8_load_ccmx (it8, it8_lcms, error);
		if (!ret)
			goto out;
	}

	/* set common bits */
	cd_it8_set_title (it8, cmsIT8GetProperty (it8_lcms, "DISPLAY"));
	cd_it8_set_originator (it8, cmsIT8GetProperty (it8_lcms, "ORIGINATOR"));
	cd_it8_set_reference (it8, cmsIT8GetProperty (it8_lcms, "REFERENCE"));
out:
	if (it8_lcms != NULL)
		cmsIT8Free (it8_lcms);
	return ret;
}

/**
 * cd_it8_load_from_file:
 * @it8: a #CdIt8 instance.
 * @file: a #GFile
 * @error: a #GError, or %NULL
 *
 * Loads a it8 file from disk.
 *
 * Return value: %TRUE if a valid it8 file was read.
 *
 * Since: 0.1.20
 **/
gboolean
cd_it8_load_from_file (CdIt8 *it8, GFile *file, GError **error)
{
	gboolean ret;
	gchar *data = NULL;
	gsize size = 0;

	g_return_val_if_fail (CD_IS_IT8 (it8), FALSE);
	g_return_val_if_fail (G_IS_FILE (file), FALSE);

	/* load file */
	ret = g_file_load_contents (file, NULL, &data, &size, NULL, error);
	if (!ret)
		goto out;

	/* load data */
	ret = cd_it8_load_from_data (it8, data, size, error);
	if (!ret)
		goto out;
out:
	g_free (data);
	return ret;
}

/**
 * cd_it8_color_match:
 **/
static gboolean
cd_it8_color_match (CdColorRGB *rgb, gdouble r, gdouble g, gdouble b)
{
	if (ABS (rgb->R - r) > 0.01f)
		return FALSE;
	if (ABS (rgb->G - g) > 0.01f)
		return FALSE;
	if (ABS (rgb->B - b) > 0.01f)
		return FALSE;
	return TRUE;
}

/**
 * cd_it8_save_to_file_ti1_ti3:
 **/
static gboolean
cd_it8_save_to_file_ti1_ti3 (CdIt8 *it8, cmsHANDLE it8_lcms, GError **error)
{
	CdColorRGB *rgb_tmp;
	CdColorXYZ lumi_xyz;
	CdColorXYZ *xyz_tmp;
	gboolean ret = TRUE;
	gchar *lumi_str = NULL;
	gdouble normalize = 0.0f;
	guint i;
	guint luminance_samples = 0;

	/* calculate the absolute XYZ in candelas per meter squared */
	cd_color_clear_xyz (&lumi_xyz);
	if (it8->priv->normalized) {
		for (i = 0; i < it8->priv->array_rgb->len; i++) {
			rgb_tmp = g_ptr_array_index (it8->priv->array_rgb, i);

			/* is this 100% white? */
			ret = cd_it8_color_match (rgb_tmp, 1.0f, 1.0f, 1.0f);
			if (!ret)
				continue;
			luminance_samples++;
			xyz_tmp = g_ptr_array_index (it8->priv->array_xyz, i);
			lumi_xyz.X += xyz_tmp->X;
			lumi_xyz.Y += xyz_tmp->Y;
			lumi_xyz.Z += xyz_tmp->Z;
			if (xyz_tmp->Y > normalize)
				normalize = xyz_tmp->Y;
		}
		if (luminance_samples == 0) {
			ret = FALSE;
			g_set_error_literal (error, 1, 0,
					     "Failed to find any white samples");
			goto out;
		}
		lumi_xyz.X /= luminance_samples;
		lumi_xyz.Y /= luminance_samples;
		lumi_xyz.Z /= luminance_samples;

		/* scale all the readings to 100 */
		normalize = 100.0f / normalize;
	}
	lumi_str = g_strdup_printf ("%f %f %f", lumi_xyz.X, lumi_xyz.Y, lumi_xyz.Z);

	/* write data */
	if (it8->priv->kind == CD_IT8_KIND_TI1) {
		cmsIT8SetSheetType (it8_lcms, "CTI1   ");
		cmsIT8SetPropertyStr (it8_lcms, "DESCRIPTOR",
				      "Calibration Target chart information 1");
	} else if (it8->priv->kind == CD_IT8_KIND_TI3) {
		cmsIT8SetSheetType (it8_lcms, "CTI3   ");
		cmsIT8SetPropertyStr (it8_lcms, "DESCRIPTOR",
				      "Calibration Target chart information 3");
	}
	if (it8->priv->kind == CD_IT8_KIND_TI3) {
		cmsIT8SetPropertyStr (it8_lcms, "DEVICE_CLASS",
				      "DISPLAY");
	}
	cmsIT8SetPropertyStr (it8_lcms, "COLOR_REP", "RGB_XYZ");
	if (it8->priv->instrument != NULL) {
		cmsIT8SetPropertyStr (it8_lcms, "TARGET_INSTRUMENT",
				      it8->priv->instrument);
	}
	cmsIT8SetPropertyStr (it8_lcms, "INSTRUMENT_TYPE_SPECTRAL",
			      it8->priv->spectral ? "YES" : "NO");
	if (it8->priv->normalized) {
		cmsIT8SetPropertyStr (it8_lcms, "NORMALIZED_TO_Y_100", "YES");
		cmsIT8SetPropertyStr (it8_lcms, "LUMINANCE_XYZ_CDM2", lumi_str);
	} else {
		cmsIT8SetPropertyStr (it8_lcms, "NORMALIZED_TO_Y_100", "NO");
	}
	cmsIT8SetPropertyDbl (it8_lcms, "NUMBER_OF_FIELDS", 7);
	cmsIT8SetPropertyDbl (it8_lcms, "NUMBER_OF_SETS", it8->priv->array_rgb->len);
	cmsIT8SetDataFormat (it8_lcms, 0, "SAMPLE_ID");
	cmsIT8SetDataFormat (it8_lcms, 1, "RGB_R");
	cmsIT8SetDataFormat (it8_lcms, 2, "RGB_G");
	cmsIT8SetDataFormat (it8_lcms, 3, "RGB_B");
	cmsIT8SetDataFormat (it8_lcms, 4, "XYZ_X");
	cmsIT8SetDataFormat (it8_lcms, 5, "XYZ_Y");
	cmsIT8SetDataFormat (it8_lcms, 6, "XYZ_Z");

	/* write to the it8 file */
	for (i = 0; i < it8->priv->array_rgb->len; i++) {
		rgb_tmp = g_ptr_array_index (it8->priv->array_rgb, i);
		xyz_tmp = g_ptr_array_index (it8->priv->array_xyz, i);

		cmsIT8SetDataRowColDbl(it8_lcms, i, 0, i + 1);
		cmsIT8SetDataRowColDbl(it8_lcms, i, 1, rgb_tmp->R);
		cmsIT8SetDataRowColDbl(it8_lcms, i, 2, rgb_tmp->G);
		cmsIT8SetDataRowColDbl(it8_lcms, i, 3, rgb_tmp->B);
		if (it8->priv->normalized) {
			cmsIT8SetDataRowColDbl(it8_lcms, i, 4, xyz_tmp->X * normalize);
			cmsIT8SetDataRowColDbl(it8_lcms, i, 5, xyz_tmp->Y * normalize);
			cmsIT8SetDataRowColDbl(it8_lcms, i, 6, xyz_tmp->Z * normalize);
		} else {
			cmsIT8SetDataRowColDbl(it8_lcms, i, 4, xyz_tmp->X);
			cmsIT8SetDataRowColDbl(it8_lcms, i, 5, xyz_tmp->Y);
			cmsIT8SetDataRowColDbl(it8_lcms, i, 6, xyz_tmp->Z);
		}
	}
out:
	return ret;
}

/**
 * cd_it8_save_to_file_ccmx:
 **/
static gboolean
cd_it8_save_to_file_ccmx (CdIt8 *it8, cmsHANDLE it8_lcms, GError **error)
{
	gboolean ret = TRUE;

	cmsIT8SetSheetType (it8_lcms, "CCMX   ");
	cmsIT8SetPropertyStr (it8_lcms, "DESCRIPTOR",
			      "Device Correction Matrix");

	cmsIT8SetPropertyStr (it8_lcms, "COLOR_REP", "XYZ");
	cmsIT8SetPropertyDbl (it8_lcms, "NUMBER_OF_FIELDS", 3);
	cmsIT8SetPropertyDbl (it8_lcms, "NUMBER_OF_SETS", 3);
	cmsIT8SetDataFormat (it8_lcms, 0, "XYZ_X");
	cmsIT8SetDataFormat (it8_lcms, 1, "XYZ_Y");
	cmsIT8SetDataFormat (it8_lcms, 2, "XYZ_Z");

	/* save instrument */
	if (it8->priv->instrument != NULL) {
		cmsIT8SetPropertyStr (it8_lcms, "INSTRUMENT",
				      it8->priv->instrument);
	}

	/* just load the matrix */
	cmsIT8SetDataRowColDbl (it8_lcms, 0, 0, it8->priv->matrix.m00);
	cmsIT8SetDataRowColDbl (it8_lcms, 0, 1, it8->priv->matrix.m01);
	cmsIT8SetDataRowColDbl (it8_lcms, 0, 2, it8->priv->matrix.m02);
	cmsIT8SetDataRowColDbl (it8_lcms, 1, 0, it8->priv->matrix.m10);
	cmsIT8SetDataRowColDbl (it8_lcms, 1, 1, it8->priv->matrix.m11);
	cmsIT8SetDataRowColDbl (it8_lcms, 1, 2, it8->priv->matrix.m12);
	cmsIT8SetDataRowColDbl (it8_lcms, 2, 0, it8->priv->matrix.m20);
	cmsIT8SetDataRowColDbl (it8_lcms, 2, 1, it8->priv->matrix.m21);
	cmsIT8SetDataRowColDbl (it8_lcms, 2, 2, it8->priv->matrix.m22);

	return ret;
}

/**
 * cd_it8_save_to_file:
 * @it8: a #CdIt8 instance.
 * @file: a #GFile
 * @error: a #GError, or %NULL
 *
 * Saves a it8 file to disk
 *
 * Return value: %TRUE if it8 file was saved.
 *
 * Since: 0.1.20
 **/
gboolean
cd_it8_save_to_file (CdIt8 *it8, GFile *file, GError **error)
{
	cmsHANDLE it8_lcms = NULL;
	const gchar *tmp;
	gboolean ret;
	gchar *data = NULL;
	gsize size = 0;
	guint i;

	g_return_val_if_fail (CD_IS_IT8 (it8), FALSE);
	g_return_val_if_fail (G_IS_FILE (file), FALSE);

	/* set common data */
	it8_lcms = cmsIT8Alloc (NULL);
	if (it8->priv->title != NULL) {
		cmsIT8SetPropertyStr (it8_lcms, "DISPLAY",
				      it8->priv->title);
	}
	if (it8->priv->originator != NULL) {
		cmsIT8SetPropertyStr (it8_lcms, "ORIGINATOR",
				      it8->priv->originator);
	}
	if (it8->priv->reference != NULL) {
		cmsIT8SetPropertyStr (it8_lcms, "REFERENCE",
				      it8->priv->reference);
	}

	/* set ti1 and ti3 specific data */
	if (it8->priv->kind == CD_IT8_KIND_TI1 ||
	    it8->priv->kind == CD_IT8_KIND_TI3) {
		ret = cd_it8_save_to_file_ti1_ti3 (it8, it8_lcms, error);
		if (!ret)
			goto out;
	} else if (it8->priv->kind == CD_IT8_KIND_CCMX) {
		ret = cd_it8_save_to_file_ccmx (it8, it8_lcms, error);
		if (!ret)
			goto out;
	}

	/* save any options */
	for (i = 0; i < it8->priv->options->len; i++) {
		tmp = g_ptr_array_index (it8->priv->options, i);
		cmsIT8SetPropertyStr (it8_lcms, tmp, "TRUE");
	}

	/* write the file */
	ret = cmsIT8SaveToMem (it8_lcms, NULL, (cmsUInt32Number *) &size);
	g_assert (ret);
	data = g_malloc (size);
	ret = cmsIT8SaveToMem (it8_lcms, data, (cmsUInt32Number *) &size);
	g_assert (ret);

	/* save file (without the last '\0' byte) */
	ret = g_file_replace_contents (file, data, size - 1, NULL,
				       FALSE, G_FILE_CREATE_NONE,
				       NULL, NULL, error);
	if (!ret)
		goto out;
out:
	if (it8_lcms != NULL)
		cmsIT8Free (it8_lcms);
	g_free (data);
	return ret;
}

/**
 * cd_it8_add_option:
 * @it8: a #CdIt8 instance.
 * @option: A IT8 option, e.g. "TYPE_LCD"
 *
 * Sets any extra options that have to be set in the CCMX file
 *
 * Since: 0.1.20
 **/
void
cd_it8_add_option (CdIt8 *it8, const gchar *option)
{
	g_return_if_fail (CD_IS_IT8 (it8));
	g_ptr_array_add (it8->priv->options, g_strdup (option));
}

/**
 * cd_it8_set_normalized:
 * @it8: a #CdIt8 instance.
 * @normalized: If the data is normalized
 *
 * Sets if normalized data should be written to the .it8 file.
 *
 * Since: 0.1.20
 **/
void
cd_it8_set_normalized (CdIt8 *it8, gboolean normalized)
{
	g_return_if_fail (CD_IS_IT8 (it8));
	it8->priv->normalized = normalized;
}

/**
 * cd_it8_set_spectral:
 * @it8: a #CdIt8 instance.
 * @spectral: If the data is spectral
 *
 * Sets if spectral data should be written to the .it8 file.
 *
 * Since: 0.1.20
 **/
void
cd_it8_set_spectral (CdIt8 *it8, gboolean spectral)
{
	g_return_if_fail (CD_IS_IT8 (it8));
	it8->priv->spectral = spectral;
}

/**
 * cd_it8_set_originator:
 * @it8: a #CdIt8 instance.
 * @originator: the program name, e.g. "gcm-calibrate"
 *
 * Sets the program name that created the .it8 file
 *
 * Since: 0.1.20
 **/
void
cd_it8_set_originator (CdIt8 *it8, const gchar *originator)
{
	g_return_if_fail (CD_IS_IT8 (it8));

	g_free (it8->priv->originator);
	it8->priv->originator = g_strdup (originator);
}

/**
 * cd_it8_set_title:
 * @it8: a #CdIt8 instance.
 * @title: the title name, e.g. "Factory calibration"
 *
 * Sets the display name for the file.
 *
 * Since: 0.1.20
 **/
void
cd_it8_set_title (CdIt8 *it8, const gchar *title)
{
	g_return_if_fail (CD_IS_IT8 (it8));

	g_free (it8->priv->title);
	it8->priv->title = g_strdup (title);
}

/**
 * cd_it8_set_instrument:
 * @it8: a #CdIt8 instance.
 * @instrument: the instruemnt name, e.g. "huey"
 *
 * Sets the measuring instrument that created the .it8 file
 *
 * Since: 0.1.20
 **/
void
cd_it8_set_instrument (CdIt8 *it8, const gchar *instrument)
{
	g_return_if_fail (CD_IS_IT8 (it8));

	g_free (it8->priv->instrument);
	it8->priv->instrument = g_strdup (instrument);
}

/**
 * cd_it8_set_reference:
 * @it8: a #CdIt8 instance.
 * @reference: the instruemnt name, e.g. "colormunki"
 *
 * Sets the reference that as used to create the .it8 reference
 *
 * Since: 0.1.20
 **/
void
cd_it8_set_reference (CdIt8 *it8, const gchar *reference)
{
	g_return_if_fail (CD_IS_IT8 (it8));

	g_free (it8->priv->reference);
	it8->priv->reference = g_strdup (reference);
}

/**
 * cd_it8_add_data:
 * @it8: a #CdIt8 instance.
 * @rgb: a #CdColorRGB, or %NULL
 * @xyz: a #CdColorXYZ, or %NULL
 *
 * Adds a reading to this object. If either of @rgb or @xyz is NULL then
 * a black reading (0.0, 0.0, 0.0) is added instead.
 *
 * Since: 0.1.20
 **/
void
cd_it8_add_data (CdIt8 *it8, const CdColorRGB *rgb, const CdColorXYZ *xyz)
{
	CdColorRGB *rgb_tmp;
	CdColorXYZ *xyz_tmp;

	g_return_if_fail (CD_IS_IT8 (it8));

	/* add RGB */
	if (rgb != NULL) {
		rgb_tmp = cd_color_rgb_dup (rgb);
	} else {
		rgb_tmp = cd_color_rgb_new ();
		cd_color_set_rgb (rgb_tmp, 0.0f, 0.0f, 0.0f);
	}
	g_ptr_array_add (it8->priv->array_rgb, rgb_tmp);

	/* add XYZ */
	if (rgb != NULL) {
		xyz_tmp = cd_color_xyz_dup (xyz);
	} else {
		xyz_tmp = cd_color_xyz_new ();
		cd_color_set_xyz (xyz_tmp, 0.0f, 0.0f, 0.0f);
	}
	g_ptr_array_add (it8->priv->array_xyz, xyz_tmp);
}

/**
 * cd_it8_get_data_size:
 * @it8: a #CdIt8 instance.
 *
 * Gets the data size.
 *
 * Return value: The number of RGB-XYZ readings in this object.
 *
 * Since: 0.1.20
 **/
guint
cd_it8_get_data_size (CdIt8 *it8)
{
	g_return_val_if_fail (CD_IS_IT8 (it8), G_MAXUINT);
	return it8->priv->array_xyz->len;
}

/**
 * cd_it8_get_data_item:
 * @it8: a #CdIt8 instance.
 * @idx: the item index
 * @rgb: the returned RGB value
 * @xyz: the returned XYZ value
 *
 * Gets a specific bit of data from this object.
 * The returned data are absolute readings and are not normalised.
 *
 * Return value: %TRUE if the index existed.
 *
 * Since: 0.1.20
 **/
gboolean
cd_it8_get_data_item (CdIt8 *it8, guint idx, CdColorRGB *rgb, CdColorXYZ *xyz)
{
	const CdColorRGB *rgb_tmp;
	const CdColorXYZ *xyz_tmp;

	g_return_val_if_fail (CD_IS_IT8 (it8), FALSE);

	if (idx > it8->priv->array_xyz->len)
		return FALSE;
	if (rgb != NULL) {
		rgb_tmp = g_ptr_array_index (it8->priv->array_rgb, idx);
		cd_color_copy_rgb (rgb_tmp, rgb);
	}
	if (xyz != NULL) {
		xyz_tmp = g_ptr_array_index (it8->priv->array_xyz, idx);
		cd_color_copy_xyz (xyz_tmp, xyz);
	}
	return TRUE;
}

/**********************************************************************/

/*
 * cd_it8_get_property:
 */
static void
cd_it8_get_property (GObject *object,
		     guint prop_id,
		     GValue *value,
		     GParamSpec *pspec)
{
	CdIt8 *it8 = CD_IT8 (object);

	switch (prop_id) {
	case PROP_KIND:
		g_value_set_uint (value, it8->priv->kind);
		break;
	case PROP_NORMALIZED:
		g_value_set_boolean (value, it8->priv->normalized);
		break;
	case PROP_ORIGINATOR:
		g_value_set_string (value, it8->priv->originator);
		break;
	case PROP_TITLE:
		g_value_set_string (value, it8->priv->title);
		break;
	case PROP_INSTRUMENT:
		g_value_set_string (value, it8->priv->instrument);
		break;
	case PROP_REFERENCE:
		g_value_set_string (value, it8->priv->reference);
		break;
	case PROP_SPECTRAL:
		g_value_set_boolean (value, it8->priv->spectral);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}


/**
 * cd_it8_set_property:
 **/
static void
cd_it8_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
	CdIt8 *it8 = CD_IT8 (object);

	switch (prop_id) {
	case PROP_KIND:
		it8->priv->kind = g_value_get_uint (value);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

/*
 * cd_it8_class_init:
 */
static void
cd_it8_class_init (CdIt8Class *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->get_property = cd_it8_get_property;
	object_class->set_property = cd_it8_set_property;
	object_class->finalize = cd_it8_finalize;

	/**
	 * CdIt8:kind:
	 *
	 * The kind of IT8 file.
	 *
	 * Since: 0.1.20
	 **/
	g_object_class_install_property (object_class,
					 PROP_KIND,
					 g_param_spec_uint ("kind",
							    NULL, NULL,
							    0, G_MAXUINT, 0,
							    G_PARAM_READWRITE));

	/**
	 * CdIt8:normalized:
	 *
	 * If the results file is normalized.
	 *
	 * Since: 0.1.20
	 **/
	g_object_class_install_property (object_class,
					 PROP_NORMALIZED,
					 g_param_spec_boolean ("normalized",
							       NULL, NULL,
							       FALSE,
							       G_PARAM_READABLE));

	/**
	 * CdIt8:originator:
	 *
	 * The framework that created the results, e.g. "cd-self-test"
	 *
	 * Since: 0.1.20
	 **/
	g_object_class_install_property (object_class,
					 PROP_ORIGINATOR,
					 g_param_spec_string ("originator",
							      NULL, NULL,
							      NULL,
							      G_PARAM_READABLE));

	/**
	 * CdIt8:title:
	 *
	 * The file title, e.g. "Factor calibration".
	 *
	 * Since: 0.1.20
	 **/
	g_object_class_install_property (object_class,
					 PROP_TITLE,
					 g_param_spec_string ("title",
							      NULL, NULL,
							      NULL,
							      G_PARAM_READABLE));

	/**
	 * CdIt8:instrument:
	 *
	 * The instrument that created the results, e.g. "huey"
	 *
	 * Since: 0.1.20
	 **/
	g_object_class_install_property (object_class,
					 PROP_INSTRUMENT,
					 g_param_spec_string ("instrument",
							      NULL, NULL,
							      NULL,
							      G_PARAM_READABLE));

	/**
	 * CdIt8:reference:
	 *
	 * The reference that created the results, e.g. "colormunki"
	 *
	 * Since: 0.1.20
	 **/
	g_object_class_install_property (object_class,
					 PROP_REFERENCE,
					 g_param_spec_string ("reference",
							      NULL, NULL,
							      NULL,
							      G_PARAM_READABLE));

	/**
	 * CdIt8:spectral:
	 *
	 * If the results file is spectral.
	 *
	 * Since: 0.1.20
	 **/
	g_object_class_install_property (object_class,
					 PROP_SPECTRAL,
					 g_param_spec_boolean ("spectral",
							       NULL, NULL,
							       FALSE,
							       G_PARAM_READABLE));

	g_type_class_add_private (klass, sizeof (CdIt8Private));
}

/*
 * cd_it8_init:
 */
static void
cd_it8_init (CdIt8 *it8)
{
	it8->priv = CD_IT8_GET_PRIVATE (it8);

	it8->priv->array_rgb = g_ptr_array_new_with_free_func (cd_color_rgb_free);
	it8->priv->array_xyz = g_ptr_array_new_with_free_func (cd_color_xyz_free);
	it8->priv->options = g_ptr_array_new_with_free_func (g_free);

	/* ensure the remote errors are registered */
	cd_it8_error_quark ();
}

/*
 * cd_it8_finalize:
 */
static void
cd_it8_finalize (GObject *object)
{
	CdIt8 *it8 = CD_IT8 (object);

	g_return_if_fail (CD_IS_IT8 (object));

	g_ptr_array_unref (it8->priv->array_rgb);
	g_ptr_array_unref (it8->priv->array_xyz);
	g_ptr_array_unref (it8->priv->options);
	g_free (it8->priv->originator);
	g_free (it8->priv->title);
	g_free (it8->priv->instrument);
	g_free (it8->priv->reference);

	G_OBJECT_CLASS (cd_it8_parent_class)->finalize (object);
}

/**
 * cd_it8_new:
 *
 * Creates a new #CdIt8 object.
 *
 * Return value: a new CdIt8 object.
 *
 * Since: 0.1.20
 **/
CdIt8 *
cd_it8_new (void)
{
	CdIt8 *it8;
	it8 = g_object_new (CD_TYPE_IT8, NULL);
	return CD_IT8 (it8);
}

/**
 * cd_it8_new_with_kind:
 * @kind: a #CdIt8Kind, e.g %CD_IT8_KIND_TI3.
 *
 * Creates a new #CdIt8 object.
 *
 * Return value: a new CdIt8 object.
 *
 * Since: 0.1.20
 **/
CdIt8 *
cd_it8_new_with_kind (CdIt8Kind kind)
{
	CdIt8 *it8;
	it8 = g_object_new (CD_TYPE_IT8,
			    "kind", kind,
			    NULL);
	return CD_IT8 (it8);
}

