//-----------------------------------------------------------------------------
//
// ImageLib Sources
// Copyright (C) 2000-2002 by Denton Woods
// Last modified: 09/01/2003 <--Y2K Compliant! =]
//
// Filename: src-IL/src/il_pcx.c
//
// Description: Reads and writes from/to a .pcx file.
//
//-----------------------------------------------------------------------------


#include "il_internal.h"
#ifndef IL_NO_PCX
#include "il_pcx.h"
#include "il_manip.h"


//! Checks if the file specified in FileName is a valid .pcx file.
ILboolean ilIsValidPcx(ILconst_string FileName) {
	ILHANDLE	PcxFile;
	ILboolean	bPcx = IL_FALSE;

	if (!iCheckExtension(FileName, IL_TEXT("pcx"))) {
		ilSetError(IL_INVALID_EXTENSION);
		return bPcx;
	}

	PcxFile = iopenr(FileName);
	if (PcxFile == NULL) {
		ilSetError(IL_COULD_NOT_OPEN_FILE);
		return bPcx;
	}

	bPcx = ilIsValidPcxF(PcxFile);
	icloser(PcxFile);

	return bPcx;
}


//! Checks if the ILHANDLE contains a valid .pcx file at the current position.
ILboolean ilIsValidPcxF(ILHANDLE File) {
	ILuint		FirstPos;
	ILboolean	bRet;

	iSetInputFile(File);
	FirstPos = itell();
	bRet = iIsValidPcx();
	iseek(FirstPos, IL_SEEK_SET);

	return bRet;
}


//! Checks if Lump is a valid .pcx lump.
ILboolean ilIsValidPcxL(const ILvoid *Lump, ILuint Size) {
	iSetInputLump(Lump, Size);
	return iIsValidPcx();
}


// Internal function obtain the .pcx header from the current file.
ILboolean iGetPcxHead(PCXHEAD *Head) {

	Head->Manufacturer = igetc();

	Head->Version = igetc();

	Head->Encoding = igetc();

	Head->Bpp = igetc();

	Head->Xmin = GetLittleUShort();

	Head->Ymin = GetLittleUShort();

	Head->Xmax = GetLittleUShort();

	Head->Ymax = GetLittleUShort();

	Head->HDpi = GetLittleUShort();

	Head->VDpi = GetLittleUShort();

	iread(Head->ColMap, 1, 48);

	Head->Reserved = igetc();

	Head->NumPlanes = igetc();

	Head->Bps = GetLittleUShort();

	Head->PaletteInfo = GetLittleUShort();

	Head->HScreenSize = GetLittleUShort();

	Head->VScreenSize = GetLittleUShort();

	iread(Head->Filler, 1, 54);


	return IL_TRUE;
}


// Internal function to get the header and check it.
ILboolean iIsValidPcx() {
	PCXHEAD Head;

	if (!iGetPcxHead(&Head))
		return IL_FALSE;
	iseek(-(ILint)sizeof(PCXHEAD), IL_SEEK_CUR);

	return iCheckPcx(&Head);
}


// Internal function used to check if the HEADER is a valid .pcx header.
// Should we also do a check on Header->Bpp?
ILboolean iCheckPcx(PCXHEAD *Header) {
	ILuint	Test;

	//	Got rid of the Reserved check, because I've seen some .pcx files with invalid values in it.
	if (Header->Manufacturer != 10 || Header->Encoding != 1/* || Header->Reserved != 0*/)
		return IL_FALSE;

	// Try to support all pcx versions, as they only differ in allowed formats...
	// Let's hope it works.
	if(Header->Version != 5 && Header->Version != 0 && Header->Version != 2 &&
		 Header->VDpi != 3 && Header->VDpi != 4)
		return IL_FALSE;

	// See if the padding size is correct
	Test = Header->Xmax - Header->Xmin + 1;
	if (Header->Bpp >= 8) {
		if (Test & 1) {
			if (Header->Bps != Test + 1)
				return IL_FALSE;
		}
		else {
			if (Header->Bps != Test)  // No padding
				return IL_FALSE;
		}
	}

	/* for (i = 0; i < 54; i++) { useless check
		if (Header->Filler[i] != 0)
			return IL_FALSE;
	} */

	return IL_TRUE;
}


//! Reads a .pcx file
ILboolean ilLoadPcx(ILconst_string FileName) {
	ILHANDLE	PcxFile;
	ILboolean	bPcx = IL_FALSE;

	PcxFile = iopenr(FileName);
	if (PcxFile == NULL) {
		ilSetError(IL_COULD_NOT_OPEN_FILE);
		return bPcx;
	}

	bPcx = ilLoadPcxF(PcxFile);
	icloser(PcxFile);

	return bPcx;
}


//! Reads an already-opened .pcx file
ILboolean ilLoadPcxF(ILHANDLE File) {
	ILuint		FirstPos;
	ILboolean	bRet;

	iSetInputFile(File);
	FirstPos = itell();
	bRet = iLoadPcxInternal();
	iseek(FirstPos, IL_SEEK_SET);

	return bRet;
}


//! Reads from a memory "lump" that contains a .pcx
ILboolean ilLoadPcxL(const ILvoid *Lump, ILuint Size) {
	iSetInputLump(Lump, Size);
	return iLoadPcxInternal();
}


// Internal function used to load the .pcx.
ILboolean iLoadPcxInternal() {
	PCXHEAD	Header;
	ILboolean bPcx = IL_FALSE;

	if (iCurImage == NULL) {
		ilSetError(IL_ILLEGAL_OPERATION);
		return bPcx;
	}

	if (!iGetPcxHead(&Header))
		return IL_FALSE;
	if (!iCheckPcx(&Header)) {
		ilSetError(IL_INVALID_FILE_HEADER);
		return IL_FALSE;
	}

	bPcx = iUncompressPcx(&Header);

	ilFixImage();

	return bPcx;
}


// Internal function to uncompress the .pcx (all .pcx files are rle compressed)
ILboolean iUncompressPcx(PCXHEAD *Header) {
	//changed decompression loop 2003-09-01
	//From the pcx spec: "There should always
	//be a decoding break at the end of each scan line.
	//But there will not be a decoding break at the end of
	//each plane within each scan line."
	//This is now handled correctly (hopefully ;) )

	ILubyte	ByteHead, Colour, *ScanLine /* For all planes */;
	ILuint ScanLineSize;
	ILuint	c, i, x, y;

	if (Header->Bpp < 8) {
		/*ilSetError(IL_FORMAT_NOT_SUPPORTED);
		return IL_FALSE;*/
		return iUncompressSmall(Header);
	}

	if (!ilTexImage(Header->Xmax - Header->Xmin + 1, Header->Ymax - Header->Ymin + 1, 1, Header->NumPlanes, 0, IL_UNSIGNED_BYTE, NULL))
		return IL_FALSE;
	iCurImage->Origin = IL_ORIGIN_UPPER_LEFT;

	switch (iCurImage->Bpp)
	{
		case 1:
			iCurImage->Format = IL_COLOUR_INDEX;
			iCurImage->Pal.PalType = IL_PAL_RGB24;
			iCurImage->Pal.PalSize = 256 * 3;  // Need to find out for sure...
			iCurImage->Pal.Palette = (ILubyte*)ialloc(iCurImage->Pal.PalSize);
			if (iCurImage->Pal.Palette == NULL) {
				return IL_FALSE;
			}
			break;
		//case 2:  // No 16-bit images in the pcx format!
		case 3:
			iCurImage->Format = IL_RGB;
			iCurImage->Pal.Palette = NULL;
			iCurImage->Pal.PalSize = 0;
			iCurImage->Pal.PalType = IL_PAL_NONE;
			break;
		case 4:
			iCurImage->Format = IL_RGBA;
			iCurImage->Pal.Palette = NULL;
			iCurImage->Pal.PalSize = 0;
			iCurImage->Pal.PalType = IL_PAL_NONE;
			break;

		default:
			ilSetError(IL_ILLEGAL_FILE_VALUE);
			return IL_FALSE;
	}

	ScanLineSize = iCurImage->Bpp*Header->Bps;
	ScanLine = (ILubyte*)ialloc(ScanLineSize);
	if (ScanLine == NULL) {
		return IL_FALSE;
	}


	//changed 2003-09-01
	//having the decoding code twice is error-prone,
	//so I made iUnCache() smart enough to grasp
	//if iPreCache() wasn't called and call it
	//anyways.
	if (iGetHint(IL_MEM_SPEED_HINT) == IL_FASTEST)
		iPreCache(iCurImage->SizeOfData / 4);

	for (y = 0; y < iCurImage->Height; y++) {
		x = 0;
		//read scanline
		while (x < ScanLineSize) {
			if (iread(&ByteHead, 1, 1) != 1) {
				iUnCache();
				goto file_read_error;
			}
			if ((ByteHead & 0xC0) == 0xC0) {
				ByteHead &= 0x3F;
				if (iread(&Colour, 1, 1) != 1) {
					iUnCache();
					goto file_read_error;
				}
				if (x + ByteHead > ScanLineSize) {
					iUnCache();
					goto file_read_error;
				}
				for (i = 0; i < ByteHead; i++) {
					ScanLine[x++] = Colour;
				}
			}
			else {
				ScanLine[x++] = ByteHead;
			}
		}

		//convert plane-separated scanline into index, rgb or rgba pixels.
		//there might be a padding byte at the end of each scanline...
		for (x = 0; x < iCurImage->Width; x++) {
			for(c = 0; c < iCurImage->Bpp; c++) {
				iCurImage->Data[y * iCurImage->Bps + x * iCurImage->Bpp + c] =
						ScanLine[x + c * Header->Bps];
			}
		}
	}

	iUnCache();

	// Read in the palette
	if (Header->Version == 5 && iCurImage->Bpp == 1) {
		x = itell();
		if (iread(&ByteHead, 1, 1) == 0) {  // If true, assume that we have a luminance image.
			ilGetError();  // Get rid of the IL_FILE_READ_ERROR.
			iCurImage->Format = IL_LUMINANCE;
			if (iCurImage->Pal.Palette)
				ifree(iCurImage->Pal.Palette);
			iCurImage->Pal.PalSize = 0;
			iCurImage->Pal.PalType = IL_PAL_NONE;
		}
		else {
			if (ByteHead != 12)  // Some Quake2 .pcx files don't have this byte for some reason.
				iseek(-1, IL_SEEK_CUR);
			if (iread(iCurImage->Pal.Palette, 1, iCurImage->Pal.PalSize) != iCurImage->Pal.PalSize)
				goto file_read_error;
		}
	}

	ifree(ScanLine);

	return IL_TRUE;

file_read_error:
	ifree(ScanLine);

	//added 2003-09-01
	ilSetError(IL_FILE_READ_ERROR);
	return IL_FALSE;
}


ILboolean iUncompressSmall(PCXHEAD *Header) {
	ILuint	i = 0, j, k, c, d, x, y, Bps;
	ILubyte	HeadByte, Colour, Data = 0, *ScanLine;

	if (!ilTexImage(Header->Xmax - Header->Xmin + 1, Header->Ymax - Header->Ymin + 1, 1, 1, 0, IL_UNSIGNED_BYTE, NULL)) {
		return IL_FALSE;
	}
	iCurImage->Origin = IL_ORIGIN_UPPER_LEFT;

	switch (Header->NumPlanes)
	{
		case 1:
			iCurImage->Format = IL_LUMINANCE;
			break;
		case 4:
			iCurImage->Format = IL_COLOUR_INDEX;
			break;
		default:
			ilSetError(IL_ILLEGAL_FILE_VALUE);
			return IL_FALSE;
	}

	if (Header->NumPlanes == 1 && Header->Bpp == 1) {
		for (j = 0; j < iCurImage->Height; j++) {
			i = 0; //number of written pixels
			while (i < iCurImage->Width) {
				if (iread(&HeadByte, 1, 1) != 1)
					return IL_FALSE;
				if (HeadByte >= 192) {
					HeadByte -= 192;
					if (iread(&Data, 1, 1) != 1)
						return IL_FALSE;

					for (c = 0; c < HeadByte; c++) {
						k = 128;
						for (d = 0; d < 8 && i < iCurImage->Width; d++) {
							iCurImage->Data[j * iCurImage->Width + i++] = ((Data & k) != 0 ? 255 : 0);
							k >>= 1;
						}
					}
				}
				else {
					k = 128;
					for (c = 0; c < 8 && i < iCurImage->Width; c++) {
						iCurImage->Data[j * iCurImage->Width + i++] = ((HeadByte & k) != 0 ? 255 : 0);
						k >>= 1;
					}
				}
			}

			//if(Data != 0)
			//changed 2003-09-01:
			//There has to be an even number of bytes per line in a pcx.
			//One byte can hold up to 8 bits, so Width/8 bytes
			//are needed to hold a 1 bit per pixel image line.
			//If Width/8 is even no padding is needed,
			//one pad byte has to be read otherwise.
			//(let's hope the above is true ;-))
			if(!((iCurImage->Width >> 3) & 0x1))
				igetc();	// Skip pad byte
		}
	}
	else if (Header->NumPlanes == 4 && Header->Bpp == 1){   // 4-bit images
		//changed decoding 2003-09-10 (was buggy)...could need a speedup

		Bps = Header->Bps * Header->NumPlanes * 8;
		iCurImage->Pal.Palette = (ILubyte*)ialloc(16 * 3);  // Size of palette always (48 bytes).
		ScanLine = (ILubyte*)ialloc(Bps);
		if (iCurImage->Pal.Palette == NULL || ScanLine == NULL) {
			ifree(ScanLine);
			ifree(iCurImage->Pal.Palette);
			return IL_FALSE;
		}
		memcpy(iCurImage->Pal.Palette, Header->ColMap, 16 * 3);
		iCurImage->Pal.PalSize = 16 * 3;
		iCurImage->Pal.PalType = IL_PAL_RGB24;

		memset(iCurImage->Data, 0, iCurImage->SizeOfData);

		if (iGetHint(IL_MEM_SPEED_HINT) == IL_FASTEST)
			iPreCache(iCurImage->SizeOfData / 4);
		for (y = 0; y < iCurImage->Height; y++) {
			x = 0;
			while (x < Bps) {
				if (iread(&HeadByte, 1, 1) != 1) {
					iUnCache();
					ifree(ScanLine);
					return IL_FALSE;
				}
				if ((HeadByte & 0xC0) == 0xC0) {
					HeadByte &= 0x3F;
					if (iread(&Colour, 1, 1) != 1) {
						iUnCache();
						ifree(ScanLine);
						return IL_FALSE;
					}
					for (i = 0; i < HeadByte; i++) {
						k = 128;
						for (j = 0; j < 8 && x < Bps; j++) {
							ScanLine[x++] = (Colour & k)?1:0;
							k >>= 1;
						}
					}
				}
				else {
					k = 128;
					for (j = 0; j < 8 && x < Bps; j++) {
						ScanLine[x++] = (HeadByte & k)?1:0;
						k >>= 1;
					}
				}
			}

			for (x = 0; x < iCurImage->Width; x++) {  // 'Cleverly' ignores the pad bytes. ;)
				for(c = 0; c < Header->NumPlanes; c++)
					iCurImage->Data[y * iCurImage->Width + x] |= ScanLine[x + c*Header->Bps*8] << c;
			}
		}
		iUnCache();
		ifree(ScanLine);
	}
	else {
		ilSetError(IL_FORMAT_NOT_SUPPORTED);
		return IL_FALSE;
	}

	return IL_TRUE;
}


//! Writes a .pcx file
ILboolean ilSavePcx(ILconst_string FileName) {
	ILHANDLE	PcxFile;
	ILboolean	bPcx = IL_FALSE;

	if (ilGetBoolean(IL_FILE_MODE) == IL_FALSE) {
		if (iFileExists(FileName)) {
			ilSetError(IL_FILE_ALREADY_EXISTS);
			return IL_FALSE;
		}
	}

	PcxFile = iopenw(FileName);
	if (PcxFile == NULL) {
		ilSetError(IL_COULD_NOT_OPEN_FILE);
		return bPcx;
	}

	bPcx = ilSavePcxF(PcxFile);
	iclosew(PcxFile);

	return bPcx;
}


//! Writes a .pcx to an already-opened file
ILboolean ilSavePcxF(ILHANDLE File) {
	iSetOutputFile(File);
	return iSavePcxInternal();
}


//! Writes a .pcx to a memory "lump"
ILboolean ilSavePcxL(ILvoid *Lump, ILuint Size) {
	iSetOutputLump(Lump, Size);
	return iSavePcxInternal();
}


// Internal function used to save the .pcx.
ILboolean iSavePcxInternal() {
	ILuint	i, c, PalSize;
	ILpal	*TempPal;
	ILimage	*TempImage = iCurImage;
	ILubyte	*TempData;

	if (iCurImage == NULL) {
		ilSetError(IL_ILLEGAL_OPERATION);
		return IL_FALSE;
	}

	switch (iCurImage->Format)
	{
		case IL_LUMINANCE:
			TempImage = iConvertImage(iCurImage, IL_COLOUR_INDEX, IL_UNSIGNED_BYTE);
			if (TempImage == NULL)
				return IL_FALSE;
			break;

		case IL_BGR:
			TempImage = iConvertImage(iCurImage, IL_RGB, IL_UNSIGNED_BYTE);
			if (TempImage == NULL)
				return IL_FALSE;
			break;

		case IL_BGRA:
			TempImage = iConvertImage(iCurImage, IL_RGBA, IL_UNSIGNED_BYTE);
			if (TempImage == NULL)
				return IL_FALSE;
			break;

		default:
			if (iCurImage->Bpc > 1) {
				TempImage = iConvertImage(iCurImage, iCurImage->Format, IL_UNSIGNED_BYTE);
				if (TempImage == NULL)
					return IL_FALSE;
			}
	}

	if (TempImage->Origin != IL_ORIGIN_UPPER_LEFT) {
		TempData = iGetFlipped(TempImage);
		if (TempData == NULL) {
			if (TempImage != iCurImage) {
				ilCloseImage(TempImage);
			}
			return IL_FALSE;
		}
	}
	else {
		TempData = TempImage->Data;
	}


	iputc(0xA);  // Manufacturer - always 10
	iputc(0x5);  // Version Number - always 5
	iputc(0x1);  // Encoding - always 1
	iputc(0x8);  // Bits per channel
	SaveLittleUShort(0);  // X Minimum
	SaveLittleUShort(0);  // Y Minimum
	SaveLittleUShort((ILushort)(iCurImage->Width - 1));
	SaveLittleUShort((ILushort)(iCurImage->Height - 1));
	SaveLittleUShort(0);
	SaveLittleUShort(0);

	// Useless palette info?
	for (i = 0; i < 48; i++) {
		iputc(0);
	}
	iputc(0x0);  // Reserved - always 0

	iputc(iCurImage->Bpp);  // Number of planes - only 1 is supported right now

	SaveLittleUShort((ILushort)(iCurImage->Width & 1 ? iCurImage->Width + 1 : iCurImage->Width));  // Bps
	SaveLittleUShort(0x1);  // Palette type - ignored?

	// Mainly filler info
	for (i = 0; i < 58; i++) {
		iputc(0x0);
	}

	// Output data
	for (i = 0; i < TempImage->Height; i++) {
		for (c = 0; c < TempImage->Bpp; c++) {
			encLine(TempData + TempImage->Bps * i + c, TempImage->Width, (ILubyte)(TempImage->Bpp - 1));
		}
	}

	// Automatically assuming we have a palette...dangerous!
	//	Also assuming 3 bpp palette
	iputc(0xC);  // Pad byte must have this value

	// If the current image has a palette, take care of it
	if (TempImage->Format == IL_COLOUR_INDEX) {
		// If the palette in .pcx format, write it directly
		if (TempImage->Pal.PalType == IL_PAL_RGB24) {
			iwrite(TempImage->Pal.Palette, 1, TempImage->Pal.PalSize);
		}
		else {
			TempPal = iConvertPal(&TempImage->Pal, IL_PAL_RGB24);
			if (TempPal == NULL) {
				if (TempImage->Origin == IL_ORIGIN_LOWER_LEFT)
					ifree(TempData);
				if (TempImage != iCurImage)
					ilCloseImage(TempImage);
				return IL_FALSE;
			}

			iwrite(TempPal->Palette, 1, TempPal->PalSize);
			ifree(TempPal->Palette);
			ifree(TempPal);
		}
	}

	// If the palette is not all 256 colours, we have to pad it.
	PalSize = 768 - iCurImage->Pal.PalSize;
	for (i = 0; i < PalSize; i++) {
		iputc(0x0);
	}

	if (TempImage->Origin == IL_ORIGIN_LOWER_LEFT)
		ifree(TempData);
	if (TempImage != iCurImage)
		ilCloseImage(TempImage);

	return IL_TRUE;
}


// Routine used from ZSoft's pcx documentation
ILuint encput(ILubyte byt, ILubyte cnt) {
	if (cnt) {
		if ((cnt == 1) && (0xC0 != (0xC0 & byt))) {
			if (IL_EOF == iputc(byt))
				return(0);     /* disk write error (probably full) */
			return(1);
		}
		else {
			if (IL_EOF == iputc((ILubyte)((ILuint)0xC0 | cnt)))
				return (0);      /* disk write error */
			if (IL_EOF == iputc(byt))
				return (0);      /* disk write error */
			return (2);
		}
	}

	return (0);
}

/* This subroutine encodes one scanline and writes it to a file.
It returns number of bytes written into outBuff, 0 if failed. */

ILuint encLine(ILubyte *inBuff, ILint inLen, ILubyte Stride)
{
	ILubyte _this, last;
	ILint srcIndex, i;
	ILint total;
	ILubyte runCount;     // max single runlength is 63
	total = 0;
	runCount = 1;
	last = *(inBuff);

	// Find the pixel dimensions of the image by calculating 
	//[XSIZE = Xmax - Xmin + 1] and [YSIZE = Ymax - Ymin + 1].  
	//Then calculate how many bytes are in a "run"

	for (srcIndex = 1; srcIndex < inLen; srcIndex++) {
		inBuff += Stride;
		_this = *(++inBuff);
		if (_this == last) {  // There is a "run" in the data, encode it
			runCount++;
			if (runCount == 63) {
				if (! (i = encput(last, runCount)))
						return (0);
				total += i;
				runCount = 0;
			}
		}
		else {  // No "run"  -  _this != last
			if (runCount) {
				if (! (i = encput(last, runCount)))
					return(0);
				total += i;
			}
			last = _this;
			runCount = 1;
		}
	}  // endloop

	if (runCount) {  // finish up
		if (! (i = encput(last, runCount)))
			return (0);
		if (inLen % 2)
			iputc(0);
		return (total + i);
	}
	else {
		if (inLen % 2)
			iputc(0);
	}

	return (total);
}

#endif//IL_NO_PCX
