#include <Draw/Draw.h>
#include <setjmp.h>
#include "jpg.h"
#define HAVE_BOOLEAN
#define boolean int
#undef FAR
extern "C" {
#include "lib/jinclude.h"
#include "lib/jpeglib.h"
#include "lib/jerror.h"
}
#undef XINT32


namespace Upp {

#define LLOG(x)  // LOG(x)

static void NoOutput(j_common_ptr cinfo)
{
}

enum 
{
	STREAM_BUF_SIZE = 16384,
	ROW_BUF_SIZE    = 16384,
};

enum {
	EXIF_BYTE      =  1, // An 8-bit unsigned integer
	EXIF_ASCII     =  2, // An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL
	EXIF_SHORT     =  3, // A 16-bit (2-byte) unsigned integer
	EXIF_LONG      =  4,  // A 32-bit (4-byte) unsigned integer
	EXIF_RATIONAL  =  5, // Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator
	EXIF_UNDEFINED =  7, // An 8-bit byte that can take any value depending on the field definition
	EXIF_SLONG     =  9, // A 32-bit (4-byte) signed integer (2's complement notation)
	EXIF_SRATIONAL = 10, // Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator
};

struct jpg_stream_destination_mgr
{
	jpeg_destination_mgr  pub;
	Stream               *stream;
	JOCTET               *buffer;
};

typedef jpg_stream_destination_mgr *stream_dest_ptr;

static void init_destination(j_compress_ptr cinfo)
{
	stream_dest_ptr dest = (stream_dest_ptr)cinfo->dest;
	dest->buffer = (JOCTET *)
		(*cinfo->mem->alloc_small)((j_common_ptr)cinfo, JPOOL_IMAGE,
			STREAM_BUF_SIZE * sizeof(JOCTET));
	dest->pub.next_output_byte = dest->buffer;
	dest->pub.free_in_buffer = STREAM_BUF_SIZE;
}

static boolean empty_output_buffer(j_compress_ptr cinfo)
{
	stream_dest_ptr dest = (stream_dest_ptr)cinfo->dest;
	dest->stream->Put(dest->buffer, STREAM_BUF_SIZE * sizeof(JOCTET));
	dest->pub.next_output_byte = dest->buffer;
	dest->pub.free_in_buffer = STREAM_BUF_SIZE;
	return true;
}

static void term_destination(j_compress_ptr cinfo)
{
	stream_dest_ptr dest = (stream_dest_ptr)cinfo->dest;
	size_t done = STREAM_BUF_SIZE - dest->pub.free_in_buffer;
	if(done > 0)
		dest->stream->Put(dest->buffer, (dword)(done * sizeof(JOCTET)));
}

static void jpeg_stream_dest(j_compress_ptr cinfo, Stream& stream)
{
	if(cinfo->dest == NULL)
		cinfo->dest = (jpeg_destination_mgr *)
			(*cinfo->mem->alloc_small)((j_common_ptr)cinfo, JPOOL_PERMANENT,
				sizeof(jpg_stream_destination_mgr));

	stream_dest_ptr dest = (stream_dest_ptr)cinfo->dest;
	dest->pub.init_destination    = init_destination;
	dest->pub.empty_output_buffer = empty_output_buffer;
	dest->pub.term_destination    = term_destination;
	dest->pub.next_output_byte    = NULL;
	dest->pub.free_in_buffer      = 0;
	dest->stream                  = &stream;
	dest->buffer                  = NULL;
}

struct jpg_stream_source_mgr
{
	jpeg_source_mgr pub;
	Stream         *stream;
	JOCTET         *buffer;
	bool            start_of_file;
};

typedef jpg_stream_source_mgr *stream_src_ptr;

static void init_source(j_decompress_ptr cinfo)
{
	stream_src_ptr src = (stream_src_ptr)cinfo->src;
	src->start_of_file = true;
}

static boolean fill_input_buffer(j_decompress_ptr cinfo)
{
	stream_src_ptr src = (stream_src_ptr)cinfo->src;
	size_t nbytes = src->stream->Get(src->buffer, STREAM_BUF_SIZE * sizeof(JOCTET));
	if(nbytes == 0)
	{
		if(src->start_of_file)
			ERREXIT(cinfo, JERR_INPUT_EMPTY);
		WARNMS(cinfo, JWRN_JPEG_EOF);
		/* Insert a fake EOI marker */
		src->buffer[0] = (JOCTET) 0xFF;
		src->buffer[1] = (JOCTET) JPEG_EOI;
		nbytes = 2;
	}

	src->pub.next_input_byte = src->buffer;
	src->pub.bytes_in_buffer = nbytes;
	src->start_of_file       = false;

	return true;
}

static void skip_input_data(j_decompress_ptr cinfo, long num_bytes)
{
	stream_src_ptr src = (stream_src_ptr)cinfo->src;
	int above = (int)(num_bytes - src->pub.bytes_in_buffer);
	if(above < 0)
	{ // we're still within the input buffer
		src->pub.next_input_byte += num_bytes;
		src->pub.bytes_in_buffer = -above;
	}
	else
	{
		src->stream->Seek(src->stream->GetPos() + above);
		src->start_of_file = false;
		src->pub.next_input_byte = NULL;
		src->pub.bytes_in_buffer = 0;
	}
}

static void term_source(j_decompress_ptr cinfo)
{
	stream_src_ptr src = (stream_src_ptr)cinfo->src;
	src->pub.next_input_byte = NULL;
	src->pub.bytes_in_buffer = 0;
}

static void jpeg_stream_src(j_decompress_ptr cinfo, Stream& stream)
{
	stream_src_ptr src = (stream_src_ptr)cinfo->src;
	if(src == NULL)
	{
		cinfo->src = (jpeg_source_mgr *)
			(*cinfo->mem->alloc_small)((j_common_ptr)cinfo, JPOOL_PERMANENT,
				sizeof(jpg_stream_source_mgr));
		src = (stream_src_ptr)cinfo->src;
		src->buffer = (JOCTET *)(*cinfo->mem->alloc_small)((j_common_ptr)cinfo,
			JPOOL_PERMANENT, STREAM_BUF_SIZE * sizeof(JOCTET));
	}

	src->pub.init_source       = init_source;
	src->pub.fill_input_buffer = fill_input_buffer;
	src->pub.skip_input_data   = skip_input_data;
	src->pub.resync_to_restart = jpeg_resync_to_restart;
	src->pub.term_source       = term_source;
	src->pub.bytes_in_buffer   = 0;
	src->pub.next_input_byte   = NULL;
	src->stream                = &stream;
}

struct jpeg_longjmp_error_mgr : public jpeg_error_mgr {
	jmp_buf jmpbuf;
};

static void error_exit(j_common_ptr cinfo)
{
	(*cinfo->err->output_message)(cinfo);
	jpeg_longjmp_error_mgr *jlem = (jpeg_longjmp_error_mgr *)cinfo->err;
	longjmp(jlem->jmpbuf, 1);
}

class JPGRaster::Data {
	friend class JPGRaster;

public:
	Data(JPGRaster& owner);
	~Data();

	bool Create(Stream& stream);
	void Construct();
	void Free();
	Raster::Info GetInfo();
	Raster::Line GetLine(int line);
	void ScanMetaData();
	void ScanExifData(const char *begin, const char *end);
	Value GetMetaData(String id);
	void EnumMetaData(Vector<String>& id_list);
	String GetThumbnail();

private:
	bool Init();

	int     Exif16(const char *s, const char *end) { return s + 2 < end ? exif_big_endian ? Peek16be(s) : Peek16le(s) : 0; }
	int     Exif32(const char *s, const char *end) { return s + 4 < end ? exif_big_endian ? Peek32be(s) : Peek32le(s) : 0; }
	double  ExifF5(const char *s, const char *end);

	enum IFD_TYPE { BASE_IFD, GPS_IFD };
	int  ExifDir(const char *begin, const char *end, int offset, IFD_TYPE type);

private:
	JPGRaster& owner;
	Stream *stream;
	int64 stream_fpos;

	RasterFormat format;
	RGBA palette[256];

	VectorMap<String, Value> metadata;

	jpeg_decompress_struct   cinfo;
	jpeg_longjmp_error_mgr  *jerr;

	Size size;
	Size dot_size;
	int row_bytes;
	int row_bytes_4;
	bool finish;
	bool exif_big_endian;

	int next_line;
};

void JPGRaster::Data::Construct()
{
	stream = NULL;
	size = dot_size = Null;
	next_line = 0;
	finish = false;

	jpeg_create_decompress(&cinfo);
}

JPGRaster::Data::Data(JPGRaster& owner_)
: owner(owner_)
{
	jerr = (jpeg_longjmp_error_mgr *)malloc(sizeof(jpeg_longjmp_error_mgr));
	Construct();
}

void JPGRaster::Data::Free()
{
	if(setjmp(jerr->jmpbuf))
		return;
	if(finish)
		jpeg_abort_decompress(&cinfo);
	jpeg_destroy_decompress(&cinfo);
}

JPGRaster::Data::~Data()
{
	Free();
	free(jerr);
}

bool JPGRaster::Data::Create(Stream& stream_)
{
	stream = &stream_;
	stream_fpos = stream->GetPos();
	cinfo.err = jpeg_std_error(jerr);
	cinfo.err->output_message = &NoOutput;
	cinfo.dct_method = JDCT_IFAST;
	jerr->error_exit = error_exit;

	return Init();
}

void JPGRaster::Data::ScanMetaData()
{
	if(!metadata.IsEmpty())
		return;
	for(jpeg_saved_marker_ptr p = cinfo.marker_list; p; p = p->next) {
		String data(p->data, p->data_length);
		String key;
		if(p->marker == JPEG_COM)
			key = "COM";
		else if(p->marker >= JPEG_APP0 && p->marker <= JPEG_APP0 + 15) {
			key = Format("APP%d", p->marker - JPEG_APP0);
			if(p->marker == JPEG_APP0 + 1 && !memcmp(data, "Exif\0\0", 6)) {
				ScanExifData(data.GetIter(6), data.end());
			}
		}
		if(metadata.Find(key) >= 0) {
			for(int i = 1;; i++) {
				String suffix;
				suffix << key << '$' << i;
				if(metadata.Find(suffix) < 0) {
					key = suffix;
					break;
				}
			}
		}
		metadata.Add(key, data);
	}
}

double JPGRaster::Data::ExifF5(const char *s, const char *end)
{
	unsigned num = Exif32(s + 0, end);
	unsigned den = Exif32(s + 4, end);
	return num / (double)(den ? den : 1);
}

int JPGRaster::Data::ExifDir(const char *begin, const char *end, int offset, IFD_TYPE type)
{
	if(offset < 0 || begin + offset > end)
		return 0;

	const char *e = begin + offset;
	int nitems = Exif16(e, end);
	e += 2;
	for(int i = 0; i < nitems; i++, e += 12) {
		int tag = Exif16(e, end);
		int fmt = Exif16(e + 2, end);
		int count = Exif32(e + 4, end);
		static const int fmtlen[] = {
			1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8
		};
		int len = 0;
		if(fmt > 0 && fmt <= __countof(fmtlen))
			len = fmtlen[fmt - 1] * count;
		const char *data = e + 8;
		if(len > 4)
			data = begin + Exif32(data, end);

		auto Text20 = [&](int atag, const char *id) {
			if(tag == atag) {
				String s;
				const char *e = min(end, data + 20);
				while(data < e && *data)
					s.Cat(*data++);
				metadata.Add(id, s);
			}
		};
			
		auto F5 = [&](int atag, const char *id) {
			if(tag == atag && fmt == EXIF_RATIONAL)
				metadata.Add(id, ExifF5(data, end));
		};
		
		auto I16 = [&](int atag, const char *id) {
			if(tag == atag && fmt == 3)
				metadata.Add(id, Exif16(data, end));
		};

		if(type == BASE_IFD) {
			I16(0x112, "orientation");
			I16(0x112, "Orientation");
			Text20(0x131, "Software");
			Text20(0x132, "DateTime");
			Text20(0x9003, "DateTimeOriginal");
			Text20(0x9004, "DateTimeDigitized");
			F5(0x011a, "XResolution");
			F5(0x011b, "YResolution");
			I16(0x0128, "ResolutionUnit");
			
			if(tag == 0x8825 && data + 20 < end) {
				int offset = Exif32(data, end);
				ExifDir(begin, end, offset, GPS_IFD);
			}
		}
		else if(type == GPS_IFD) {
			if((tag == 2 || tag == 4) && fmt == EXIF_RATIONAL && count == 3) {
				metadata.Add(tag == 2 ? "GPSLatitude" : "GPSLongitude",
					ExifF5(data + 0, end) + ExifF5(data + 8, end) / 60 + ExifF5(data + 16, end) / 3600);
			}
			else if(tag == 6 && fmt == EXIF_RATIONAL && count == 1)
				metadata.Add("GPSAltitude", ExifF5(data, end));
			else if(tag == 16 && fmt == EXIF_ASCII && count == 2 && *data)
				metadata.Add("GPSImgDirectionRef", String(*data, 1));
			else if(tag == 17 && fmt == EXIF_RATIONAL && count == 1)
				metadata.Add("GPSImgDirection", ExifF5(data + 0, end));
		}
	}
	return Exif32(e, end);
}

void JPGRaster::Data::ScanExifData(const char *begin, const char *end)
{
	const char *p = begin;
	if(p[0] == 'I' && p[1] == 'I')
		exif_big_endian = false;
	else if(p[0] == 'M' && p[1] == 'M')
		exif_big_endian = true;
	else
		return;
	for(int diroff = Exif32(p + 4, end); diroff && begin + diroff < end; diroff = ExifDir(begin, end, diroff, BASE_IFD))
		;
}

Value JPGRaster::Data::GetMetaData(String id)
{
	ScanMetaData();
	return metadata.Get(id, Value());
}

void JPGRaster::Data::EnumMetaData(Vector<String>& id_list)
{
	ScanMetaData();
	id_list <<= metadata.GetKeys();
}

String JPGRaster::Data::GetThumbnail()
{
	ScanMetaData();
	String app1 = GetMetaData("APP1");
	if(!app1.GetCount())
		return Null;
	const char *begin = ~app1 + 6;
	const char *end = app1.End();
	const char *p = begin + Exif32(begin + 4, end);
	if(p + 2 >= end) return Null;
	p += Exif16(p, end) * 12 + 2;
	if(p + 4 >= end) return Null;
	p = begin + Exif32(p, end);
	if(p <= begin || p + 2 >= end) return Null;
	word count = Exif16(p, end);
	p += 2;
	dword offset = 0;
	dword len = 0;
	for(int n = 0; n < count; n++) {
		if(p + 12 >= end) return Null;
		if(Exif32(p + 4, end) == 1) {
			dword val = 0;
			switch(Exif16(p + 2, end)) {
			case 4:
			case 9:
				val = Exif32(p + 8, end);
				break;
			case 3:
			case 8:
				val = Exif16(p + 8, end);
				break;
			}
			if(val)
				switch(Exif16(p, end)) {
				case 0x201:
					offset = val;
					break;
				case 0x202:
					len = val;
					break;
				}
		}
		p += 12;
	}
	return offset && len && begin + offset + len < end ? String(begin + offset, len) : String();
}

bool JPGRaster::Data::Init()
{
	if(setjmp(jerr->jmpbuf))
		return false;
	jpeg_stream_src(&cinfo, *stream);
	jpeg_save_markers(&cinfo, JPEG_COM, 0xFFFF);
	for(int i = 0; i <= 15; i++)
		jpeg_save_markers(&cinfo, JPEG_APP0 + i, 0xFFFF);
	jpeg_read_header(&cinfo, TRUE);
	jpeg_start_decompress(&cinfo);

	switch(cinfo.output_components) {
		case 1: {
			format.Set8();
			for(int i = 0; i < 256; i++) {
				RGBA rgba;
				rgba.r = rgba.g = rgba.b = i;
				rgba.a = 255;
				palette[i] = rgba;
			}
			break;
		}
		case 3:
		case 4: {
			format.Set24le(0xFF, 0xFF00, 0xFF0000);
			break;
		}
		default: {
			LLOG("JPGRaster: invalid number of components: " << (int)cinfo.output_components);
			return false;
		}
	}

	size.cx = cinfo.output_width;
	size.cy = cinfo.output_height;

	row_bytes = cinfo.output_components * size.cx;
	row_bytes_4 = (row_bytes + 3) & -4;

	double dot_scaling = (cinfo.density_unit == 1 ? 600 : cinfo.density_unit == 2 ? 600.0 / 2.54 : 0);
	if(dot_scaling && cinfo.X_density > 0) {
		dot_size.cx = fround(cinfo.image_width  * dot_scaling / cinfo.X_density);
		dot_size.cy = fround(cinfo.image_height * dot_scaling / cinfo.Y_density);
	}

	finish = true;
	return true;
}

Raster::Info JPGRaster::Data::GetInfo()
{
	Raster::Info info;
	try {
		info.bpp = 24;
		info.colors = 0;
		info.dots = dot_size;
		info.hotspot = Point(0, 0);
		info.kind = IMAGE_OPAQUE;
	}
	catch(Exc e) {
		LLOG(e);
	}
	return info;
}

Raster::Line JPGRaster::Data::GetLine(int line)
{
	byte *rowbuf = new byte[row_bytes_4];
	if(setjmp(jerr->jmpbuf))
		return Raster::Line(rowbuf, &owner, true);

	ASSERT(line >= 0 && line < size.cy);
	if(line < next_line) {
		stream->Seek(stream_fpos);
		Stream *s = stream;
		Free();
		Construct();
		Create(*s);
		next_line = 0;
		LOG("Seek back");
	}
	JSAMPROW rowptr[] = { (JSAMPROW)rowbuf };
	while(next_line++ < line)
		jpeg_read_scanlines(&cinfo, rowptr, 1);
	jpeg_read_scanlines(&cinfo, rowptr, 1);

	if(cinfo.output_components == 4) { 
		/* Convert CMYK scanline to RGB */
		JSAMPLE k;
		for(int i = 0, j = 0; i < row_bytes_4; i++){			
			k = GETJSAMPLE(rowbuf[i+3]);
			rowbuf[j++] = GETJSAMPLE(rowbuf[i++]) * k / 255;
		    rowbuf[j++] = GETJSAMPLE(rowbuf[i++]) * k / 255;
		    rowbuf[j++] = GETJSAMPLE(rowbuf[i++]) * k / 255;
		}	
	}

	return Raster::Line(rowbuf, &owner, true);
}

JPGRaster::JPGRaster()
{
}

JPGRaster::~JPGRaster()
{
}

bool JPGRaster::Create()
{
	ASSERT(sizeof(JSAMPLE) == sizeof(byte));
	data = new Data(*this);
	return data->Create(GetStream());
}

Size JPGRaster::GetSize()
{
	return data->size;
}

Raster::Info JPGRaster::GetInfo()
{
	ASSERT(data);
	Raster::Info info;
	info.kind = IMAGE_OPAQUE;
	if(data->cinfo.output_components == 1) {
		info.bpp = 8;
		info.colors = 256;
	}
	else {
		info.bpp = 24;
		info.colors = 0;
	}
	info.dots = data->dot_size;
	info.hotspot = Null;
	Value v = GetMetaData("orientation");
	if(IsNumber(v))
		info.orientation = clamp((int)v - 1, 0, 7);
	return info;
}

Value JPGRaster::GetMetaData(String id)
{
	ASSERT(data);
	return data->GetMetaData(id);
}

void JPGRaster::EnumMetaData(Vector<String>& id_list)
{
	ASSERT(data);
	data->EnumMetaData(id_list);
}

Raster::Line JPGRaster::GetLine(int line)
{
	ASSERT(data);
	return data->GetLine(line);
}

const RGBA *JPGRaster::GetPalette()
{
	ASSERT(data);
	return data->palette;
}

const RasterFormat *JPGRaster::GetFormat()
{
	ASSERT(data);
	return &data->format;
}

class JPGEncoder::Data {
public:
	Data();
	~Data();

	void Start(Stream& stream, Size size, Size dots, int quality);
	void WriteLineRaw(const byte *rgba);

private:
	struct jpeg_compress_struct cinfo;
	struct jpeg_error_mgr jerr;
	Size size;
	int line;
};

JPGEncoder::Data::Data()
{
	cinfo.err = jpeg_std_error(&jerr);
	cinfo.err->output_message = &NoOutput;
	jpeg_create_compress(&cinfo);
	cinfo.dct_method = JDCT_IFAST;
}

JPGEncoder::Data::~Data()
{
	jpeg_destroy_compress(&cinfo);
}

void JPGEncoder::Data::Start(Stream& stream, Size size_, Size dots, int quality)
{
	size = size_;

	jpeg_stream_dest(&cinfo, stream);

	cinfo.image_width = size.cx;
	cinfo.image_height = size.cy;
	cinfo.input_components = 3; // # of color components per pixel
	cinfo.in_color_space = JCS_RGB; // colorspace of input image

	jpeg_set_defaults(&cinfo);

	if(dots.cx || dots.cy)
	{ // set up image density
		cinfo.density_unit = 1; // dots per inch
		cinfo.X_density = dots.cx ? fround(size.cx * 600.0 / dots.cx) : 0;
		cinfo.Y_density = dots.cy ? fround(size.cy * 600.0 / dots.cy) : 0;
	}

	jpeg_set_quality(&cinfo, quality, true); // limit to baseline-JPEG values
	jpeg_start_compress(&cinfo, true);

	line = 0;

	ASSERT(sizeof(JSAMPLE) == sizeof(byte));
}

void JPGEncoder::Data::WriteLineRaw(const byte *s)
{
	JSAMPROW rowptr[] = { (byte *)s };
	jpeg_write_scanlines(&cinfo, rowptr, 1);
	if(++line >= size.cy)
		jpeg_finish_compress(&cinfo);
}

JPGEncoder::JPGEncoder(int quality_)
: quality(quality_)
{
	format.Set24le(0xff, 0xff00, 0xff0000);
}

JPGEncoder::~JPGEncoder()
{
}

int JPGEncoder::GetPaletteCount()
{
	return 0;
}

void JPGEncoder::Start(Size sz)
{
	data = new Data;
	data->Start(GetStream(), sz, GetDots(), quality);
}

void JPGEncoder::WriteLineRaw(const byte *s)
{
	data->WriteLineRaw(s);
}

Image JPGRaster::GetExifThumbnail()
{
	ASSERT(data);
	StringStream ss(data->GetThumbnail());
	return StreamRaster::LoadStringAny(ss);
}

bool IsJPG(StreamRaster *s)
{
	return dynamic_cast<JPGRaster *>(s);
}

INITIALIZER(JPGRaster)
{
	StreamRaster::Register<JPGRaster>();
	SetIsJPGFn(IsJPG);
}

}
