/*
 * Copyright (C)2005-2022 Haxe Foundation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */
#include <stdlib.h>
#include <string.h>
#include <neko.h>
#include <zlib.h>

/**
	<doc>
	<h1>ZLib</h1>
	<p>
	Give access to the popular ZLib compression library, used in several file
	formats such as ZIP and PNG.
	</p>
	</doc>
**/

DEFINE_KIND(k_stream_def);
DEFINE_KIND(k_stream_inf);

#define val_stream(v)	((z_stream *)val_data(v))
#define val_flush(s)	*((int*)(((char*)s)+sizeof(z_stream)))

static field id_read, id_write, id_done;

DEFINE_ENTRY_POINT(zlib_main);

void zlib_main() {
	id_read = val_id("read");
	id_write = val_id("write");
	id_done = val_id("done");
}

static void free_stream_def( value v ) {
	z_stream *s = val_stream(v);
	deflateEnd(s); // no error
	free(s);
	val_kind(v) = NULL;
	val_gc(v,NULL);
}

static void free_stream_inf( value v ) {
	z_stream *s = val_stream(v);
	inflateEnd(s); // no error
	free(s);
	val_kind(v) = NULL;
	val_gc(v,NULL);
}

static void zlib_error( z_stream *z, int err ) {
	buffer b = alloc_buffer("ZLib Error : ");
	if( z && z->msg ) {
		buffer_append(b,z->msg);
		buffer_append(b," (");
	}
	val_buffer(b,alloc_int(err));
	if( z && z->msg )
		buffer_append_char(b,')');
	val_throw(buffer_to_string(b));
}

/**
	deflate_init : level:int -> 'dstream
	<doc>Open a compression stream with the given level of compression</doc>
**/
static value deflate_init( value level ) {
	z_stream *z;
	value s;
	int err;
	val_check(level,int);
	z = (z_stream*)malloc(sizeof(z_stream) + sizeof(int));
	memset(z,0,sizeof(z_stream));
	val_flush(z) = Z_NO_FLUSH;
	if( (err = deflateInit(z,val_int(level))) != Z_OK ) {
		free(z);
		zlib_error(NULL,err);
	}
	s = alloc_abstract(k_stream_def,z);
	val_gc(s,free_stream_def);
	return s;
}

/**
	deflate_buffer : 'dstream -> src:string -> srcpos:int -> dst:string -> dstpos:int -> { done => bool, read => int, write => int }
**/
static value deflate_buffer( value s, value src, value srcpos, value dst, value dstpos ) {
	z_stream *z;
	int slen, dlen, err;
	value o;
	val_check_kind(s,k_stream_def);
	val_check(src,string);
	val_check(srcpos,int);
	val_check(dst,string);
	val_check(dstpos,int);
	z = val_stream(s);
	if( val_int(srcpos) < 0 || val_int(dstpos) < 0 )
		neko_error();
	slen = val_strlen(src) - val_int(srcpos);
	dlen = val_strlen(dst) - val_int(dstpos);
	if( slen < 0 || dlen < 0 )
		neko_error();
	z->next_in = (Bytef*)(val_string(src) + val_int(srcpos));
	z->next_out = (Bytef*)(val_string(dst) + val_int(dstpos));
	z->avail_in = slen;
	z->avail_out = dlen;
	if( (err = deflate(z,val_flush(z))) < 0 )
		zlib_error(z,err);
	z->next_in = NULL;
	z->next_out = NULL;
	o = alloc_object(NULL);
	alloc_field(o,id_done,alloc_bool(err == Z_STREAM_END));
	alloc_field(o,id_read,alloc_int(slen - z->avail_in));
	alloc_field(o,id_write,alloc_int(dlen - z->avail_out));
	return o;
}

/**
	deflate_end : 'dstream -> void
	<doc>Close a compression stream</doc>
**/
static value deflate_end( value s ) {
	val_check_kind(s,k_stream_def);
	free_stream_def(s);
	return val_null;
}

/**
	inflate_init : window_size:int? -> 'istream
	<doc>Open a decompression stream</doc>
**/
static value inflate_init( value wsize ) {
	z_stream *z;
	value s;
	int err;
	int wbits;
	if( val_is_null(wsize) )
		wbits = MAX_WBITS;
	else {
		val_check(wsize,int);
		wbits = val_int(wsize);
	}
	z = (z_stream*)malloc(sizeof(z_stream) + sizeof(int));
	memset(z,0,sizeof(z_stream));
	val_flush(z) = Z_NO_FLUSH;
	if( (err = inflateInit2(z,wbits)) != Z_OK ) {
		free(z);
		zlib_error(NULL,err);
	}
	s = alloc_abstract(k_stream_inf,z);
	val_gc(s,free_stream_inf);
	return s;
}

/**
	inflate_buffer : 'istream -> src:string -> srcpos:int -> dst:string -> dstpos:int -> { done => bool, read => int, write => int }
**/
static value inflate_buffer( value s, value src, value srcpos, value dst, value dstpos ) {
	z_stream *z;
	int slen, dlen, err;
	value o;
	val_check_kind(s,k_stream_inf);
	val_check(src,string);
	val_check(srcpos,int);
	val_check(dst,string);
	val_check(dstpos,int);
	z = val_stream(s);
	if( val_int(srcpos) < 0 || val_int(dstpos) < 0 )
		neko_error();
	slen = val_strlen(src) - val_int(srcpos);
	dlen = val_strlen(dst) - val_int(dstpos);
	if( slen < 0 || dlen < 0 )
		neko_error();
	z->next_in = (Bytef*)(val_string(src) + val_int(srcpos));
	z->next_out = (Bytef*)(val_string(dst) + val_int(dstpos));
	z->avail_in = slen;
	z->avail_out = dlen;
	if( (err = inflate(z,val_flush(z))) < 0 )
		zlib_error(z,err);
	z->next_in = NULL;
	z->next_out = NULL;
	o = alloc_object(NULL);
	alloc_field(o,id_done,alloc_bool(err == Z_STREAM_END));
	alloc_field(o,id_read,alloc_int(slen - z->avail_in));
	alloc_field(o,id_write,alloc_int(dlen - z->avail_out));
	return o;
}

/**
	inflate_end : 'istream -> void
	<doc>Close a decompression stream</doc>
**/
static value inflate_end( value s ) {
	val_check_kind(s,k_stream_inf);
	free_stream_inf(s);
	return val_null;
}

/**
	set_flush_mode : 'stream -> string -> void
	<doc>Change the flush mode ("NO","SYNC","FULL","FINISH","BLOCK")</doc>
**/
static value set_flush_mode( value s, value flush ) {
	int f;
	if( !val_is_kind(s,k_stream_inf) )
		val_check_kind(s,k_stream_def);
	val_check(flush,string);
	if( strcmp(val_string(flush),"NO") == 0 )
		f = Z_NO_FLUSH;
	else if( strcmp(val_string(flush),"SYNC") == 0 )
		f = Z_SYNC_FLUSH;
	else if( strcmp(val_string(flush),"FULL") == 0 )
		f = Z_FULL_FLUSH;
	else if( strcmp(val_string(flush),"FINISH") == 0 )
		f = Z_FINISH;
	else if( strcmp(val_string(flush),"BLOCK") == 0 )
		f = Z_BLOCK;
	else
		neko_error();
	val_flush(val_stream(s)) = f;
	return val_null;
}

/**
	get_adler32 : 'stream -> 'int32
	<doc>Returns the adler32 value of the stream</doc>
**/
static value get_adler32( value s ) {
	if( !val_is_kind(s,k_stream_inf) )
		val_check_kind(s,k_stream_def);
	return alloc_int32(val_stream(s)->adler);
}


/**
	update_adler32 : adler:'int32 -> string -> pos:int -> len:int -> 'int32
	<doc>Update an adler32 value with a substring</doc>
**/
static value update_adler32( value adler, value s, value pos, value len ) {
	val_check(adler,int32);
	val_check(s,string);
	val_check(pos,int);
	val_check(len,int);
	if( val_int(pos) < 0 || val_int(len) < 0 || val_int(pos) + val_int(len) > val_strlen(s) )
		neko_error();
	return alloc_int32(adler32(val_int32(adler),(Bytef*)(val_string(s)+val_int(pos)),val_int(len)));
}

/**
	update_crc32 : crc:'int32 -> string -> pos:int -> len:int -> 'int32
	<doc>Update a CRC32 value with a substring</doc>
**/
static value update_crc32( value crc, value s, value pos, value len ) {
	val_check(crc,int32);
	val_check(s,string);
	val_check(pos,int);
	val_check(len,int);
	if( val_int(pos) < 0 || val_int(len) < 0 || val_int(pos) + val_int(len) > val_strlen(s) )
		neko_error();
	return alloc_int32(crc32(val_int32(crc),(Bytef*)(val_string(s)+val_int(pos)),val_int(len)));
}

/**
	deflate_bound : 'dstream -> n:int -> int
	<doc>Return the maximum buffer size needed to write [n] bytes</doc>
**/
static value deflate_bound( value s, value size ) {
	val_check_kind(s,k_stream_def);
	val_check(size,int);
	return alloc_int(deflateBound(val_stream(s),val_int(size)));
}

DEFINE_PRIM(deflate_init,1);
DEFINE_PRIM(deflate_buffer,5);
DEFINE_PRIM(deflate_end,1);
DEFINE_PRIM(inflate_init,1);
DEFINE_PRIM(inflate_buffer,5);
DEFINE_PRIM(inflate_end,1);
DEFINE_PRIM(set_flush_mode,2);
DEFINE_PRIM(deflate_bound,2);

DEFINE_PRIM(get_adler32,1);
DEFINE_PRIM(update_adler32,4);
DEFINE_PRIM(update_crc32,4);

/* ************************************************************************ */
