
#include <stdio.h>
#include <stdlib.h>
#include "jpeg.h"
#include "jpeglib.h"
#include "jerror.h"


static const void *in_ptr;
static int in_length;
static bool in_overrun;

static void *out_ptr;
static int out_length;
static int out_total;



static void init_source( j_decompress_ptr cinfo )
{
	cinfo->src->next_input_byte = (const JOCTET *)in_ptr;
	cinfo->src->bytes_in_buffer = in_length;
	in_overrun = false;
}


static jp_bool fill_input_buffer( j_decompress_ptr cinfo )
{
	static JOCTET eoibuf[2];
	
	in_overrun = true;
	eoibuf[0] = 0xff;
	eoibuf[1] = JPEG_EOI;
	cinfo->src->next_input_byte = eoibuf;
	cinfo->src->bytes_in_buffer = 2;
	
	return true;
}


static void skip_input_data( j_decompress_ptr cinfo, long num_bytes )
{
	if (num_bytes >= cinfo->src->bytes_in_buffer) {
		fill_input_buffer( cinfo );
	} else {
		cinfo->src->next_input_byte += num_bytes;
		cinfo->src->bytes_in_buffer -= num_bytes;
	}
}


static void term_source( j_decompress_ptr /* cinfo */ )
{
	// nothing necessary
}


static jpeg_source_mgr jSrc = {
	NULL,	// next_input_byte
	0,		// bytes_in_buffer
	init_source,
	fill_input_buffer,
	skip_input_data,
	jpeg_resync_to_restart,	// use the default
	term_source
};


static void init_destination( j_compress_ptr cinfo )
{
	out_total = 0;
	cinfo->dest->next_output_byte = ((JOCTET *)out_ptr);
	cinfo->dest->free_in_buffer = out_length;
}


static jp_bool empty_output_buffer( j_compress_ptr cinfo )
{
	out_total += out_length;
	cinfo->dest->next_output_byte = ((JOCTET *)out_ptr);
	cinfo->dest->free_in_buffer = out_length;
	return TRUE;
}


static void term_destination( j_compress_ptr cinfo )
{
	out_total += out_length - cinfo->dest->free_in_buffer;
}


static jpeg_destination_mgr jDst = {
	NULL,	// next_output_byte
	0,		// free_in_buffer
	init_destination,
	empty_output_buffer,
	term_destination
};




void decompress_jpeg( void *out, const void *in, int in_size, int width, int height, int channels, bool hq )
{
	if (channels == 4) {
		unsigned char *buf = (unsigned char *)malloc( width * height * 4 );
		unsigned char *rgb_in = buf;
		unsigned char *alpha_in = buf + 3 * width * height;
		unsigned char *read = (unsigned char *)in;
		int rgblen = (read[0] << 16) + (read[1] << 8) + (read[2]);
		read += 3;
		decompress_jpeg( rgb_in, read, rgblen, width, height, 3, hq );
		decompress_jpeg( alpha_in, read + rgblen, in_size - (3 + rgblen), width, height, 1, hq );
		int count = width * height;
		unsigned char *rgba_out = (unsigned char *)out;
		for (int i = 0; i < count; ++i) {
			rgba_out[0] = rgb_in[0];
			rgba_out[1] = rgb_in[1];
			rgba_out[2] = rgb_in[2];
			rgb_in += 3;
			rgba_out[3] = *alpha_in++;
			rgba_out += 4;
		}
		free( buf );
	} else {
		struct jpeg_decompress_struct cinfo;
		struct jpeg_error_mgr jerr;
		JSAMPROW *rows;
		
		cinfo.err = jpeg_std_error( &jerr );
		jpeg_create_decompress( &cinfo );
		
		cinfo.src = &jSrc;
		in_ptr = in;
		in_length = in_size;
		
		jpeg_read_header( &cinfo, true );
		
		cinfo.scale_denom = cinfo.image_width / width;
		if (hq) {
			cinfo.dct_method = JDCT_IFAST;
			cinfo.do_fancy_upsampling = true;
		} else {
			cinfo.dct_method = JDCT_IFAST;
			cinfo.do_fancy_upsampling = false;
		}
		
		jpeg_start_decompress( &cinfo );
		
		rows = new JSAMPROW[height];
		for (int y = 0; y < height; ++y) {
			rows[y] = ((JSAMPROW)out) + y * channels * width;
		}
		
		while (cinfo.output_scanline < cinfo.output_height) {
			int outlines = jpeg_read_scanlines( &cinfo, rows + cinfo.output_scanline, height - cinfo.output_scanline );
			if (outlines < 1) break;
		}
		
		delete [] rows;
		
		jpeg_finish_decompress( &cinfo );
		jpeg_destroy_decompress( &cinfo );
	}
}


void compress_jpeg( void *out, int *out_len, const void *in, int width, int height, int channels, int quality )
{
	if (channels == 4) {
		int count = width * height;
		unsigned char *buf = (unsigned char *)malloc( count * 4 );
		const unsigned char *rgba_in = (const unsigned char *)in;
		unsigned char *rgb_out = buf + count;
		unsigned char *alpha_out = buf;
		for (int i = 0; i < count; ++i) {
			rgb_out[0] = rgba_in[0];
			rgb_out[1] = rgba_in[1];
			rgb_out[2] = rgba_in[2];
			rgb_out += 3;
			*alpha_out++ = rgba_in[3];
			rgba_in += 4;
		}
		int rgb_len = *out_len - 3;
		unsigned char *write = (unsigned char *)out;
		compress_jpeg( write + 3, &rgb_len, buf + count, width, height, 3, quality );
		int alpha_len = *out_len - (3 + rgb_len);
		write[0] = (unsigned char)(rgb_len >> 16);
		write[1] = (unsigned char)(rgb_len >> 8);
		write[2] = (unsigned char)rgb_len;
		compress_jpeg( write + 3 + rgb_len, &alpha_len, buf, width, height, 1, quality );
		*out_len = 3 + rgb_len + alpha_len;
		free( buf );
	} else {
		struct jpeg_compress_struct cinfo;
		struct jpeg_error_mgr jerr;
		JSAMPROW *rows;
		
		cinfo.err = jpeg_std_error(&jerr);
		jpeg_create_compress(&cinfo);
		
		cinfo.dest = &jDst;
		out_ptr = out;
		out_length = *out_len;
		
		cinfo.image_width = width;
		cinfo.image_height = height;
		cinfo.input_components = channels;
		cinfo.in_color_space = (channels == 3 ? JCS_RGB : JCS_GRAYSCALE);
		jpeg_set_defaults( &cinfo );
		
		cinfo.dct_method = JDCT_FLOAT;
		cinfo.optimize_coding = true;
		
		jpeg_set_quality( &cinfo, quality, false );
		
		jpeg_start_compress( &cinfo, true );
		
		rows = new JSAMPROW[height];
		for (int y = 0; y < height; ++y) {
			rows[y] = ((JSAMPROW)in) + y * channels * width;
		}
		
		while (cinfo.next_scanline < cinfo.image_height) {
			if (jpeg_write_scanlines( &cinfo, rows + cinfo.next_scanline, height - cinfo.next_scanline ) < 1) break;
		}
		
		delete [] rows;
		
		jpeg_finish_compress( &cinfo );
		*out_len = out_total;
		
		jpeg_destroy_compress(&cinfo);
	}
}


int estimate_jpeg_quality( const void *jpeg, int jpeg_len, int width, int height, int channels )
{
	void *decompressed = malloc( width * height * channels );
	decompress_jpeg( decompressed, jpeg, jpeg_len, width, height, channels, true );
	
	int low = 1;
	int high = 100;
	int bufsize = width * height * channels * 3;
	void *recompressed = malloc( bufsize );
	
	int lowsize = bufsize;
	compress_jpeg( recompressed, &lowsize, decompressed, width, height, channels, low );
	int highsize = bufsize;
	compress_jpeg( recompressed, &highsize, decompressed, width, height, channels, high );
	
	while (high > low + 1) {
		int test = low + (high - low) / 2;
		int testsize = bufsize;
		compress_jpeg( recompressed, &testsize, decompressed, width, height, channels, test );
		if (testsize >= jpeg_len) {
			high = test;
			highsize = testsize;
		}
		if (testsize <= jpeg_len) {
			low = test;
			lowsize = testsize;
		}
	}
	free( recompressed );
	free( decompressed );
	if (jpeg_len > (highsize + lowsize) / 2) {
		return high;
	} else {
		return low;
	}
}


