/*
 * GdkPixbuf library - PCX image loader
 *
 * Copyright (C) 2003 Josh A. Beam
 *
 * Authors: Josh A. Beam <josh@joshbeam.com>
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"
#include <stdio.h>
#include <string.h>
#include "gdk-pixbuf-private.h"

#undef PCX_DEBUG

#define PCX_TASK_DONE 0
#define PCX_TASK_LOAD_HEADER 1
#define PCX_TASK_LOAD_DATA 2
#define PCX_TASK_LOAD_PALETTE 3

struct pcx_header {
	guint8 manufacturer;
	guint8 version;
	guint8 encoding;
	guint8 bitsperpixel;
	gint16 xmin;
	gint16 ymin;
	gint16 xmax;
	gint16 ymax;
	guint16 horizdpi;
	guint16 vertdpi;
	guint8 palette[48];
	guint8 reserved;
	guint8 colorplanes;
	guint16 bytesperline;
	guint16 palettetype;
	guint16 hscrsize;
	guint16 vscrsize;
	guint8 filler[54];
};

struct pcx_context {
	GdkPixbuf *pixbuf;
	gint rowstride;

	GdkPixbufModuleSizeFunc size_func;
	GdkPixbufModuleUpdatedFunc updated_func;
	GdkPixbufModulePreparedFunc prepared_func;
	gpointer user_data;

	guchar current_task;

	gboolean header_loaded;
	struct pcx_header *header;
	guint bpp;
	gint width, height;
	guint num_planes;
	guint bytesperline;

	guchar *buf;
	guint buf_size;
	guint buf_pos;
	guchar *data;
	guchar *line;
	guint current_line;
	guchar *p_data;
};

/*
 * set context's image information based on the header
 */
static void
fill_pcx_context(struct pcx_context *context)
{
	struct pcx_header *header = context->header;

	context->bpp = header->bitsperpixel;
	context->width = header->xmax - header->xmin + 1;
	context->height = header->ymax - header->ymin + 1;
	context->num_planes = header->colorplanes;
	context->bytesperline = header->bytesperline;

	if(header->version == 5 && context->bpp == 8 && context->num_planes == 3)
		context->bpp = 24;
}

static void
free_pcx_context(struct pcx_context *context, gboolean unref_pixbuf)
{
	g_free(context->header);
	g_free(context->buf);
	if(unref_pixbuf && context->pixbuf)
		g_object_unref(context->pixbuf);
	g_free(context->line);
	g_free(context->p_data);

	g_free(context);
}

/*
 * read each plane of a single scanline. store_planes is
 * the number of planes that can be stored in the planes array.
 * data is the pointer to the block of memory to read
 * from, size is the length of that data, and line_bytes
 * is where the number of bytes read will be stored.
 */
static gboolean
read_scanline_data(guchar *data, guint size, guchar *planes[],
                   guint store_planes, guint num_planes, guint bytesperline,
                   guint *line_bytes)
{
	guint i, j;
	guint p, count;
	guint d = 0;
	guint8 byte;

	for(p = 0; p < num_planes; p++) {
		for(i = 0; i < bytesperline;) { /* i incremented when line byte set */
			if(d >= size)
				return FALSE;
			byte = data[d++];

			if(byte >> 6 == 0x3) {
				count = byte & ~(0x3 << 6);
				if(count == 0)
					return FALSE;
				if(d >= size)
					return FALSE;
				byte = data[d++];
			} else {
				count = 1;
			}

			for(j = 0; j < count; j++) {
				if(p < store_planes)
					planes[p][i++] = byte;
				else
					i++;

				if(i >= bytesperline) {
					p++;
					if(p >= num_planes) {
						*line_bytes = d;
						return TRUE;
					}
					i = 0;
				}
			}
		}
	}

	*line_bytes = d; /* number of bytes read for scanline */
	return TRUE;
}

static gpointer
gdk_pixbuf__pcx_begin_load(GdkPixbufModuleSizeFunc size_func,
                           GdkPixbufModulePreparedFunc prepared_func,
                           GdkPixbufModuleUpdatedFunc updated_func,
                           gpointer user_data, GError **error)
{
	struct pcx_context *context;

	context = g_new0(struct pcx_context, 1);
	if(!context)
		return NULL;

	context->header = g_try_malloc(sizeof(struct pcx_header));
	if(!context->header) {
		g_free(context);
		g_set_error_literal(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY, _("Couldn't allocate memory for header"));
		return NULL;
	}

	context->size_func = size_func;
	context->updated_func = updated_func;
	context->prepared_func = prepared_func;
	context->user_data = user_data;

	context->current_task = PCX_TASK_LOAD_HEADER;

	context->buf = g_try_malloc(sizeof(guchar) * 512);
	if(!context->buf) {
		g_free(context->header);
		g_free(context);
		g_set_error_literal(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY, _("Couldn't allocate memory for context buffer"));
		return NULL;
	}
	context->buf_size = 512;

	context->header_loaded = FALSE;

	return context;
}

static gboolean
pcx_resize_context_buf(struct pcx_context *context, guint size)
{
	guchar *new_buf;

	new_buf = g_try_realloc(context->buf, size);
	if(!new_buf)
		return FALSE;

	context->buf = new_buf;
	context->buf_size = size;
	return TRUE;
}

/*
 * remove a number of bytes (specified by size) from the
 * beginning of a context's buf
 */
static gboolean
pcx_chop_context_buf(struct pcx_context *context, guint size)
{
	guint i, j;

	if (size > context->buf_pos)
		return FALSE;
	else if (size == 0)
		return TRUE;

	for (i = 0, j = size; j < context->buf_pos; i++, j++)
		context->buf[i] = context->buf[j];

	context->buf_pos -= size;

	return TRUE;
}

static guchar
read_pixel_1(guchar *data, guint offset)
{
	guchar retval;
	guint bit_offset;

	if(!(offset % 8)) {
		offset /= 8;
		retval = data[offset] >> 7;
	} else {
		bit_offset = offset % 8;
		offset -= bit_offset;
		offset /= 8;
		retval = (data[offset] >> (7 - bit_offset)) & 0x1;
	}

	return retval;
}

static guchar
read_pixel_4(guchar *data, guint offset)
{
	guchar retval;

	if(!(offset % 2)) {
		offset /= 2;
		retval = data[offset] >> 4;
	} else {
		offset--;
		offset /= 2;
		retval = data[offset] & 0xf;
	}

	return retval;
}

static gboolean
pcx_increment_load_data_1(struct pcx_context *context)
{
	guint i;
	guchar *planes[4];
	guint line_bytes;
	guint store_planes;

	if(context->num_planes == 4) {
		planes[0] = context->line;
		planes[1] = planes[0] + context->bytesperline;
		planes[2] = planes[1] + context->bytesperline;
		planes[3] = planes[2] + context->bytesperline;
		store_planes = 4;
	} else if(context->num_planes == 3) {
		planes[0] = context->line;
		planes[1] = planes[0] + context->bytesperline;
		planes[2] = planes[1] + context->bytesperline;
		store_planes = 3;
	} else if(context->num_planes == 2) {
		planes[0] = context->line;
		planes[1] = planes[0] + context->bytesperline;
		store_planes = 2;
	} else if(context->num_planes == 1) {
		planes[0] = context->line;
		store_planes = 1;
	} else {
		return FALSE;
	}

	while(read_scanline_data(context->buf, context->buf_pos, planes, store_planes, context->num_planes, context->bytesperline, &line_bytes)) {
		pcx_chop_context_buf(context, line_bytes);

		for(i = 0; i < context->width; i++) {
			guchar p;

			if(context->num_planes == 4) {
				p = read_pixel_1(planes[3], i);
				p <<= 1;
				p |= read_pixel_1(planes[2], i);
				p <<= 1;
				p |= read_pixel_1(planes[1], i);
				p <<= 1;
				p |= read_pixel_1(planes[0], i);
			} else if(context->num_planes == 3) {
				p = read_pixel_1(planes[2], i);
				p <<= 1;
				p |= read_pixel_1(planes[1], i);
				p <<= 1;
				p |= read_pixel_1(planes[0], i);
			} else if(context->num_planes == 2) {
				p = read_pixel_1(planes[1], i);
				p <<= 1;
				p |= read_pixel_1(planes[0], i);
			} else if(context->num_planes == 1) {
				p = read_pixel_1(planes[0], i);
			} else {
				return FALSE;
			}
			p &= 0xf;
			context->data[context->current_line * context->rowstride + i * 3 + 0] = context->header->palette[p * 3 + 0];
			context->data[context->current_line * context->rowstride + i * 3 + 1] = context->header->palette[p * 3 + 1];
			context->data[context->current_line * context->rowstride + i * 3 + 2] = context->header->palette[p * 3 + 2];
		}

		if(context->updated_func)
			context->updated_func(context->pixbuf, 0, context->current_line, context->width, 1, context->user_data);

		context->current_line++;

		if(context->current_line == context->height) {
			context->current_task = PCX_TASK_DONE;
			return TRUE;
		}
	}

	return TRUE;
}

static gboolean
pcx_increment_load_data_2(struct pcx_context *context)
{
	guint i;
	guchar *planes[1];
	guint line_bytes;
	guint shift, h;

	planes[0] = context->line;

	while(read_scanline_data(context->buf, context->buf_pos, planes, 1, context->num_planes, context->bytesperline, &line_bytes)) {
		pcx_chop_context_buf(context, line_bytes);

		for(i = 0; i < context->width; i++) {
			shift = 6 - 2 * (i % 4);
			h = (planes[0][i / 4] >> shift) & 0x3;
			context->data[context->current_line * context->rowstride + i * 3 + 0] = context->header->palette[h * 3 + 0];
			context->data[context->current_line * context->rowstride + i * 3 + 1] = context->header->palette[h * 3 + 1];
			context->data[context->current_line * context->rowstride + i * 3 + 2] = context->header->palette[h * 3 + 2];
		}

		if(context->updated_func)
			context->updated_func(context->pixbuf, 0, context->current_line, context->width, 1, context->user_data);

		context->current_line++;

		if(context->current_line == context->height) {
			context->current_task = PCX_TASK_DONE;
			return TRUE;
		}
	}

	return TRUE;
}

static gboolean
pcx_increment_load_data_4(struct pcx_context *context)
{
	guint i;
	guchar *planes[1];
	guint line_bytes;

	planes[0] = context->line;

	while(read_scanline_data(context->buf, context->buf_pos, planes, 1, context->num_planes, context->bytesperline, &line_bytes)) {
		pcx_chop_context_buf(context, line_bytes);

		for(i = 0; i < context->width; i++) {
			guchar p;

			p = read_pixel_4(planes[0], i) & 0xf;
			context->data[context->current_line * context->rowstride + i * 3 + 0] = context->header->palette[p * 3 + 0];
			context->data[context->current_line * context->rowstride + i * 3 + 1] = context->header->palette[p * 3 + 1];
			context->data[context->current_line * context->rowstride + i * 3 + 2] = context->header->palette[p * 3 + 2];
		}

		if(context->updated_func)
			context->updated_func(context->pixbuf, 0, context->current_line, context->width, 1, context->user_data);

		context->current_line++;

		if(context->current_line == context->height) {
			context->current_task = PCX_TASK_DONE;
			return TRUE;
		}
	}

	return TRUE;
}

/*
 * for loading 8-bit pcx images, we keep a buffer containing
 * each pixel's palette number; once we've loaded each scanline,
 * we wait for loading to stop and call pcx_load_palette_8,
 * which finds the palette at the end of the pcx data and sets the
 * RGB data.
 */
static gboolean
pcx_increment_load_data_8(struct pcx_context *context)
{
	guint i;
	guchar *planes[1];
	guint line_bytes;

	planes[0] = context->line;

	while(read_scanline_data(context->buf, context->buf_pos, planes, 1, context->num_planes, context->bytesperline, &line_bytes)) {
		pcx_chop_context_buf(context, line_bytes);

		for(i = 0; i < context->width; i++)
			context->p_data[context->current_line * context->width + i + 0] = planes[0][i];

		context->current_line++;

		if(context->current_line == context->height) {
			context->current_task = PCX_TASK_LOAD_PALETTE;
			return TRUE;
		}
	}

	return TRUE;
}

/*
 * read the palette and set the RGB data
 */
static gboolean
pcx_load_palette_8(struct pcx_context *context)
{
	guint i, j;

	if(context->current_line < context->height)
		return FALSE;

	if(context->buf_pos >= 769) {
		guchar *palette = context->buf + (context->buf_pos - 769);

		if(palette[0] == 12) {
			palette++;
			for(i = 0; i < context->height; i++) {
				for(j = 0; j < context->width; j++) {
					context->data[i * context->rowstride + j * 3 + 0] = palette[(context->p_data[i * context->width + j]) * 3 + 0];
					context->data[i * context->rowstride + j * 3 + 1] = palette[(context->p_data[i * context->width + j]) * 3 + 1];
					context->data[i * context->rowstride + j * 3 + 2] = palette[(context->p_data[i * context->width + j]) * 3 + 2];
				}

				if(context->updated_func)
					context->updated_func(context->pixbuf, 0, i, context->width, 1, context->user_data);
			}

#ifdef PCX_DEBUG
			g_print("read palette\n");
#endif

			context->current_task = PCX_TASK_DONE;
			return TRUE;
		} else {
#ifdef PCX_DEBUG
			g_print("this ain't a palette\n");
#endif
			return FALSE;
		}
	}

	return FALSE;
}

/*
 * in 24-bit images, each scanline has three color planes
 * for red, green, and blue, respectively.
 */
static gboolean
pcx_increment_load_data_24(struct pcx_context *context)
{
	guint i;
	guchar *planes[3];
	guint line_bytes;

	planes[0] = context->line;
	planes[1] = planes[0] + context->bytesperline;
	planes[2] = planes[1] + context->bytesperline;

	while(read_scanline_data(context->buf, context->buf_pos, planes, 3, context->num_planes, context->bytesperline, &line_bytes)) {
		pcx_chop_context_buf(context, line_bytes);

		for(i = 0; i < context->width; i++) {
			context->data[context->current_line * context->rowstride + i * 3 + 0] = planes[0][i];
			context->data[context->current_line * context->rowstride + i * 3 + 1] = planes[1][i];
			context->data[context->current_line * context->rowstride + i * 3 + 2] = planes[2][i];
		}

		if(context->updated_func)
			context->updated_func(context->pixbuf, 0, context->current_line, context->width, 1, context->user_data);

		context->current_line++;

		if(context->current_line == context->height) {
			context->current_task = PCX_TASK_DONE;
			return TRUE;
		}
	}

	return TRUE;
}

static gboolean
gdk_pixbuf__pcx_load_increment(gpointer data, const guchar *buf, guint size,
                               GError **error)
{
	struct pcx_context *context = (struct pcx_context *)data;
	struct pcx_header *header;
	guint i;
	gboolean retval = TRUE;

	/* if context's buf isn't large enough to hold its current data plus the passed buf, increase its size */
	if(context->buf_pos + size > context->buf_size) {
		if(!pcx_resize_context_buf(context, sizeof(guchar) * (context->buf_pos + size))) {
			g_set_error_literal(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY, _("Couldn't allocate memory for context buffer"));
			return FALSE;
		}
	}

	for(i = 0; i < size; i++)
		context->buf[context->buf_pos++] = buf[i];

	if(context->current_task == PCX_TASK_LOAD_HEADER) {
		if(!context->header_loaded && context->buf_pos > sizeof(struct pcx_header)) { /* set header */
			gint width, height;

			memcpy(context->header, context->buf, sizeof(struct pcx_header));
			pcx_chop_context_buf(context, sizeof(struct pcx_header));
			header = context->header;

			/* convert the multi-byte header variables that will be used */
			header->xmin = GINT16_FROM_LE(header->xmin);
			header->ymin = GINT16_FROM_LE(header->ymin);
			header->xmax = GINT16_FROM_LE(header->xmax);
			header->ymax = GINT16_FROM_LE(header->ymax);
			header->bytesperline = GUINT16_FROM_LE(header->bytesperline);

#ifdef PCX_DEBUG
			g_print ("Manufacturer %d\n"
			         "Version %d\n"
			         "Encoding %d\n"
				 "Bits/Pixel %d\n"
				 "Planes %d\n"
				 "Palette %d\n", 
				 header->manufacturer, header->version, 
				 header->encoding, header->bitsperpixel,
				 header->colorplanes, header->palettetype);
#endif

			context->header_loaded = TRUE;
			fill_pcx_context(context);

			width = context->width;
			height = context->height;
			if(width <= 0 || height <= 0) {
				g_set_error_literal(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_CORRUPT_IMAGE, _("Image has invalid width and/or height"));
				return FALSE;
			}
			if (context->size_func)
			  {
			    (*context->size_func) (&width, &height, context->user_data);
			    if (width == 0 || height == 0)
			      return TRUE;
			  }

			switch(context->bpp) {
				default:
					g_set_error_literal(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE, _("Image has unsupported bpp"));
					return FALSE;
					break;
				case 1:
					if(context->num_planes < 1 || context->num_planes > 4) {
						g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE, _("Image has unsupported number of %d-bit planes"), 1);
						return FALSE;
					}
					break;
				case 2:
				case 4:
				case 8:
					if(context->num_planes != 1) {
					  g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE, _("Image has unsupported number of %d-bit planes"), (gint)context->bpp);
						return FALSE;
					}
					break;
				case 24:
					break; /* context's bpp is set to 24 if there are three 8-bit planes */
			}

#ifdef PCX_DEBUG
			g_print("io-pcx: header loaded\n");
			g_print("bpp: %u\n", context->bpp);
			g_print("dimensions: %ux%u\n", context->width, context->height);
#endif

			context->pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, context->width, context->height);
			if(!context->pixbuf) {
				g_set_error_literal(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY, _("Couldn't create new pixbuf"));
				return FALSE;
			}
			context->data = gdk_pixbuf_get_pixels(context->pixbuf);
			context->rowstride = gdk_pixbuf_get_rowstride(context->pixbuf);

			context->line = g_try_malloc(sizeof(guchar) * context->bytesperline * context->num_planes);
			if(!context->line) {
				g_set_error_literal(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY, _("Couldn't allocate memory for line data"));
				return FALSE;
			}

			if(context->bpp == 8) {
				context->p_data = g_try_malloc(sizeof(guchar) * context->width * context->height);
				if(!context->p_data) {
					g_set_error_literal(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY, _("Couldn't allocate memory for PCX image"));
					return FALSE;
				}
			}

			if(context->prepared_func)
			        context->prepared_func(context->pixbuf, NULL, context->user_data);

			context->current_task = PCX_TASK_LOAD_DATA;
		}

		retval = TRUE;
	}

	if(context->current_task == PCX_TASK_LOAD_DATA) {
		switch(context->bpp) {
			default:
				g_set_error_literal(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE, _("Image has unsupported bpp"));
				retval = FALSE;
				break;
			case 1:
				retval = pcx_increment_load_data_1(context);
				break;
			case 2:
				retval = pcx_increment_load_data_2(context);
				break;
			case 4:
				retval = pcx_increment_load_data_4(context);
				break;
			case 8:
				retval = pcx_increment_load_data_8(context);
				break;
			case 24:
				retval = pcx_increment_load_data_24(context);
				break;
		}
	}

	return retval;
}

static gboolean
gdk_pixbuf__pcx_stop_load(gpointer data, GError **error)
{
	struct pcx_context *context = (struct pcx_context *)data;

	if(context->current_line != context->height) {
		g_set_error_literal(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, _("Didn't get all lines of PCX image"));
		free_pcx_context(context, FALSE);
		return FALSE;
	}

	if(context->current_task == PCX_TASK_LOAD_PALETTE) {
		if(!pcx_load_palette_8(context)) {
			g_set_error_literal(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, _("No palette found at end of PCX data"));
			free_pcx_context(context, FALSE);
			return FALSE;
		}
	}

	free_pcx_context(context, FALSE);

	return TRUE;
}

#ifndef INCLUDE_pcx
#define MODULE_ENTRY(function) G_MODULE_EXPORT void function
#else
#define MODULE_ENTRY(function) void _gdk_pixbuf__pcx_ ## function
#endif

MODULE_ENTRY (fill_vtable) (GdkPixbufModule *module)
{
	module->begin_load = gdk_pixbuf__pcx_begin_load;
	module->stop_load = gdk_pixbuf__pcx_stop_load;
	module->load_increment = gdk_pixbuf__pcx_load_increment;
}

MODULE_ENTRY (fill_info) (GdkPixbufFormat *info)
{
	static const GdkPixbufModulePattern signature[] = {
		{ "\x0a \x01", NULL, 100 },
		{ "\x0a\x02\x01", NULL, 100 },
		{ "\x0a\x03\x01", NULL, 100 },
		{ "\x0a\x04\x01", NULL, 100 },
		{ "\x0a\x05\x01", NULL, 100 },
		{ NULL, NULL, 0 }
	};
	static const gchar *mime_types[] = {
		"image/x-pcx",
		NULL,
	};
	static const gchar *extensions[] = {
		"pcx",
		NULL,
	};

	info->name = "pcx";
	info->signature = (GdkPixbufModulePattern *) signature;
	info->description = NC_("image format", "PCX");
	info->mime_types = (gchar **) mime_types;
	info->extensions = (gchar **) extensions;
	info->flags = GDK_PIXBUF_FORMAT_THREADSAFE;
	info->license = "LGPL";
}
