/* Eye Of MATE -- JPEG Metadata Reader
 *
 * Copyright (C) 2008 The Free Software Foundation
 *
 * Author: Felix Riemann <friemann@svn.gnome.org>
 *
 * Based on the original EomMetadataReader code.
 *
 * 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 St, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>

#include "eom-metadata-reader.h"
#include "eom-metadata-reader-jpg.h"
#include "eom-debug.h"

typedef enum {
	EMR_READ = 0,
	EMR_READ_SIZE_HIGH_BYTE,
	EMR_READ_SIZE_LOW_BYTE,
	EMR_READ_MARKER,
	EMR_SKIP_BYTES,
	EMR_READ_APP1,
	EMR_READ_EXIF,
	EMR_READ_XMP,
	EMR_READ_ICC,
	EMR_READ_IPTC,
	EMR_FINISHED
} EomMetadataReaderState;

typedef enum {
	EJA_EXIF = 0,
	EJA_XMP,
	EJA_OTHER
} EomJpegApp1Type;


#define EOM_JPEG_MARKER_START   0xFF
#define EOM_JPEG_MARKER_SOI     0xD8
#define EOM_JPEG_MARKER_APP1	0xE1
#define EOM_JPEG_MARKER_APP2	0xE2
#define EOM_JPEG_MARKER_APP14	0xED

#define IS_FINISHED(priv) (priv->state == EMR_READ  && \
                           priv->exif_chunk != NULL && \
                           priv->icc_chunk  != NULL && \
                           priv->iptc_chunk != NULL && \
                           priv->xmp_chunk  != NULL)

struct _EomMetadataReaderJpgPrivate {
	EomMetadataReaderState  state;

	/* data fields */
	guint    exif_len;
	gpointer exif_chunk;

	gpointer iptc_chunk;
	guint	 iptc_len;

	guint icc_len;
	gpointer icc_chunk;

	gpointer xmp_chunk;
	guint xmp_len;

	/* management fields */
	int      size;
	int      last_marker;
	int      bytes_read;
};

#define EOM_METADATA_READER_JPG_GET_PRIVATE(object) \
	(G_TYPE_INSTANCE_GET_PRIVATE ((object), EOM_TYPE_METADATA_READER_JPG, EomMetadataReaderJpgPrivate))

static void
eom_metadata_reader_jpg_init_emr_iface (gpointer g_iface, gpointer iface_data);


G_DEFINE_TYPE_WITH_CODE (EomMetadataReaderJpg, eom_metadata_reader_jpg,
			 G_TYPE_OBJECT,
			 G_IMPLEMENT_INTERFACE (EOM_TYPE_METADATA_READER,
			 		eom_metadata_reader_jpg_init_emr_iface))


static void
eom_metadata_reader_jpg_dispose (GObject *object)
{
	EomMetadataReaderJpg *emr = EOM_METADATA_READER_JPG (object);

	if (emr->priv->exif_chunk != NULL) {
		g_free (emr->priv->exif_chunk);
		emr->priv->exif_chunk = NULL;
	}

	if (emr->priv->iptc_chunk != NULL) {
		g_free (emr->priv->iptc_chunk);
		emr->priv->iptc_chunk = NULL;
	}

	if (emr->priv->xmp_chunk != NULL) {
		g_free (emr->priv->xmp_chunk);
		emr->priv->xmp_chunk = NULL;
	}

	if (emr->priv->icc_chunk != NULL) {
		g_free (emr->priv->icc_chunk);
		emr->priv->icc_chunk = NULL;
	}

	G_OBJECT_CLASS (eom_metadata_reader_jpg_parent_class)->dispose (object);
}

static void
eom_metadata_reader_jpg_init (EomMetadataReaderJpg *obj)
{
	EomMetadataReaderJpgPrivate *priv;

	priv = obj->priv =  EOM_METADATA_READER_JPG_GET_PRIVATE (obj);
	priv->exif_chunk = NULL;
	priv->exif_len = 0;
	priv->iptc_chunk = NULL;
	priv->iptc_len = 0;
	priv->icc_chunk = NULL;
	priv->icc_len = 0;
}

static void
eom_metadata_reader_jpg_class_init (EomMetadataReaderJpgClass *klass)
{
	GObjectClass *object_class = (GObjectClass*) klass;

	object_class->dispose = eom_metadata_reader_jpg_dispose;

	g_type_class_add_private (klass, sizeof (EomMetadataReaderJpgPrivate));
}

static gboolean
eom_metadata_reader_jpg_finished (EomMetadataReaderJpg *emr)
{
	g_return_val_if_fail (EOM_IS_METADATA_READER_JPG (emr), TRUE);

	return (emr->priv->state == EMR_FINISHED);
}


static EomJpegApp1Type
eom_metadata_identify_app1 (gchar *buf, guint len)
{
 	if (len < 5) {
 		return EJA_OTHER;
 	}

 	if (len < 29) {
 		return (strncmp ("Exif", buf, 5) == 0 ? EJA_EXIF : EJA_OTHER);
 	}

 	if (strncmp ("Exif", buf, 5) == 0) {
 		return EJA_EXIF;
 	} else if (strncmp ("http://ns.adobe.com/xap/1.0/", buf, 29) == 0) {
 		return EJA_XMP;
 	}

 	return EJA_OTHER;
}

static void
eom_metadata_reader_get_next_block (EomMetadataReaderJpgPrivate* priv,
				    guchar *chunk,
				    int* i,
				    const guchar *buf,
				    int len,
				    EomMetadataReaderState state)
{
	if (*i + priv->size < len) {
		/* read data in one block */
		memcpy ((guchar*) (chunk) + priv->bytes_read, &buf[*i], priv->size);
		priv->state = EMR_READ;
		*i = *i + priv->size - 1; /* the for-loop consumes the other byte */
	} else {
		int chunk_len = len - *i;
		memcpy ((guchar*) (chunk) + priv->bytes_read, &buf[*i], chunk_len);
		priv->bytes_read += chunk_len; /* bytes already read */
		priv->size = (*i + priv->size) - len; /* remaining data to read */
		*i = len - 1;
		priv->state = state;
	}
}

static void
eom_metadata_reader_jpg_consume (EomMetadataReaderJpg *emr, const guchar *buf, guint len)
{
	EomMetadataReaderJpgPrivate *priv;
 	EomJpegApp1Type app1_type;
	int i;
	EomMetadataReaderState next_state = EMR_READ;
	guchar *chunk = NULL;

	g_return_if_fail (EOM_IS_METADATA_READER_JPG (emr));

	priv = emr->priv;

	if (priv->state == EMR_FINISHED) return;

	for (i = 0; (i < len) && (priv->state != EMR_FINISHED); i++) {

		switch (priv->state) {
		case EMR_READ:
			if (buf[i] == EOM_JPEG_MARKER_START) {
				priv->state = EMR_READ_MARKER;
			}
			else {
				priv->state = EMR_FINISHED;
			}
			break;

		case EMR_READ_MARKER:
			if ((buf [i] & 0xF0) == 0xE0 || buf[i] == 0xFE) {
			/* we are reading some sort of APPxx or COM marker */
				/* these are always followed by 2 bytes of size information */
				priv->last_marker = buf [i];
				priv->size = 0;
				priv->state = EMR_READ_SIZE_HIGH_BYTE;

				eom_debug_message (DEBUG_IMAGE_DATA, "APPx or COM Marker Found: %x", priv->last_marker);
			}
			else {
				/* otherwise simply consume the byte */
				priv->state = EMR_READ;
			}
			break;

		case EMR_READ_SIZE_HIGH_BYTE:
			priv->size = (buf [i] & 0xff) << 8;
			priv->state = EMR_READ_SIZE_LOW_BYTE;
			break;

		case EMR_READ_SIZE_LOW_BYTE:
			priv->size |= (buf [i] & 0xff);

			if (priv->size > 2)  /* ignore the two size-bytes */
				priv->size -= 2;

			if (priv->size == 0) {
				priv->state = EMR_READ;
			} else if (priv->last_marker == EOM_JPEG_MARKER_APP1 &&
				   ((priv->exif_chunk == NULL) || (priv->xmp_chunk == NULL)))
			{
				priv->state = EMR_READ_APP1;
			} else if (priv->last_marker == EOM_JPEG_MARKER_APP2 &&
				   priv->icc_chunk == NULL && priv->size > 14)
			{
	 			/* Chunk has 14 bytes identification data */
				priv->state = EMR_READ_ICC;
			} else if (priv->last_marker == EOM_JPEG_MARKER_APP14 &&
				priv->iptc_chunk == NULL)
			{
				priv->state = EMR_READ_IPTC;
			} else {
				priv->state = EMR_SKIP_BYTES;
			}

			priv->last_marker = 0;
			break;

		case EMR_SKIP_BYTES:
			eom_debug_message (DEBUG_IMAGE_DATA, "Skip bytes: %i", priv->size);

			if (i + priv->size < len) {
				i = i + priv->size - 1; /* the for-loop consumes the other byte */
				priv->size = 0;
			}
			else {
				priv->size = (i + priv->size) - len;
				i = len - 1;
			}
			if (priv->size == 0) { /* don't need to skip any more bytes */
				priv->state = EMR_READ;
			}
			break;

		case EMR_READ_APP1:
			eom_debug_message (DEBUG_IMAGE_DATA, "Read APP1 data, Length: %i", priv->size);

			app1_type = eom_metadata_identify_app1 ((gchar*) &buf[i], priv->size);

			switch (app1_type) {
			case EJA_EXIF:
				if (priv->exif_chunk == NULL) {
					priv->exif_chunk = g_new0 (guchar, priv->size);
					priv->exif_len = priv->size;
					priv->bytes_read = 0;
					chunk = priv->exif_chunk;
					next_state = EMR_READ_EXIF;
				} else {
					chunk = NULL;
					priv->state = EMR_SKIP_BYTES;
				}
				break;
			case EJA_XMP:
				if (priv->xmp_chunk == NULL) {
					priv->xmp_chunk = g_new0 (guchar, priv->size);
					priv->xmp_len = priv->size;
					priv->bytes_read = 0;
					chunk = priv->xmp_chunk;
					next_state = EMR_READ_XMP;
				} else {
					chunk = NULL;
					priv->state = EMR_SKIP_BYTES;
				}
				break;
			case EJA_OTHER:
			default:
				/* skip unknown data */
				chunk = NULL;
				priv->state = EMR_SKIP_BYTES;
				break;
			}

			if (chunk) {
				eom_metadata_reader_get_next_block (priv, chunk,
								    &i, buf,
								    len,
								    next_state);
			}

			if (IS_FINISHED(priv))
				priv->state = EMR_FINISHED;
			break;

		case EMR_READ_EXIF:
			eom_debug_message (DEBUG_IMAGE_DATA, "Read continuation of EXIF data, length: %i", priv->size);
			{
 				eom_metadata_reader_get_next_block (priv, priv->exif_chunk,
 								    &i, buf, len, EMR_READ_EXIF);
			}
			if (IS_FINISHED(priv))
				priv->state = EMR_FINISHED;
			break;

		case EMR_READ_XMP:
			eom_debug_message (DEBUG_IMAGE_DATA, "Read continuation of XMP data, length: %i", priv->size);
			{
				eom_metadata_reader_get_next_block (priv, priv->xmp_chunk,
 								    &i, buf, len, EMR_READ_XMP);
			}
			if (IS_FINISHED (priv))
				priv->state = EMR_FINISHED;
			break;

		case EMR_READ_ICC:
			eom_debug_message (DEBUG_IMAGE_DATA,
					   "Read continuation of ICC data, "
					   "length: %i", priv->size);

			if (priv->icc_chunk == NULL) {
				priv->icc_chunk = g_new0 (guchar, priv->size);
				priv->icc_len = priv->size;
				priv->bytes_read = 0;
			}

			eom_metadata_reader_get_next_block (priv,
							    priv->icc_chunk,
							    &i, buf, len,
							    EMR_READ_ICC);

			/* Test that the chunk actually contains ICC data. */
			if (priv->state == EMR_READ && priv->icc_chunk) {
			    	const char* icc_chunk = priv->icc_chunk;
				gboolean valid = TRUE;

				/* Chunk should begin with the
				 * ICC_PROFILE\0 identifier */
				valid &= strncmp (icc_chunk,
						  "ICC_PROFILE\0",12) == 0;
				/* Make sure this is the first and only
				 * ICC chunk in the file as we don't
				 * support merging chunks yet. */
				valid &=  *(guint16*)(icc_chunk+12) == 0x101;

				if (!valid) {
					/* This no ICC data. Throw it away. */
					eom_debug_message (DEBUG_IMAGE_DATA,
					"Supposed ICC chunk didn't validate. "
					"Ignoring.");
					g_free (priv->icc_chunk);
					priv->icc_chunk = NULL;
					priv->icc_len = 0;
				}
			}

			if (IS_FINISHED(priv))
				priv->state = EMR_FINISHED;
			break;

		case EMR_READ_IPTC:
			eom_debug_message (DEBUG_IMAGE_DATA,
					   "Read continuation of IPTC data, "
					   "length: %i", priv->size);

			if (priv->iptc_chunk == NULL) {
				priv->iptc_chunk = g_new0 (guchar, priv->size);
				priv->iptc_len = priv->size;
				priv->bytes_read = 0;
			}

			eom_metadata_reader_get_next_block (priv,
							    priv->iptc_chunk,
							    &i, buf, len,
							    EMR_READ_IPTC);

			if (IS_FINISHED(priv))
				priv->state = EMR_FINISHED;
			break;

		default:
			g_assert_not_reached ();
		}
	}
}

/* Returns the raw exif data. NOTE: The caller of this function becomes
 * the new owner of this piece of memory and is responsible for freeing it!
 */
static void
eom_metadata_reader_jpg_get_exif_chunk (EomMetadataReaderJpg *emr, guchar **data, guint *len)
{
	EomMetadataReaderJpgPrivate *priv;

	g_return_if_fail (EOM_IS_METADATA_READER (emr));
	priv = emr->priv;

	*data = (guchar*) priv->exif_chunk;
	*len = priv->exif_len;

	priv->exif_chunk = NULL;
	priv->exif_len = 0;
}

#ifdef HAVE_EXIF
static gpointer
eom_metadata_reader_jpg_get_exif_data (EomMetadataReaderJpg *emr)
{
	EomMetadataReaderJpgPrivate *priv;
	ExifData *data = NULL;

	g_return_val_if_fail (EOM_IS_METADATA_READER (emr), NULL);
	priv = emr->priv;

	if (priv->exif_chunk != NULL) {
		data = exif_data_new_from_data (priv->exif_chunk, priv->exif_len);
	}

	return data;
}
#endif


#ifdef HAVE_EXEMPI

/* skip the signature */
#define EOM_XMP_OFFSET (29)

static gpointer
eom_metadata_reader_jpg_get_xmp_data (EomMetadataReaderJpg *emr )
{
	EomMetadataReaderJpgPrivate *priv;
	XmpPtr xmp = NULL;

	g_return_val_if_fail (EOM_IS_METADATA_READER (emr), NULL);

	priv = emr->priv;

	if (priv->xmp_chunk != NULL) {
		xmp = xmp_new (priv->xmp_chunk+EOM_XMP_OFFSET,
			       priv->xmp_len-EOM_XMP_OFFSET);
	}

	return (gpointer)xmp;
}
#endif

/*
 * FIXME: very broken, assumes the profile fits in a single chunk.  Change to
 * parse the sections and construct a single memory chunk, or maybe even parse
 * the profile.
 */
#ifdef HAVE_LCMS
static gpointer
eom_metadata_reader_jpg_get_icc_profile (EomMetadataReaderJpg *emr)
{
	EomMetadataReaderJpgPrivate *priv;
	cmsHPROFILE profile = NULL;

	g_return_val_if_fail (EOM_IS_METADATA_READER (emr), NULL);

	priv = emr->priv;

	if (priv->icc_chunk) {
		profile = cmsOpenProfileFromMem(priv->icc_chunk + 14, priv->icc_len - 14);

		if (profile) {
			eom_debug_message (DEBUG_LCMS, "JPEG has ICC profile");
		} else {
			eom_debug_message (DEBUG_LCMS, "JPEG has invalid ICC profile");
		}
	}

#ifdef HAVE_EXIF
	if (!profile && priv->exif_chunk != NULL) {
		ExifEntry *entry;
		ExifByteOrder o;
		gint color_space;
		ExifData *exif = eom_metadata_reader_jpg_get_exif_data (emr);

		if (!exif) return NULL;

		o = exif_data_get_byte_order (exif);

		entry = exif_data_get_entry (exif, EXIF_TAG_COLOR_SPACE);

		if (entry == NULL) {
			exif_data_unref (exif);
			return NULL;
		}

		color_space = exif_get_short (entry->data, o);

		switch (color_space) {
		case 1:
			eom_debug_message (DEBUG_LCMS, "JPEG is sRGB");

			profile = cmsCreate_sRGBProfile ();

			break;
		case 2:
			eom_debug_message (DEBUG_LCMS, "JPEG is Adobe RGB (Disabled)");

			/* TODO: create Adobe RGB profile */
			//profile = cmsCreate_Adobe1998Profile ();

			break;
		case 0xFFFF:
		  	{
			cmsCIExyY whitepoint;
			cmsCIExyYTRIPLE primaries;
			cmsToneCurve *gamma[3];
			double gammaValue;
			ExifRational r;

			const int offset = exif_format_get_size (EXIF_FORMAT_RATIONAL);

			entry = exif_data_get_entry (exif, EXIF_TAG_WHITE_POINT);

			if (entry && entry->components == 2) {
				r = exif_get_rational (entry->data, o);
				whitepoint.x = (double) r.numerator / r.denominator;

				r = exif_get_rational (entry->data + offset, o);
				whitepoint.y = (double) r.numerator / r.denominator;
				whitepoint.Y = 1.0;
			} else {
				eom_debug_message (DEBUG_LCMS, "No whitepoint found");
				break;
			}

			entry = exif_data_get_entry (exif, EXIF_TAG_PRIMARY_CHROMATICITIES);

			if (entry && entry->components == 6) {
				r = exif_get_rational (entry->data + 0 * offset, o);
				primaries.Red.x = (double) r.numerator / r.denominator;

				r = exif_get_rational (entry->data + 1 * offset, o);
				primaries.Red.y = (double) r.numerator / r.denominator;

				r = exif_get_rational (entry->data + 2 * offset, o);
				primaries.Green.x = (double) r.numerator / r.denominator;

				r = exif_get_rational (entry->data + 3 * offset, o);
				primaries.Green.y = (double) r.numerator / r.denominator;

				r = exif_get_rational (entry->data + 4 * offset, o);
				primaries.Blue.x = (double) r.numerator / r.denominator;

				r = exif_get_rational (entry->data + 5 * offset, o);
				primaries.Blue.y = (double) r.numerator / r.denominator;

				primaries.Red.Y = primaries.Green.Y = primaries.Blue.Y = 1.0;
			} else {
				eom_debug_message (DEBUG_LCMS, "No primary chromaticities found");
				break;
			}

			entry = exif_data_get_entry (exif, EXIF_TAG_GAMMA);

			if (entry) {
				r = exif_get_rational (entry->data, o);
				gammaValue = (double) r.numerator / r.denominator;
			} else {
				eom_debug_message (DEBUG_LCMS, "No gamma found");
				gammaValue = 2.2;
			}

			gamma[0] = gamma[1] = gamma[2] = cmsBuildGamma (NULL, gammaValue);

			profile = cmsCreateRGBProfile (&whitepoint, &primaries, gamma);

			cmsFreeToneCurve(gamma[0]);

			eom_debug_message (DEBUG_LCMS, "JPEG is calibrated");

			break;
			}
		}

		exif_data_unref (exif);
	}
#endif
	return profile;
}
#endif

static void
eom_metadata_reader_jpg_init_emr_iface (gpointer g_iface, gpointer iface_data)
{
	EomMetadataReaderInterface *iface;

	iface = (EomMetadataReaderInterface*)g_iface;

	iface->consume =
		(void (*) (EomMetadataReader *self, const guchar *buf, guint len))
			eom_metadata_reader_jpg_consume;
	iface->finished =
		(gboolean (*) (EomMetadataReader *self))
			eom_metadata_reader_jpg_finished;
	iface->get_raw_exif =
		(void (*) (EomMetadataReader *self, guchar **data, guint *len))
			eom_metadata_reader_jpg_get_exif_chunk;
#ifdef HAVE_EXIF
	iface->get_exif_data =
		(gpointer (*) (EomMetadataReader *self))
			eom_metadata_reader_jpg_get_exif_data;
#endif
#ifdef HAVE_LCMS
	iface->get_icc_profile =
		(gpointer (*) (EomMetadataReader *self))
			eom_metadata_reader_jpg_get_icc_profile;
#endif
#ifdef HAVE_EXEMPI
	iface->get_xmp_ptr =
		(gpointer (*) (EomMetadataReader *self))
			eom_metadata_reader_jpg_get_xmp_data;
#endif
}