#include "mupdf/fitz.h"

#include <stdio.h>
#include <jpeglib.h>

#ifndef SHARE_JPEG
typedef void * backing_store_ptr;
#include "jmemcust.h"
#endif

typedef struct fz_dctd_s fz_dctd;

struct fz_dctd_s
{
	fz_stream *chain;
	fz_stream *jpegtables;
	fz_stream *curr_stm;
	fz_context *ctx;
	int color_transform;
	int init;
	int stride;
	int l2factor;
	unsigned char *scanline;
	unsigned char *rp, *wp;
	struct jpeg_decompress_struct cinfo;
	struct jpeg_source_mgr srcmgr;
	struct jpeg_error_mgr errmgr;
	jmp_buf jb;
	char msg[JMSG_LENGTH_MAX];

	unsigned char buffer[4096];
};

#ifdef SHARE_JPEG

#define JZ_DCT_STATE_FROM_CINFO(c) (fz_dctd *)((c)->client_data)

static void fz_dct_mem_init(struct jpeg_decompress_struct *cinfo, fz_dctd *state)
{
	cinfo->client_data = state;
}

#define fz_dct_mem_term(cinfo)

#else /* SHARE_JPEG */

#define JZ_DCT_STATE_FROM_CINFO(c) (fz_dctd *)(GET_CUST_MEM_DATA(c)->priv)

static void *
fz_dct_mem_alloc(j_common_ptr cinfo, size_t size)
{
	fz_dctd *state = JZ_DCT_STATE_FROM_CINFO(cinfo);
	return fz_malloc_no_throw(state->ctx, size);
}

static void
fz_dct_mem_free(j_common_ptr cinfo, void *object, size_t size)
{
	fz_dctd *state = JZ_DCT_STATE_FROM_CINFO(cinfo);
	fz_free(state->ctx, object);
}

static void
fz_dct_mem_init(struct jpeg_decompress_struct *cinfo, fz_dctd *state)
{
	jpeg_cust_mem_data *custmptr;
	custmptr = fz_malloc_struct(state->ctx, jpeg_cust_mem_data);
	if (!jpeg_cust_mem_init(custmptr, (void *) state, NULL, NULL, NULL,
				fz_dct_mem_alloc, fz_dct_mem_free,
				fz_dct_mem_alloc, fz_dct_mem_free, NULL))
	{
		fz_free(state->ctx, custmptr);
		fz_throw(state->ctx, FZ_ERROR_GENERIC, "cannot initialize custom JPEG memory handler");
	}
	cinfo->client_data = custmptr;
}

static void
fz_dct_mem_term(struct jpeg_decompress_struct *cinfo)
{
	if (cinfo->client_data)
	{
		fz_dctd *state = JZ_DCT_STATE_FROM_CINFO(cinfo);
		fz_free(state->ctx, cinfo->client_data);
		cinfo->client_data = NULL;
	}
}

#endif /* SHARE_JPEG */

static void error_exit_dct(j_common_ptr cinfo)
{
	char msg[JMSG_LENGTH_MAX];
	fz_dctd *state = JZ_DCT_STATE_FROM_CINFO(cinfo);
	fz_context *ctx = state->ctx;
	cinfo->err->format_message(cinfo, msg);
	fz_throw(ctx, FZ_ERROR_GENERIC, "jpeg error: %s", msg);
}

static void init_source_dct(j_decompress_ptr cinfo)
{
	/* nothing to do */
}

static void term_source_dct(j_decompress_ptr cinfo)
{
	/* nothing to do */
}

static boolean fill_input_buffer_dct(j_decompress_ptr cinfo)
{
	struct jpeg_source_mgr *src = cinfo->src;
	fz_dctd *state = JZ_DCT_STATE_FROM_CINFO(cinfo);
	fz_context *ctx = state->ctx;
	fz_stream *curr_stm = state->curr_stm;

	curr_stm->rp = curr_stm->wp;
	fz_try(ctx)
	{
		src->bytes_in_buffer = fz_available(ctx, curr_stm, 1);
	}
	fz_catch(ctx)
	{
		return 0;
	}
	src->next_input_byte = curr_stm->rp;

	if (src->bytes_in_buffer == 0)
	{
		static unsigned char eoi[2] = { 0xFF, JPEG_EOI };
		fz_warn(state->ctx, "premature end of file in jpeg");
		src->next_input_byte = eoi;
		src->bytes_in_buffer = 2;
	}

	return 1;
}

static void skip_input_data_dct(j_decompress_ptr cinfo, long num_bytes)
{
	struct jpeg_source_mgr *src = cinfo->src;
	if (num_bytes > 0)
	{
		while ((size_t)num_bytes > src->bytes_in_buffer)
		{
			num_bytes -= (long)src->bytes_in_buffer;
			(void) src->fill_input_buffer(cinfo);
		}
		src->next_input_byte += num_bytes;
		src->bytes_in_buffer -= num_bytes;
	}
}

static int
next_dctd(fz_context *ctx, fz_stream *stm, size_t max)
{
	fz_dctd *state = stm->state;
	j_decompress_ptr cinfo = &state->cinfo;
	unsigned char *p = state->buffer;
	unsigned char *ep;
	int c;

	if (max > sizeof(state->buffer))
		max = sizeof(state->buffer);
	ep = state->buffer + max;

	fz_try(ctx)
	{
		if (!state->init)
		{
			state->init = 1;

			/* Skip over any stray whitespace at the start of the stream */
			while ((c = fz_peek_byte(ctx, state->chain)) == '\n' || c == '\r' || c == ' ')
				(void)fz_read_byte(ctx, state->chain);

			jpeg_create_decompress(cinfo);

			cinfo->src = &state->srcmgr;
			cinfo->src->init_source = init_source_dct;
			cinfo->src->fill_input_buffer = fill_input_buffer_dct;
			cinfo->src->skip_input_data = skip_input_data_dct;
			cinfo->src->resync_to_restart = jpeg_resync_to_restart;
			cinfo->src->term_source = term_source_dct;

			/* optionally load additional JPEG tables first */
			if (state->jpegtables)
			{
				state->curr_stm = state->jpegtables;
				cinfo->src->next_input_byte = state->curr_stm->rp;
				cinfo->src->bytes_in_buffer = state->curr_stm->wp - state->curr_stm->rp;
				jpeg_read_header(cinfo, 0);
				state->curr_stm->rp = state->curr_stm->wp - state->cinfo.src->bytes_in_buffer;
				state->curr_stm = state->chain;
			}

			cinfo->src->next_input_byte = state->curr_stm->rp;
			cinfo->src->bytes_in_buffer = state->curr_stm->wp - state->curr_stm->rp;

			jpeg_read_header(cinfo, 1);

			/* default value if ColorTransform is not set */
			if (state->color_transform == -1)
			{
				if (state->cinfo.num_components == 3)
					state->color_transform = 1;
				else
					state->color_transform = 0;
			}

			if (cinfo->saw_Adobe_marker)
				state->color_transform = cinfo->Adobe_transform;

			/* Guess the input colorspace, and set output colorspace accordingly */
			switch (cinfo->num_components)
			{
			case 3:
				if (state->color_transform)
					cinfo->jpeg_color_space = JCS_YCbCr;
				else
					cinfo->jpeg_color_space = JCS_RGB;
				break;
			case 4:
				if (state->color_transform)
					cinfo->jpeg_color_space = JCS_YCCK;
				else
					cinfo->jpeg_color_space = JCS_CMYK;
				break;
			}

			cinfo->scale_num = 8/(1<<state->l2factor);
			cinfo->scale_denom = 8;

			jpeg_start_decompress(cinfo);

			state->stride = cinfo->output_width * cinfo->output_components;
			state->scanline = fz_malloc(ctx, state->stride);
			state->rp = state->scanline;
			state->wp = state->scanline;
		}

		while (state->rp < state->wp && p < ep)
			*p++ = *state->rp++;

		while (p < ep)
		{
			if (cinfo->output_scanline == cinfo->output_height)
				break;

			if (p + state->stride <= ep)
			{
				jpeg_read_scanlines(cinfo, &p, 1);
				p += state->stride;
			}
			else
			{
				jpeg_read_scanlines(cinfo, &state->scanline, 1);
				state->rp = state->scanline;
				state->wp = state->scanline + state->stride;
			}

			while (state->rp < state->wp && p < ep)
				*p++ = *state->rp++;
		}
		stm->rp = state->buffer;
		stm->wp = p;
		stm->pos += (p - state->buffer);
	}
	fz_catch(ctx)
	{
		if (cinfo->src)
			state->curr_stm->rp = state->curr_stm->wp - cinfo->src->bytes_in_buffer;
		fz_rethrow(ctx);
	}

	if (p == stm->rp)
		return EOF;

	return *stm->rp++;
}

static void
close_dctd(fz_context *ctx, void *state_)
{
	fz_dctd *state = (fz_dctd *)state_;

	if (state->init)
	{
		/* We call jpeg_abort rather than the more usual
		 * jpeg_finish_decompress here. This has the same effect,
		 * but doesn't spew warnings if we didn't read enough data etc.
		 * Annoyingly jpeg_abort can throw
		 */
		fz_try(ctx)
			jpeg_abort((j_common_ptr)&state->cinfo);
		fz_catch(ctx)
		{
			/* Ignore any errors here */
		}

		jpeg_destroy_decompress(&state->cinfo);
	}

	fz_dct_mem_term(&state->cinfo);

	if (state->cinfo.src)
		state->curr_stm->rp = state->curr_stm->wp - state->cinfo.src->bytes_in_buffer;

	fz_free(ctx, state->scanline);
	fz_drop_stream(ctx, state->chain);
	fz_drop_stream(ctx, state->jpegtables);
	fz_free(ctx, state);
}

/* Default: color_transform = -1 (unset), l2factor = 0, jpegtables = NULL */
fz_stream *
fz_open_dctd(fz_context *ctx, fz_stream *chain, int color_transform, int l2factor, fz_stream *jpegtables)
{
	fz_dctd *state = fz_malloc_struct(ctx, fz_dctd);
	j_decompress_ptr cinfo = &state->cinfo;

	state->ctx = ctx;

	fz_try(ctx)
		fz_dct_mem_init(cinfo, state);
	fz_catch(ctx)
	{
		fz_free(ctx, state);
		fz_rethrow(ctx);
	}

	state->color_transform = color_transform;
	state->init = 0;
	state->l2factor = l2factor;
	state->chain = fz_keep_stream(ctx, chain);
	state->jpegtables = fz_keep_stream(ctx, jpegtables);
	state->curr_stm = state->chain;

	cinfo->src = NULL;
	cinfo->err = &state->errmgr;
	jpeg_std_error(cinfo->err);
	cinfo->err->error_exit = error_exit_dct;

	return fz_new_stream(ctx, state, next_dctd, close_dctd);
}
