#include <fitz.h>

#define noDEBUG 1

#ifdef DEBUG
#define DPRINT(...) fprintf(stderr, __VA_ARGS__)
#else
#define DPRINT(...)
#endif

enum
{
	MINBITS = 9,
	MAXBITS = 12,
	MAXBYTES = 2,
	NUMCODES = (1 << MAXBITS),
	LZW_CLEAR = 256,
	LZW_EOD = 257,
	LZW_FIRST = 258,
	HSIZE = 9001,		/* 91% occupancy (???) */
	HSHIFT = (13 - 8),
};

typedef struct lzw_hash_s lzw_hash;

struct lzw_hash_s
{
	int hash;
	int code;
};

typedef struct fz_lzwe_s fz_lzwe;

struct fz_lzwe_s
{
	fz_filter super;

	int earlychange;

	int bidx;		/* partial bits used in out->wp */
	unsigned char bsave;	/* partial byte saved between process() calls */

	int resume;
	int code;
	int fcode;
	int hcode;

	int codebits;
	int oldcode;
	int nextcode;

	lzw_hash table[HSIZE];
};

static void
clearhash(fz_lzwe *lzw)
{
	int i;
	for (i = 0; i < HSIZE; i++)
		lzw->table[i].hash = -1;
}

fz_error *
fz_newlzwe(fz_filter **fp, fz_obj *params)
{
	FZ_NEWFILTER(fz_lzwe, lzw, lzwe);

	lzw->earlychange = 0;

	if (params) {
		fz_obj *obj;
		obj = fz_dictgets(params, "EarlyChange");
		if (obj) lzw->earlychange = fz_toint(obj) != 0;
	}

	lzw->bidx = 0;
	lzw->bsave = 0;

	lzw->resume = 0;
	lzw->code = -1;
	lzw->hcode = -1;
	lzw->fcode = -1;

	lzw->codebits = MINBITS;
	lzw->nextcode = LZW_FIRST;
	lzw->oldcode = -1;	/* generates LZW_CLEAR */

	clearhash(lzw);

	return nil;
}

void
fz_droplzwe(fz_filter *filter)
{
}

static void
putcode(fz_lzwe *lzw, fz_buffer *out, int code)
{
	int nbits = lzw->codebits;

	DPRINT("CODE[%d] %d\n", nbits, code);

	while (nbits > 0)
	{
		if (lzw->bidx == 0) {
			*out->wp = 0;
		}

		/* code does not fit: shift right */
		if (nbits > (8 - lzw->bidx)) {
			*out->wp |= code >> (nbits - (8 - lzw->bidx));
			nbits = nbits - (8 - lzw->bidx);
			lzw->bidx = 0;
			out->wp ++;
		}

		/* shift left */
		else {
			*out->wp |= code << ((8 - lzw->bidx) - nbits);
			lzw->bidx += nbits;
			if (lzw->bidx == 8) {
				lzw->bidx = 0;
				out->wp ++;
			}
			nbits = 0;
		}
	}
}


static fz_error *
compress(fz_lzwe *lzw, fz_buffer *in, fz_buffer *out)
{
	if (lzw->resume) {
		lzw->resume = 0;
		goto resume;
	}

	/* at start of data, output a clear code */
	if (lzw->oldcode == -1)
	{
		if (out->wp + 3 > out->ep)
			return fz_ioneedout;

		if (in->rp + 1 > in->wp) {
			if (in->eof)
				goto eof;
			return fz_ioneedin;
		}

		putcode(lzw, out, LZW_CLEAR);

		lzw->oldcode = *in->rp++;
	}

begin:
	while (1)
	{
		if (in->rp + 1 > in->wp) {
			if (in->eof)
				goto eof;
			return fz_ioneedin;
		}

		/* read character */
		lzw->code = *in->rp++;

		DPRINT("READ %d\n", lzw->code);

		/* hash string + character */
		lzw->fcode = (lzw->code << MAXBITS) + lzw->oldcode;
		lzw->hcode = (lzw->code << HSHIFT) ^ lzw->oldcode;

		/* primary hash */
		if (lzw->table[lzw->hcode].hash == lzw->fcode) {
			lzw->oldcode = lzw->table[lzw->hcode].code;
			continue;
		}

		/* secondary hash */
		if (lzw->table[lzw->hcode].hash != -1) {
			int disp = HSIZE - lzw->hcode;
			if (lzw->hcode == 0)
				disp = 1;
			do {
				lzw->hcode = lzw->hcode - disp;
				if (lzw->hcode < 0)
					lzw->hcode += HSIZE;
				if (lzw->table[lzw->hcode].hash == lzw->fcode) {
					lzw->oldcode = lzw->table[lzw->hcode].code;
					goto begin;
				}
			} while (lzw->table[lzw->hcode].hash != -1);
		}

resume:
		/* new entry: emit code and add to table */

		/* reserve space for this code and an eventual CLEAR code */
		if (out->wp + 5 > out->ep) {	
			lzw->resume = 1;
			return fz_ioneedout;
		}

		putcode(lzw, out, lzw->oldcode);

		lzw->oldcode = lzw->code;
		lzw->table[lzw->hcode].code = lzw->nextcode;
		lzw->table[lzw->hcode].hash = lzw->fcode;

		lzw->nextcode ++;

		/* table is full: emit clear code and reset */
		if (lzw->nextcode == NUMCODES - 1) {
			putcode(lzw, out, LZW_CLEAR);
			clearhash(lzw);
			lzw->nextcode = LZW_FIRST;
			lzw->codebits = MINBITS;
		}

		/* check if next entry will be too big for the code size */
		else if (lzw->nextcode >= (1 << lzw->codebits) - lzw->earlychange) {
			lzw->codebits ++;
		}
	}

eof:
	if (out->wp + 5 > out->ep)
		return fz_ioneedout;

	putcode(lzw, out, lzw->oldcode);
	putcode(lzw, out, LZW_EOD);

	out->eof = 1;
	return fz_iodone;
}

fz_error *
fz_processlzwe(fz_filter *filter, fz_buffer *in, fz_buffer *out)
{
	fz_lzwe *lzw = (fz_lzwe*)filter;
	fz_error *err;

	/* restore partial bits */
	*out->wp = lzw->bsave;

	err = compress(lzw, in, out);

	/* save partial bits */
	lzw->bsave = *out->wp;

	return err;
}