/* 
 * GdkPixbuf library - TGA image loader
 * Copyright (C) 1999 Nicola Girardi <nikke@swlibero.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 */

/*
 * Some NOTES about the TGA loader (2001/06/07, nikke@swlibero.org)
 *
 * - The module doesn't currently provide support for TGA images where the
 *   order of the pixels isn't left-to-right and top-to-bottom.  I plan to
 *   add support for those files as soon as I get one of them.  I haven't
 *   run into one yet.  (And I don't seem to be able to create it with GIMP.)
 *
 * - The TGAFooter isn't present in all TGA files.  In fact, there's an older
 *   format specification, still in use, which doesn't cover the TGAFooter.
 *   Actually, most TGA files I have are of the older type.  Anyway I put the 
 *   struct declaration here for completeness.
 *
 * - Error handling was designed to be very paranoid.
 */

#include <stdio.h>
#include <string.h>

#include "gdk-pixbuf.h"
#include "gdk-pixbuf-io.h"
#include "gdk-pixbuf-private.h"

#undef DEBUG_TGA

#define TGA_INTERLEAVE_MASK     0xc0
#define TGA_INTERLEAVE_NONE     0x00
#define TGA_INTERLEAVE_2WAY     0x40
#define TGA_INTERLEAVE_4WAY     0x80

#define TGA_ORIGIN_MASK         0x30
#define TGA_ORIGIN_LEFT         0x00
#define TGA_ORIGIN_RIGHT        0x10
#define TGA_ORIGIN_LOWER        0x00
#define TGA_ORIGIN_UPPER        0x20

enum {
	TGA_TYPE_NODATA = 0,
	TGA_TYPE_PSEUDOCOLOR = 1,
	TGA_TYPE_TRUECOLOR = 2,
	TGA_TYPE_GRAYSCALE = 3,
	TGA_TYPE_RLE_PSEUDOCOLOR = 9,
	TGA_TYPE_RLE_TRUECOLOR = 10,
	TGA_TYPE_RLE_GRAYSCALE = 11
};

#define LE16(p) ((p)[0] + ((p)[1] << 8))

typedef struct _IOBuffer IOBuffer;

typedef struct _TGAHeader TGAHeader;
typedef struct _TGAFooter TGAFooter;

typedef struct _TGAColormap TGAColormap;
typedef struct _TGAColor TGAColor;

typedef struct _TGAContext TGAContext;

struct _TGAHeader {
	guint8 infolen;
	guint8 has_cmap;
	guint8 type;
	
	guint8 cmap_start[2];
	guint8 cmap_n_colors[2];
	guint8 cmap_bpp;
	
	guint8 x_origin[2];
	guint8 y_origin[2];
	
	guint8 width[2];
	guint8 height[2];
	guint8 bpp;
	
	guint8 flags;
};

struct _TGAFooter {
	guint32 extension_area_offset;
	guint32 developer_directory_offset;

	/* Standard TGA signature, "TRUEVISION-XFILE.\0". */
	union {
		gchar sig_full[18];
		struct {
			gchar sig_chunk[16];
			gchar dot, null;
		} sig_struct;
	} sig;
};

struct _TGAColormap {
	gint size;
	TGAColor *cols;
};

struct _TGAColor {
	guchar r, g, b, a;
};

struct _TGAContext {
	TGAHeader *hdr;
	guint rowstride;
	guint completed_lines;
	gboolean run_length_encoded;

	TGAColormap *cmap;
	guint cmap_size;

	GdkPixbuf *pbuf;
	guint pbuf_bytes;
	guint pbuf_bytes_done;
	guchar *pptr;

	IOBuffer *in;

	gboolean skipped_info;
	gboolean prepared;
	gboolean done;

	ModulePreparedNotifyFunc pfunc;
	ModuleUpdatedNotifyFunc ufunc;
	gpointer udata;
};

struct _IOBuffer {
	guchar *data;
	guint size;
};

static IOBuffer *io_buffer_new(GError **err)
{
	IOBuffer *buffer;
	buffer = g_try_malloc(sizeof(IOBuffer));
	if (!buffer) {
		g_set_error(err, GDK_PIXBUF_ERROR,
			    GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
			    _("Can't allocate memory for IOBuffer struct"));
		return NULL;
	}
	buffer->data = NULL;
	buffer->size = 0;
	return buffer;
}

static IOBuffer *io_buffer_append(IOBuffer *buffer, 
				  const guchar *data, guint len, 
				  GError **err)
{
	if (!buffer)
		return NULL;
	if (!buffer->data) {
		buffer->data = g_try_malloc(len);
		if (!buffer->data) {
			g_set_error(err, GDK_PIXBUF_ERROR,
				    GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
				    _("Can't allocate memory for IOBuffer data"));
			g_free(buffer);
			return NULL;
		}
		g_memmove(buffer->data, data, len);
		buffer->size = len;
	} else {
		buffer->data = g_try_realloc(buffer->data, buffer->size + len);
		if (!buffer->data) {
			g_set_error(err, GDK_PIXBUF_ERROR,
				    GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
				    _("Can't realloc IOBuffer data"));
			g_free(buffer);
			return NULL;
		}
		g_memmove(&buffer->data[buffer->size], data, len);
		buffer->size += len;
	}
	return buffer;
}

static IOBuffer *io_buffer_free_segment(IOBuffer *buffer, 
					guint count,
                                        GError **err)
{
	g_return_val_if_fail(buffer != NULL, NULL);
	g_return_val_if_fail(buffer->data != NULL, NULL);
	if (count == buffer->size) {
		g_free(buffer->data);
		buffer->data = NULL;
		buffer->size = 0;
	} else {
		guchar *new_buf;
		guint new_size;

		new_size = buffer->size - count;
		new_buf = g_try_malloc(new_size);
		if (!new_buf) {
			g_set_error(err, GDK_PIXBUF_ERROR,
				    GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
				    _("Can't allocate temporary IOBuffer data"));
			g_free(buffer->data);
			g_free(buffer);
			return NULL;
		}

		g_memmove(new_buf, &buffer->data[count], new_size);
		g_free(buffer->data);
		buffer->data = new_buf;
		buffer->size = new_size;
	}
	return buffer;
}

static void io_buffer_free(IOBuffer *buffer)
{
	g_return_if_fail(buffer != NULL);
	if (buffer->data)
		g_free(buffer->data);
	g_free(buffer);
}

static void free_buffer(guchar *pixels, gpointer data)
{
	g_free(pixels);
}

static GdkPixbuf *get_contiguous_pixbuf (guint width, 
					 guint height, 
					 gboolean has_alpha)
{
	guchar *pixels;
	guint channels, rowstride, bytes;
	
	if (has_alpha) 
		channels = 4;
	else 
		channels = 3;
	
	rowstride = width * channels;
	
	if (rowstride / channels != width)
                return NULL;                

	bytes = height * rowstride;

        if (bytes / rowstride != height)
                return NULL;                

        pixels = g_try_malloc (bytes);

	if (!pixels)
		return NULL;
	
	return gdk_pixbuf_new_from_data (pixels, GDK_COLORSPACE_RGB, has_alpha, 8,
					 width, height, rowstride, free_buffer, NULL);
}

static gboolean fread_check(gpointer dest, 
			    size_t size, size_t count, 
			    FILE *f, GError **err)
{
	if (fread(dest, size, count, f) != count) {
		g_set_error(err, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
			    _("fread() failed -- premature end-of-file probably encountered"));
		return FALSE;
	}
	return TRUE;
}

static gboolean fseek_check(FILE *f, glong offset, gint whence, GError **err)
{
	if (fseek(f, offset, whence) != 0) {
		g_set_error(err, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
			    _("fseek() failed -- premature end-of-file probably encountered"));
		return FALSE;
	}
	return TRUE;
}

static gboolean fill_in_context(TGAContext *ctx, GError **err)
{
	gboolean alpha;
	guint w, h;

	g_return_val_if_fail(ctx != NULL, FALSE);

	ctx->run_length_encoded =
		((ctx->hdr->type == TGA_TYPE_RLE_PSEUDOCOLOR)
		 || (ctx->hdr->type == TGA_TYPE_RLE_TRUECOLOR)
		 || (ctx->hdr->type == TGA_TYPE_RLE_GRAYSCALE));

	if (ctx->hdr->has_cmap)
		ctx->cmap_size = ((ctx->hdr->cmap_bpp + 7) >> 3) *
			LE16(ctx->hdr->cmap_n_colors);

	alpha = ((ctx->hdr->bpp == 16) || 
		 (ctx->hdr->bpp == 32) ||
		 (ctx->hdr->has_cmap && (ctx->hdr->cmap_bpp == 32)));

	w = LE16(ctx->hdr->width);
	h = LE16(ctx->hdr->height);

	ctx->pbuf = get_contiguous_pixbuf (w, h, alpha);

	if (!ctx->pbuf) {
		g_set_error(err, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
			    _("Can't allocate new pixbuf"));
		return FALSE;
	}

	ctx->pbuf_bytes = ctx->pbuf->rowstride * ctx->pbuf->height;
	ctx->pptr = ctx->pbuf->pixels;

	if (ctx->hdr->type == TGA_TYPE_PSEUDOCOLOR)
	  ctx->rowstride = ctx->pbuf->width;
	else if (ctx->hdr->type == TGA_TYPE_GRAYSCALE)
	  ctx->rowstride = (alpha ? ctx->pbuf->width * 2 : ctx->pbuf->width);
	else if (ctx->hdr->type == TGA_TYPE_TRUECOLOR)
		ctx->rowstride = ctx->pbuf->rowstride;

	ctx->completed_lines = 0;
	return TRUE;
}

static void parse_data_for_row_pseudocolor(TGAContext *ctx)
{
	guchar *s = ctx->in->data;
	guint upper_bound = ctx->pbuf->width;
	guchar *p = ctx->pptr;

	for (; upper_bound; upper_bound--, s++) {
		*p++ = ctx->cmap->cols[*s].r;
		*p++ = ctx->cmap->cols[*s].g;
		*p++ = ctx->cmap->cols[*s].b;
		if (ctx->hdr->cmap_bpp == 32)
			*p++ = ctx->cmap->cols[*s].a;
	}
	ctx->pptr += ctx->pbuf->rowstride;
	ctx->pbuf_bytes_done += ctx->pbuf->rowstride;
	if (ctx->pbuf_bytes_done == ctx->pbuf_bytes)
		ctx->done = TRUE;
}

static void swap_channels(TGAContext *ctx)
{
	guchar swap;
	guint count;
	guchar *p = ctx->pptr;
	for (count = ctx->pbuf->width; count; count--) {
	  swap = p[0];
	  p[0] = p[2];
	  p[2] = swap;
	  p += ctx->pbuf->n_channels;
	}
}

static void parse_data_for_row_truecolor(TGAContext *ctx)
{
	g_memmove(ctx->pptr, ctx->in->data, ctx->pbuf->rowstride);
	swap_channels(ctx);
	ctx->pptr += ctx->pbuf->rowstride;
	ctx->pbuf_bytes_done += ctx->pbuf->rowstride;
	if (ctx->pbuf_bytes_done == ctx->pbuf_bytes)
		ctx->done = TRUE;
}

static void parse_data_for_row_grayscale(TGAContext *ctx)
{
	guchar *s = ctx->in->data;
	guint upper_bound = ctx->pbuf->width;

	guchar *p = ctx->pptr;
	for (; upper_bound; upper_bound--) {
		p[0] = p[1] = p[2] = *s++;
		if (ctx->pbuf->n_channels == 4)
		  p[3] = *s++;
		p += ctx->pbuf->n_channels;
	}
	ctx->pptr += ctx->pbuf->rowstride;
	ctx->pbuf_bytes_done += ctx->pbuf->rowstride;
	if (ctx->pbuf_bytes_done == ctx->pbuf_bytes)
		ctx->done = TRUE;
}

static gboolean parse_data_for_row(TGAContext *ctx, GError **err)
{
	if (ctx->hdr->type == TGA_TYPE_PSEUDOCOLOR)
		parse_data_for_row_pseudocolor(ctx);
	else if (ctx->hdr->type == TGA_TYPE_TRUECOLOR)
		parse_data_for_row_truecolor(ctx);
	else if (ctx->hdr->type == TGA_TYPE_GRAYSCALE)
		parse_data_for_row_grayscale(ctx);
	ctx->in = io_buffer_free_segment(ctx->in, ctx->rowstride, err);
	if (!ctx->in)
		return FALSE;
	(*ctx->ufunc) (ctx->pbuf, 0,
		       (ctx->pbuf_bytes_done / ctx->pbuf->rowstride) - 1,
		       ctx->pbuf->width, 1, ctx->udata);
	return TRUE;
}

static void write_rle_data(TGAContext *ctx, TGAColor *color, guint *rle_count)
{
	for (; *rle_count; (*rle_count)--) {
		g_memmove(ctx->pptr, (guchar *) color, ctx->pbuf->n_channels);
		ctx->pptr += ctx->pbuf->n_channels;
		ctx->pbuf_bytes_done += ctx->pbuf->n_channels;
		if (ctx->pbuf_bytes_done == ctx->pbuf_bytes)
			return;
	}
}

static guint parse_rle_data_pseudocolor(TGAContext *ctx)
{
	guint rle_num, raw_num;
	guchar *s, tag;
	guint n;

	g_return_val_if_fail(ctx->in->size > 0, 0);
	s = ctx->in->data;

	for (n = 0; n < ctx->in->size; ) {
		tag = *s;
		s++, n++;
		if (tag & 0x80) {
			if (n == ctx->in->size) {
				return --n;
			} else {
				rle_num = (tag & 0x7f) + 1;
				write_rle_data(ctx, &ctx->cmap->cols[*s], &rle_num);
				s++, n++;
				if (ctx->pbuf_bytes_done == ctx->pbuf_bytes) {
					ctx->done = TRUE;
					return n;
				}
			}
		} else {
			raw_num = tag + 1;
			if (n + raw_num >= ctx->in->size) {
				return --n;
			} else {
				for (; raw_num; raw_num--) {
					*ctx->pptr++ =
						ctx->cmap->cols[*s].r;
					*ctx->pptr++ =
						ctx->cmap->cols[*s].g;
					*ctx->pptr++ =
						ctx->cmap->cols[*s].b;
					if (ctx->pbuf->n_channels == 4)
						*ctx->pptr++ = ctx->cmap->cols[*s].a;
					s++, n++;
					ctx->pbuf_bytes_done += ctx->pbuf->n_channels;
					if (ctx->pbuf_bytes_done == ctx->pbuf_bytes) {
						ctx->done = TRUE;
						return n;
					}
				}
			}
		}
	}

	if (ctx->pbuf_bytes_done == ctx->pbuf_bytes)
		ctx->done = TRUE;
	return n;
}

static guint parse_rle_data_truecolor(TGAContext *ctx)
{
	TGAColor col;
	guint rle_num, raw_num;
	guchar *s, tag;
	guint n = 0;

	g_return_val_if_fail(ctx->in->size > 0, 0);
	s = ctx->in->data;

	for (n = 0; n < ctx->in->size; ) {
		tag = *s;
		s++, n++;
		if (tag & 0x80) {
			if (n + ctx->pbuf->n_channels >= ctx->in->size) {
				return --n;
			} else {
				rle_num = (tag & 0x7f) + 1;
				col.b = *s++;
				col.g = *s++;
				col.r = *s++;
				if (ctx->hdr->bpp == 32)
					col.a = *s++;
				n += ctx->pbuf->n_channels;
				write_rle_data(ctx, &col, &rle_num);
				if (ctx->pbuf_bytes_done == ctx->pbuf_bytes) {
					ctx->done = TRUE;
					return n;
				}
			}
		} else {
			raw_num = tag + 1;
			if (n + (raw_num * ctx->pbuf->n_channels) >= ctx->in->size) {
				return --n;
			} else {
				for (; raw_num; raw_num--) {
					ctx->pptr[2] = *s++;
					ctx->pptr[1] = *s++;
					ctx->pptr[0] = *s++;
					if (ctx->hdr->bpp == 32)
						ctx->pptr[3] = *s++;
					n += ctx->pbuf->n_channels;
					ctx->pptr += ctx->pbuf->n_channels;
					ctx->pbuf_bytes_done += ctx->pbuf->n_channels;
					if (ctx->pbuf_bytes_done == ctx->pbuf_bytes) {
						ctx->done = TRUE;
						return n;
					}
				}
				
				if (ctx->pbuf_bytes_done == ctx->pbuf_bytes) {
					ctx->done = TRUE;
					return n;
				}
			}
		}
	}
	if (ctx->pbuf_bytes_done == ctx->pbuf_bytes)
		ctx->done = TRUE;
	return n;
}

static guint parse_rle_data_grayscale(TGAContext *ctx)
{
	TGAColor tone;
	guint rle_num, raw_num;
	guchar *s, tag;
	guint n;

	g_return_val_if_fail(ctx->in->size > 0, 0);
	s = ctx->in->data;

	for (n = 0; n < ctx->in->size; ) {
		tag = *s;
		s++, n++;
		if (tag & 0x80) {
			if (n + (ctx->pbuf->n_channels == 4 ? 2 : 1) >= ctx->in->size) {
				return --n;
			} else {
				rle_num = (tag & 0x7f) + 1;
				tone.r = tone.g = tone.b = *s;
				s++, n++;
				if (ctx->pbuf->n_channels == 4) {
					tone.a = *s++;
					n++;
				}
				write_rle_data(ctx, &tone, &rle_num);
				if (ctx->pbuf_bytes_done == ctx->pbuf_bytes) {
					ctx->done = TRUE;
					return n;
				}
			}
		} else {
			raw_num = tag + 1;
			if (n + raw_num * (ctx->pbuf->n_channels == 4 ? 2 : 1) >= ctx->in->size) {
				return --n;
			} else {
				for (; raw_num; raw_num--) {
					ctx->pptr[0] = ctx->pptr[1] = ctx->pptr[2] = *s;
					s++, n++;
					if (ctx->pbuf->n_channels == 4) {
						ctx->pptr[3] = *s++;
						n++;
					}
					ctx->pptr += ctx->pbuf->n_channels;
					ctx->pbuf_bytes_done += ctx->pbuf->n_channels;
					if (ctx->pbuf_bytes_done == ctx->pbuf_bytes) {
						ctx->done = TRUE;
						return n;
					}
				}
			}
		}
	}
	if (ctx->pbuf_bytes_done == ctx->pbuf_bytes)
		ctx->done = TRUE;
	return n;
}

static gboolean parse_rle_data(TGAContext *ctx, GError **err)
{
	guint count = 0;
	guint pbuf_count = 0;
	if (ctx->hdr->type == TGA_TYPE_RLE_PSEUDOCOLOR) {
		count = parse_rle_data_pseudocolor(ctx);
		pbuf_count = count * ctx->pbuf->n_channels;
	} else if (ctx->hdr->type == TGA_TYPE_RLE_TRUECOLOR) {
		count = parse_rle_data_truecolor(ctx);
		pbuf_count = count;
	} else if (ctx->hdr->type == TGA_TYPE_RLE_GRAYSCALE) {
		count = parse_rle_data_grayscale(ctx);
		pbuf_count = count * (ctx->pbuf->n_channels == 4 ? 2 : 3);
	}
	ctx->in = io_buffer_free_segment(ctx->in, count, err);
	if (!ctx->in)
		return FALSE;
	(*ctx->ufunc) (ctx->pbuf, 0, ctx->pbuf_bytes_done / ctx->pbuf->rowstride,
		       ctx->pbuf->width, pbuf_count / ctx->pbuf->rowstride,
		       ctx->udata);
	return TRUE;
}

static gboolean try_colormap(TGAContext *ctx, GError **err)
{
	static guchar *p;
	static guint n;

	g_return_val_if_fail(ctx != NULL, FALSE);
	g_return_val_if_fail(ctx->cmap_size > 0, TRUE);

	ctx->cmap = g_try_malloc(sizeof(TGAColormap));
	if (!ctx->cmap) {
		g_set_error(err, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
			    _("Can't allocate colormap structure"));
		return FALSE;
	}
	ctx->cmap->size = LE16(ctx->hdr->cmap_n_colors);
	ctx->cmap->cols = g_try_malloc(sizeof(TGAColor) * ctx->cmap->size);
	if (!ctx->cmap->cols) {
		g_set_error(err, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
			    _("Can't allocate colormap entries"));
		return FALSE;
	}

	p = ctx->in->data;
	for (n = 0; n < ctx->cmap->size; n++) {
		if ((ctx->hdr->cmap_bpp == 15) || (ctx->hdr->cmap_bpp == 16)) {
			guint16 col = p[0] + (p[1] << 8);
			ctx->cmap->cols[n].b = (col >> 7) & 0xf8;
			ctx->cmap->cols[n].g = (col >> 2) & 0xf8;
			ctx->cmap->cols[n].r = col << 3;
			p += 2;
		}
		else if ((ctx->hdr->cmap_bpp == 24) || (ctx->hdr->cmap_bpp == 32)) {
			ctx->cmap->cols[n].b = *p++;
			ctx->cmap->cols[n].g = *p++;
			ctx->cmap->cols[n].r = *p++;
			if (ctx->hdr->cmap_bpp == 32)
				ctx->cmap->cols[n].a = *p++;
		} else {
			g_set_error(err, GDK_PIXBUF_ERROR, 
				    GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
				    _("Unexpected bitdepth for colormap entries"));
			return FALSE;
		}
	}
	ctx->in = io_buffer_free_segment(ctx->in, ctx->cmap_size, err);
	if (!ctx->in)
		return FALSE;
	return TRUE;
}

static gboolean try_preload(TGAContext *ctx, GError **err)
{
	if (!ctx->hdr) {
		if (ctx->in->size >= sizeof(TGAHeader)) {
			ctx->hdr = g_try_malloc(sizeof(TGAHeader));
			if (!ctx->hdr) {
				g_set_error(err, GDK_PIXBUF_ERROR,
					    GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
					    _("Can't allocate TGA header memory"));
				return FALSE;
			}
			g_memmove(ctx->hdr, ctx->in->data, sizeof(TGAHeader));
			ctx->in = io_buffer_free_segment(ctx->in, sizeof(TGAHeader), err);
#ifdef DEBUG_TGA
			g_print ("infolen %d "
				 "has_cmap %d "
				 "type %d "
				 "cmap_start %d "
				 "cmap_n_colors %d "
				 "cmap_bpp %d "
				 "x %d y %d width %d height %d bpp %d "
				 "flags %#x",
				 ctx->hdr->infolen,
				 ctx->hdr->has_cmap,
				 ctx->hdr->type,
				 LE16(ctx->hdr->cmap_start),
				 LE16(ctx->hdr->cmap_n_colors),
				 ctx->hdr->cmap_bpp,
				 LE16(ctx->hdr->x_origin),
				 LE16(ctx->hdr->y_origin),
				 LE16(ctx->hdr->width),
				 LE16(ctx->hdr->height),
				 ctx->hdr->bpp,
				 ctx->hdr->flags);
#endif
			if (!ctx->in)
				return FALSE;
			if (LE16(ctx->hdr->width) == 0 || 
			    LE16(ctx->hdr->height) == 0) {
				g_set_error(err, GDK_PIXBUF_ERROR,
					    GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
					    _("TGA image has invalid dimensions"));
				return FALSE;
			}
			if (ctx->hdr->infolen > 255) {
				g_set_error(err, GDK_PIXBUF_ERROR,
					    GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
					    _("TGA image comment length is too long"));
				return FALSE;
			}
			if ((ctx->hdr->flags & TGA_INTERLEAVE_MASK) != 
			    TGA_INTERLEAVE_NONE ||
			    ctx->hdr->flags & TGA_ORIGIN_RIGHT || 
			    ctx->hdr->flags & TGA_ORIGIN_LOWER) {
				g_set_error(err, GDK_PIXBUF_ERROR, 
					    GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
					    _("TGA image type not supported"));
				return FALSE;
			}
			switch (ctx->hdr->type) {
			    case TGA_TYPE_PSEUDOCOLOR:
			    case TGA_TYPE_RLE_PSEUDOCOLOR:
				    if (ctx->hdr->bpp != 8) {
					    g_set_error(err, GDK_PIXBUF_ERROR, 
							GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
							_("TGA image type not supported"));
					    return FALSE;
				    }
				    break;
			    case TGA_TYPE_TRUECOLOR:
			    case TGA_TYPE_RLE_TRUECOLOR:
				    if (ctx->hdr->bpp != 24 &&
					ctx->hdr->bpp != 32) {
					    g_set_error(err, GDK_PIXBUF_ERROR, 
							GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
							_("TGA image type not supported"));
					    return FALSE;
				    }			      
				    break;
			    case TGA_TYPE_GRAYSCALE:
			    case TGA_TYPE_RLE_GRAYSCALE:
				    if (ctx->hdr->bpp != 8 &&
					ctx->hdr->bpp != 16) {
					    g_set_error(err, GDK_PIXBUF_ERROR, 
							GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
							_("TGA image type not supported"));
					    return FALSE;
				    }
				    break;
			    default:
				    g_set_error(err, GDK_PIXBUF_ERROR, 
						GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
						_("TGA image type not supported"));
				    return FALSE;    
			}
			if (!fill_in_context(ctx, err))
				return FALSE;
		} else {
			return TRUE;
		}
	}
	if (!ctx->skipped_info) {
		if (ctx->in->size >= ctx->hdr->infolen) {
			ctx->in = io_buffer_free_segment(ctx->in, ctx->hdr->infolen, err);
			if (!ctx->in)
				return FALSE;
			ctx->skipped_info = TRUE;
		} else {
			return TRUE;
		}
	}
	if (ctx->hdr->has_cmap && !ctx->cmap) {
		if (ctx->in->size >= ctx->cmap_size) {
			if (!try_colormap(ctx, err))
				return FALSE;
		} else {
			return TRUE;
		}
	}
	if (!ctx->prepared) {
		(*ctx->pfunc) (ctx->pbuf, NULL, ctx->udata);
		ctx->prepared = TRUE;
	}
	/* We shouldn't get here anyway. */
	return TRUE;
}

static gpointer gdk_pixbuf__tga_begin_load(ModulePreparedNotifyFunc f1,
					   ModuleUpdatedNotifyFunc f2,
					   gpointer udata, GError **err)
{
	TGAContext *ctx;

	ctx = g_try_malloc(sizeof(TGAContext));
	if (!ctx) {
		g_set_error(err, GDK_PIXBUF_ERROR, 
			    GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
			    _("Can't allocate memory for TGA context struct"));
		return NULL;
	}

	ctx->hdr = NULL;
	ctx->rowstride = 0;
	ctx->run_length_encoded = FALSE;

	ctx->cmap = NULL;
	ctx->cmap_size = 0;

	ctx->pbuf = NULL;
	ctx->pbuf_bytes = 0;
	ctx->pbuf_bytes_done = 0;
	ctx->pptr = NULL;

	ctx->in = io_buffer_new(err);
	if (!ctx->in) {
		g_free(ctx);
		return NULL;
	}

	ctx->skipped_info = FALSE;
	ctx->prepared = FALSE;
	ctx->done = FALSE;

	ctx->pfunc = f1;
	ctx->ufunc = f2;
	ctx->udata = udata;

	return ctx;
}

static gboolean gdk_pixbuf__tga_load_increment(gpointer data,
					       const guchar *buffer,
					       guint size,
					       GError **err)
{
	TGAContext *ctx = (TGAContext*) data;
	g_return_val_if_fail(ctx != NULL, FALSE);

	if (ctx->done)
		return TRUE;

	g_return_val_if_fail(buffer != NULL, TRUE);
	ctx->in = io_buffer_append(ctx->in, buffer, size, err);
	if (!ctx->in)
		return FALSE;
	if (!ctx->prepared) {
		if (!try_preload(ctx, err))
			return FALSE;
		if (!ctx->prepared)
			return TRUE;
		if (ctx->in->size == 0)
			return TRUE;
	}

	if (ctx->run_length_encoded) {
		if (!parse_rle_data(ctx, err))
			return FALSE;
	} else {
		while (ctx->in->size >= ctx->rowstride) {
			if (ctx->completed_lines >= ctx->pbuf->height) {
				g_set_error(err, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
					    _("Excess data in file"));
				return FALSE;
			}
			if (!parse_data_for_row(ctx, err))
				return FALSE;
			ctx->completed_lines++;
		}
	}

	return TRUE;
}

static gboolean gdk_pixbuf__tga_stop_load(gpointer data, GError **err)
{
	TGAContext *ctx = (TGAContext *) data;
	g_return_val_if_fail(ctx != NULL, FALSE);

	if (ctx->hdr)
	  g_free (ctx->hdr);
	if (ctx->cmap) {
	  if (ctx->cmap->cols)
	    g_free (ctx->cmap->cols);
	  g_free (ctx->cmap);
	}
	if (ctx->pbuf)
		g_object_unref (ctx->pbuf);
	if (ctx->in->size)
		ctx->in = io_buffer_free_segment (ctx->in, ctx->in->size, err);
	if (!ctx->in) {
		g_free (ctx);
		return FALSE;
	}
	io_buffer_free (ctx->in);
	g_free (ctx);
	return TRUE;
}

static TGAHeader *get_header_from_file(FILE *f, GError **err)
{
	TGAHeader *hdr;

	if (!fseek_check(f, 0, SEEK_SET, err))
		return NULL;
	if (!(hdr = g_try_malloc(sizeof(TGAHeader)))) {
		g_set_error(err, GDK_PIXBUF_ERROR, 
			    GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
			    _("Can't allocate memory for TGA header"));
		return NULL;
	}
	if (!fread_check(hdr, sizeof(TGAHeader), 1, f, err)) {
		g_free(hdr);
		return NULL;
	}
	if (hdr->infolen > 255) {
		g_set_error(err, GDK_PIXBUF_ERROR, 
			    GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
			    _("Too big value in the infolen field of TGA header."));
		g_free(hdr);
		return NULL;
	}

	return hdr;
}

static TGAColormap *get_colormap_from_file(FILE *f, 
					   TGAHeader *hdr,
					   GError **err)
{
	TGAColormap *cmap;
	guchar *pal_buf, *p;
	guint n, pal_size;
  
	if (!fseek_check(f, sizeof(TGAHeader) + hdr->infolen, SEEK_SET, err))
		return NULL;

	pal_size = LE16(hdr->cmap_n_colors) * ((hdr->cmap_bpp + 7) >> 3);
	pal_buf = g_try_malloc(pal_size);
	if (!pal_buf) {
		g_set_error(err, GDK_PIXBUF_ERROR, 
			    GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
			    _("Can't allocate memory for TGA cmap temporary buffer"));
		return NULL;
	}
	if (!fread_check(pal_buf, pal_size, 1, f, err)) {
		g_free(pal_buf);
		return NULL;
	}
	p = pal_buf;

	if (!(cmap = g_try_malloc(sizeof(TGAColormap)))) {
		g_set_error(err, GDK_PIXBUF_ERROR, 
			    GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
			    _("Can't allocate memory for TGA colormap struct"));
		g_free(pal_buf);
		return NULL;
	}
	cmap->size = LE16(hdr->cmap_n_colors);
	cmap->cols = g_try_malloc(sizeof(TGAColor) * cmap->size);
	if (!cmap->cols) {
		g_set_error(err, GDK_PIXBUF_ERROR, 
			    GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
			    _("Can't allocate memory for TGA colormap entries"));
		g_free(pal_buf);
		g_free(cmap);
		return NULL;
	}

	if (hdr->cmap_bpp != 15 && hdr->cmap_bpp != 16 &&
	    hdr->cmap_bpp != 24 && hdr->cmap_bpp != 32) {
		g_set_error(err, GDK_PIXBUF_ERROR, 
			    GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
			    _("Unexpected bitdepth for TGA colormap"));
		g_free(pal_buf);
		g_free(cmap->cols);
		g_free(cmap);
		return NULL;
	}

	for (n = 0; n < cmap->size; n++) {
		if ((hdr->cmap_bpp == 15) || (hdr->cmap_bpp == 16)) {
			guint16 col = p[0] + (p[1] << 8);
			p += 2;
			cmap->cols[n].b = (col >> 7) & 0xf8;
			cmap->cols[n].g = (col >> 2) & 0xf8;
			cmap->cols[n].r = col << 3;
		} else if ((hdr->cmap_bpp == 24) || (hdr->cmap_bpp == 32)) {
			cmap->cols[n].b = *p++;
			cmap->cols[n].g = *p++;
			cmap->cols[n].r = *p++;
			if (hdr->cmap_bpp == 32)
				cmap->cols[n].a = *p++;
		}
	}

	g_free(pal_buf);
	return cmap;
}

static GdkPixbuf *get_image_pseudocolor(FILE *f, TGAHeader *hdr,
					TGAColormap *cmap, gboolean rle,
					GError **err)
{
	GdkPixbuf *pbuf;
	guchar *p, color, tag;
	glong n, image_offset;
	guint count, w, h;
	gboolean alpha;

	image_offset = sizeof(TGAHeader) + hdr->infolen;
	if (!hdr->has_cmap) {
		g_set_error(err, GDK_PIXBUF_ERROR, 
			    GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
			    _("Pseudo-Color image without colormap"));
		return NULL;
	} else {
		image_offset += cmap->size * ((hdr->cmap_bpp + 7) >> 3);
	}
	if (!fseek_check(f, image_offset, SEEK_SET, err)) {
		g_set_error(err, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED,
			    _("Can't seek to image offset -- end-of-file probably encountered"));
		return NULL;
	}

	w = LE16(hdr->width);
	h = LE16(hdr->height);

	alpha = (hdr->cmap_bpp == 32);

	pbuf = get_contiguous_pixbuf (w, h, alpha);

	if (!pbuf) {
		g_set_error(err, GDK_PIXBUF_ERROR, 
			    GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
			    _("Can't allocate pixbuf"));
		return NULL;
	}
	pbuf->destroy_fn = free_buffer;
	pbuf->destroy_fn_data = NULL;
	p = pbuf->pixels;

	if (rle) {
		n = count = 0;
		for (; n < pbuf->width * pbuf->height;) {
			if (!fread_check(&tag, 1, 1, f, err)) {
				g_object_unref(pbuf);
				return NULL;
			}
			if (tag & 0x80) {
				count = (tag & 0x7f) + 1;
				n += count;
				if (!fread_check(&color, 1, 1, f, err)) {
					g_object_unref(pbuf);
					return NULL;
				}
				for (; count; count--) {
					*p++ = cmap->cols[color].r;
					*p++ = cmap->cols[color].g;
					*p++ = cmap->cols[color].b;
					if (hdr->cmap_bpp == 32)
						*p++ = cmap->cols[color].a;
				}
			} else {
				count = tag + 1;
				n += count;
				for (; count; count--) {
					if (!fread_check(&color, 1, 1, f, err)) {
						g_object_unref(pbuf);
						return NULL;
					}
					*p++ = cmap->cols[color].r;
					*p++ = cmap->cols[color].g;
					*p++ = cmap->cols[color].b;
					if (hdr->cmap_bpp == 32)
						*p++ = cmap->cols[color].a;
				}
			}
		}
	} else {
		for (n = 0; n < pbuf->width * pbuf->height; n++) {
			if (!fread_check(&color, 1, 1, f, err)) {
				g_object_unref(pbuf);
				return NULL;
			}
			*p++ = cmap->cols[color].r;
			*p++ = cmap->cols[color].g;
			*p++ = cmap->cols[color].b;
			if (hdr->cmap_bpp == 32)
				*p++ = cmap->cols[color].a;
		}
	}

	return pbuf;
}

static void swap_channels_pixbuf(GdkPixbuf *pbuf)
{
	guchar *p, swap;
	glong n;

	p = pbuf->pixels;
	for (n = 0; n < pbuf->width * pbuf->height; n++) {
		swap = p[0];
		p[0] = p[2];
		p[2] = swap;
		p += pbuf->n_channels;
	}
}

static GdkPixbuf *get_image_truecolor(FILE *f, TGAHeader *hdr,
				      gboolean rle, GError **err)
{
	GdkPixbuf *pbuf;
	guchar *p, tag;
	glong n, image_offset;
	guint32 pixel;
	guint count, w, h;
	gboolean alpha;

	image_offset = sizeof(TGAHeader) + hdr->infolen;
	/* A truecolor image shouldn't actually have a colormap. */
	if (hdr->has_cmap)
		image_offset += LE16(hdr->cmap_n_colors) * ((hdr->cmap_bpp + 7) >> 3);
	if (!fseek_check(f, image_offset, SEEK_SET, err))
		return NULL;

	w = LE16(hdr->width);
	h = LE16(hdr->height);
	
	alpha = (hdr->bpp == 32);

	pbuf = get_contiguous_pixbuf (w, h, alpha);

	if (!pbuf) {
		g_set_error(err, GDK_PIXBUF_ERROR, 
			    GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
			    _("Can't allocate pixbuf"));
		return NULL;
	}
	p = pbuf->pixels;

	if (rle) {
		n = count = 0;
		for (; n < pbuf->width * pbuf->height;) {
			if (!fread_check(&tag, 1, 1, f, err)) {
				g_object_unref(pbuf);
				return NULL;
			}
			if (tag & 0x80) {
				count = (tag & 0x7f) + 1;
				n += count;
				if (!fread_check(&pixel, pbuf->n_channels, 1, f, err)) {
					g_object_unref(pbuf);
					return NULL;
				}
				for (; count; count--) {
					g_memmove(p, &pixel, pbuf->n_channels);
					p += pbuf->n_channels;
				}
			} else {
				count = tag + 1;
				n += count;
				if (!fread_check(p, pbuf->n_channels * count, 1, f, err)) {
					g_object_unref(pbuf);
					return NULL;
				}
				p += pbuf->n_channels * count;
			}
		}
	} else {
		if (!fread_check(p, pbuf->rowstride * pbuf->height, 1, f, err)) {
			g_object_unref(pbuf);
			return NULL;
		}
	}

	swap_channels_pixbuf(pbuf);
	return pbuf;
}

static GdkPixbuf *get_image_grayscale(FILE *f, TGAHeader *hdr,
				      gboolean rle, GError **err)
{
	GdkPixbuf *pbuf;
	glong n, image_offset;
	guchar *p, color[2], tag;
	guint count, w, h;
	gboolean alpha;

	image_offset = sizeof(TGAHeader) + hdr->infolen;
	/* A grayscale image shouldn't actually have a colormap. */
	if (hdr->has_cmap)
		image_offset += LE16(hdr->cmap_n_colors) * ((hdr->cmap_bpp + 7) >> 3);
	if (!fseek_check(f, image_offset, SEEK_SET, err))
		return NULL;

	w = LE16(hdr->width);
	h = LE16(hdr->height);

	alpha = (hdr->bpp == 16);

	pbuf = get_contiguous_pixbuf (w, h, alpha);
	
	if (!pbuf) {
		g_set_error(err, GDK_PIXBUF_ERROR, 
			    GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
			    _("Can't allocate pixbuf"));
		return NULL;
	}
	p = pbuf->pixels;

	if (rle) {
		n = count = 0;
		for (; n < pbuf->width * pbuf->height;) {
			if (!fread_check(&tag, 1, 1, f, err)) {
				g_object_unref(pbuf);
				return NULL;
			}
			if (tag & 0x80) {
				count = (tag & 0x7f) + 1;
				n += count;
				if (!fread_check(color, (alpha ? 2 : 1), 1, f, err)) {
					g_object_unref(pbuf);
					return NULL;
				}
				for (; count; count--) {
					p[0] = p[1] = p[2] = color[0];
					if (alpha)
						p[3] = color[1];
					p += pbuf->n_channels;
				}
			} else {
				count = tag + 1;
				n += count;
				for (; count; count--) {
					if (!fread_check(color, (alpha ? 2 : 1), 1, f, err)) {
						g_object_unref(pbuf);
						return NULL;
					}
					p[0] = p[1] = p[2] = color[0];
					if (alpha)
						p[3] = color[1];
					p += pbuf->n_channels;
				}
			}
		}
	} else {
		for (n = 0; n < pbuf->width * pbuf->height; n++) {
			if (!fread_check(color, (alpha ? 2 : 1), 1, f, err)) {
				g_object_unref(pbuf);
				return NULL;
			}
			p[0] = p[1] = p[2] = color[0];
			if (alpha)
				p[3] = color[1];
			p += pbuf->n_channels;
		}
	}

	return pbuf;
}

static GdkPixbuf *gdk_pixbuf__tga_load(FILE *f, GError **err)
{
	TGAHeader *hdr;
	TGAColormap *cmap;
	GdkPixbuf *pbuf;

	cmap = NULL;
	hdr = get_header_from_file(f, err);
	if (!hdr)
		return NULL;
	if ((hdr->flags & TGA_INTERLEAVE_MASK) != TGA_INTERLEAVE_NONE ||
	    hdr->flags & TGA_ORIGIN_RIGHT || hdr->flags & TGA_ORIGIN_LOWER) {
		g_set_error(err, GDK_PIXBUF_ERROR, 
			    GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
			    _("Unsupported TGA image type"));
		g_free(hdr);
		return NULL;
	}
  
	if (hdr->has_cmap && ((hdr->type == TGA_TYPE_PSEUDOCOLOR) || 
			      (hdr->type == TGA_TYPE_RLE_PSEUDOCOLOR))) {
		cmap = get_colormap_from_file(f, hdr, err);
		if (!cmap) {
			g_free(hdr);
			return NULL;
		}
	}

	if (hdr->type == TGA_TYPE_PSEUDOCOLOR)
		pbuf = get_image_pseudocolor(f, hdr, cmap, FALSE, err);
	else if (hdr->type == TGA_TYPE_RLE_PSEUDOCOLOR)
		pbuf = get_image_pseudocolor(f, hdr, cmap, TRUE, err);
	else if (hdr->type == TGA_TYPE_TRUECOLOR)
		pbuf = get_image_truecolor(f, hdr, FALSE, err);
	else if (hdr->type == TGA_TYPE_RLE_TRUECOLOR)
		pbuf = get_image_truecolor(f, hdr, TRUE, err);
	else if (hdr->type == TGA_TYPE_GRAYSCALE)
		pbuf = get_image_grayscale(f, hdr, FALSE, err);
	else if (hdr->type == TGA_TYPE_RLE_GRAYSCALE)
		pbuf = get_image_grayscale(f, hdr, TRUE, err);
	else {
		g_set_error(err, GDK_PIXBUF_ERROR, 
			    GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
			    _("Unsupported TGA image type"));
		pbuf = NULL;
	}
  
	if (cmap) {
		g_free(cmap->cols);
		g_free(cmap);
	}
	g_free(hdr);
  
	return pbuf;
}

void
gdk_pixbuf__tga_fill_vtable (GdkPixbufModule *module)
{
	module->load = gdk_pixbuf__tga_load;
	module->begin_load = gdk_pixbuf__tga_begin_load;
	module->stop_load = gdk_pixbuf__tga_stop_load;
	module->load_increment = gdk_pixbuf__tga_load_increment;
}
