#include "img.h"
#include "img_conv.h"
#include <stdio.h>
#include <ctype.h>
#include <libheif/heif.h>
#include <libheif/heif_version.h>

#include "Icon.h"

#ifdef __cplusplus
extern "C" {
#endif

static char * ext[] = {
	"heic",
	"heif",
	NULL, /* <-- maybe avif here, see below */
	NULL
};
static int    bpp[] = { imbpp24, imByte, 0 };

static char * loadOutput[] = {
	"chroma_bits_per_pixel",
	"depth_images",
	"has_alpha",
	"is_primary",
	"ispe_height",
	"ispe_width",
	"luma_bits_per_pixel",
	"premultiplied_alpha",
	"thumbnails",
	"aux",
	"metadata",
	"thumbnail_of",
	NULL
};

#define MAX_FEATURES 32
static char * features[MAX_FEATURES+1];

static char * mime[] = {
	"image/heic",
	"image/heic-sequence",
	"image/heif",
	"image/heif-sequence",
	NULL
};

static ImgCodecInfo codec_info = {
	"libheif",
	"https://github.com/strukturag/libheif",
	LIBHEIF_NUMERIC_VERSION >> 24, (LIBHEIF_NUMERIC_VERSION >> 16) & 0xff,    /* version */
	ext,    /* extension */
	"High Efficiency Image File Format",     /* file type */
	"HEIF", /* short type */
	features,    /* features  */
	"Prima::Image::heif",     /* module */
	"Prima::Image::heif",     /* package */
	IMG_LOAD_FROM_FILE | IMG_LOAD_FROM_STREAM | IMG_LOAD_MULTIFRAME,
	bpp, /* save types */
	loadOutput,
	mime
};

static enum heif_compression_format default_compression = heif_compression_HEVC;

static HV *
load_defaults( PImgCodec c)
{
	HV * profile = newHV();
	pset_i( ignore_transformations, 0);
	pset_i( convert_hdr_to_8bit, 0);
	return profile;
}

static void *
init( PImgCodecInfo * info, void * param)
{
	*info = &codec_info;
	{
		struct heif_context*ctx = heif_context_alloc();
		struct heif_encoder_descriptor *enc[1024];
		int i, n, feat, got_hevc = 0;

		n = heif_context_get_encoder_descriptors(ctx, heif_compression_undefined, NULL,
			(const struct heif_encoder_descriptor**) enc, 1024);
		for ( i = feat = 0; i < n; i++) {
			char buf[2048], *compstr;
			const char *name, *shrt;
			enum heif_compression_format comp;
			int lossy, lossless;

			if ( feat >= MAX_FEATURES) {
				features[MAX_FEATURES] = NULL;
				break;
			}

			comp     = heif_encoder_descriptor_get_compression_format(enc[i]);
			switch ( comp ) {
			case heif_compression_HEVC:
				compstr = "HEVC";
				got_hevc = 1;
				break;
			case heif_compression_AVC:
				compstr = "AVC";
				ext[2] = "avif";
				break;
			case heif_compression_AV1:
				compstr = "AV1";
				ext[2] = "avif";
				break;
			default:
				continue;
			}
			default_compression = comp;

			name     = heif_encoder_descriptor_get_name(enc[i]);
			shrt     = heif_encoder_descriptor_get_id_name(enc[i]);
			lossy    = heif_encoder_descriptor_supports_lossy_compression(enc[i]);
			lossless = heif_encoder_descriptor_supports_lossless_compression(enc[i]);

			snprintf(buf, 2048, "encoder %s/%s%s%s (%s)",
				compstr,
				shrt,
				lossy    ? " lossy"    : "",
				lossless ? " lossless" : "",
				name
			);
			buf[2047] = 0;
			features[feat++] = duplicate_string(buf);

			if ( heif_have_decoder_for_format(comp)) {
				snprintf(buf, 2048, "decoder %s/%s", compstr, shrt);
				buf[2047] = 0;
				features[feat++] = duplicate_string(buf);
			}
		}
		if ( got_hevc ) default_compression = heif_compression_HEVC;
		heif_context_free(ctx);
	}
	if ( heif_have_encoder_for_format(default_compression))
		codec_info.IOFlags |= IMG_SAVE_TO_FILE | IMG_SAVE_TO_STREAM | IMG_SAVE_MULTIFRAME;
	return (void*)1;
}

static void
done( PImgCodec codec)
{
	int feat;
	for ( feat = 0; feat < MAX_FEATURES; feat++) {
		if ( features[feat] == NULL ) break;
		free(features[feat]);
	}
}

typedef struct {
	heif_item_id *items;
	int           count, size, curr;
	heif_item_id _buf[1];
} ItemList;

typedef struct _LoadRec {
	struct heif_context* ctx;
	struct heif_error    error;
	int                  primary_index;
	ItemList             *toplevel, *thumbnails;
	int                  *toplevel_index;
	struct heif_image_handle *curr_toplevel_handle;
} LoadRec;

static int64_t
heif_get_position(void* userdata)
{
	return req_tell(( PImgIORequest) (userdata));
}

static int
heif_read(void* data, size_t size, void* userdata)
{
	return req_read(( PImgIORequest) (userdata), size, data) < 0;
}

static int
heif_seek(int64_t position, void* userdata)
{
	return req_seek(( PImgIORequest) (userdata), (long) position, SEEK_SET) < 0;
}

static enum heif_reader_grow_status
heif_wait_for_file_size(int64_t target_size, void* userdata)
{
	PImgIORequest req = (PImgIORequest) userdata;
	long orig, curr;

	orig = req_tell(req);
	req_seek(req, 0, SEEK_END);
	curr = req_tell(req);
	req_seek(req, orig, SEEK_SET);
	return (target_size > curr) ?
		heif_reader_grow_status_size_beyond_eof :
		heif_reader_grow_status_size_reached;
}

struct heif_reader reader = {
	.reader_api_version = 1,
	.get_position       = heif_get_position,
	.read               = heif_read,
	.seek               = heif_seek,
	.wait_for_file_size = heif_wait_for_file_size
};

#define SET_ERROR(e) if (1) { strlcpy(fi->errbuf,e,256); goto FAIL; }
#define SET_HEIF_ERROR SET_ERROR(l->error.message)
#define CHECK_HEIF_ERROR if (l->error.code != heif_error_Ok) SET_HEIF_ERROR
#define CALL l->error=

static Bool
item_list_alloc(ItemList** list, int n)
{
	ItemList* p = *list;
	if ( p ) {
		if ( p-> size < n ) {
			int sz = p-> size;
			while (sz < n) sz *= 2;
			if ( !( p = realloc( p, (sz - 1) * sizeof(heif_item_id) + sizeof(ItemList))))
				return false;
			p-> size = sz;
		}
	} else {
		if ( !( p = malloc((n - 1) * sizeof(heif_item_id) + sizeof(ItemList))))
			return false;
		p-> size = n;
		p-> curr = 0;
	}
	p-> count = n;
	p-> items = p-> _buf;
	*list = p;
	return true;
}

/*
this codec throws all found images as frame numbers.
currently only thumbnails, but if i get my hands on heics with depth images and the like, I'll add these here as well.
if f ex we have a 2-frame heic where each frame has two thumbnails, Prima treats such file as this:

frame 0 -> toplevel #0
frame 1 -> toplevel #0's thumbnail #0
frame 2 -> toplevel #0's thumbnail #1
frame 3 -> toplevel #1
frame 4 -> toplevel #1's thumbnail #0
frame 5 -> toplevel #1's thumbnail #1

thumbnail extras have .thumbnail_of field set

*/
static void *
open_load( PImgCodec instance, PImgLoadFileInstance fi)
{
	LoadRec * l;
	int n, n_images;
#define PEEK_SIZE 32
	uint8_t data[PEEK_SIZE];
	enum heif_filetype_result ftype;
	heif_item_id primary_id;

	l = malloc( sizeof( LoadRec));
	if ( !l) return NULL;
	memset( l, 0, sizeof( LoadRec));

	if ( req_seek( fi-> req, 0, SEEK_SET) < 0) return NULL;
	if ( req_read( fi-> req, PEEK_SIZE, data) < PEEK_SIZE) return NULL;
	if ( req_seek( fi-> req, 0, SEEK_SET) < 0) return NULL;
	ftype = heif_check_filetype( data, PEEK_SIZE );
	switch ( ftype ) {
	case heif_filetype_yes_supported:
	case heif_filetype_maybe:
		fi-> stop = true;
		break;
	case heif_filetype_yes_unsupported:
		fi-> stop = true;
		SET_ERROR("unsupported HEIF/AVIC");
	default:
		return NULL;
	}
#undef PEEK_SIZE

	if ( !( l-> ctx = heif_context_alloc()))
		SET_ERROR("cannot create context");

#if LIBHEIF_NUMERIC_VERSION > 0x10e0100
	/* https://github.com/strukturag/libheif/pull/737 - multi-threading crashes sometimes */
	heif_context_set_max_decoding_threads(l->ctx, 0);
#endif

	CALL heif_context_read_from_reader(l->ctx, &reader, fi->req, NULL);
	CHECK_HEIF_ERROR;

	n_images = heif_context_get_number_of_top_level_images(l->ctx);
	if ( n_images < 1 ) SET_ERROR("cannot get top level images");

	CALL heif_context_get_primary_image_ID(l->ctx, &primary_id);
	CHECK_HEIF_ERROR;

	if ( !item_list_alloc(&l->toplevel, n_images))
		SET_ERROR("not enough memory");
	l->toplevel->curr = -1;

	if ( !( l->toplevel_index = malloc((n_images + 1) * sizeof(int))))
		SET_ERROR("not enough memory");
	l->toplevel_index[0] = 0;
	for ( n = 1; n <= n_images; n++) l->toplevel_index[n] = -1;

	if ( !item_list_alloc(&l->thumbnails, 16))
		SET_ERROR("not enough memory");
	l->thumbnails->count = 0;

	n = heif_context_get_list_of_top_level_image_IDs(l->ctx, l->toplevel->items, n_images);
	if ( n < n_images ) {
		n_images = n;
		if ( n < 0 ) SET_ERROR("cannot get list of top level images");
	}
	l-> primary_index = -1;
	for ( n = 0; n < n_images; n++)
		if ( primary_id == l->toplevel->items[n] ) {
			l-> primary_index = n;
			break;
		}

	if ( fi-> wantFrames ) {
		struct heif_image_handle *h;
		int index = 0;
		fi-> frameCount = n_images;
		for ( n = 0; n < n_images; n++) {
			int nn;
			CALL heif_context_get_image_handle(l-> ctx, l->toplevel->items[n], &h);
			CHECK_HEIF_ERROR;
			nn = heif_image_handle_get_number_of_thumbnails(h);
			l->toplevel_index[n] = index;
			index += 1 + nn;
			fi-> frameCount += nn;
			heif_image_handle_release(h);
		}
		l->toplevel_index[n_images] = index;
	}

	return l;

FAIL:
	if ( l-> toplevel_index) free(l-> toplevel_index);
	if ( l-> toplevel   ) free(l-> toplevel);
	if ( l-> thumbnails ) free(l-> thumbnails);
	if ( l->ctx) heif_context_free(l-> ctx);
	free(l);
	return NULL;
}

static void
read_metadata(const struct heif_image_handle* h, HV * profile)
{
	int i, n;
	heif_item_id* ids;
	AV * av;

	n = heif_image_handle_get_number_of_metadata_blocks(h, NULL);
	if ( n < 1 ) return;

	if ( !( ids = malloc(n * sizeof(heif_item_id))))
		return;

	i = heif_image_handle_get_list_of_metadata_block_IDs(h, NULL, ids, n);
	if ( i > n ) n = i;
	if ( n < 1 ) goto BAILOUT;

	av = newAV();
	pset_sv_noinc( metadata, newRV_noinc((SV*) av));
	for ( i = 0; i < n; i++) {
		char * c;
		size_t sz;
		SV * sv;
		struct heif_error error;
		HV * profile;

		profile = newHV();
		av_push( av, newRV_noinc((SV*)profile));

		c = (char*) heif_image_handle_get_metadata_type(h, ids[i]);
		pset_c(type, c);
		c = (char*) heif_image_handle_get_metadata_content_type(h, ids[i]);
		pset_c(content_type, c);

		sz = heif_image_handle_get_metadata_size(h, ids[i]);
		if ( sz < 0 ) continue;
		sv = prima_array_new(sz);
		error = heif_image_handle_get_metadata(h, ids[i], prima_array_get_storage(sv));
		if (error.code != heif_error_Ok) {
			sv_free(sv);
			continue;
		}
		pset_sv_noinc(content, sv);
	}

BAILOUT:
	free(ids);
}

static Bool
set_toplevel_handle( PImgLoadFileInstance fi, int toplevel)
{
	int n;
	struct heif_image_handle* h = NULL;
	LoadRec * l  = ( LoadRec *) fi-> instance;

	if ( toplevel == l->toplevel->curr && l->curr_toplevel_handle)
		return true;

	if ( l->curr_toplevel_handle ) {
		heif_image_handle_release(l->curr_toplevel_handle);
		l->curr_toplevel_handle = NULL;
	}
	CALL heif_context_get_image_handle(l-> ctx, l->toplevel->items[toplevel], &h);
	CHECK_HEIF_ERROR;
	l->toplevel->curr = toplevel;
	l->curr_toplevel_handle = h;

	n = heif_image_handle_get_number_of_thumbnails(h);
	if ( !item_list_alloc(&l->thumbnails, n))
		SET_ERROR("not enough memory");
	bzero(l->thumbnails->items, sizeof(heif_item_id) * n);
	heif_image_handle_get_list_of_thumbnail_IDs(h, l->thumbnails->items, l->thumbnails->count);

	return true;

FAIL:
	return false;
}

static struct heif_image_handle*
load_thumbnail( PImgLoadFileInstance fi, int toplevel)
{
	LoadRec * l  = ( LoadRec *) fi-> instance;
	struct heif_image_handle* h = NULL;

	if ( !set_toplevel_handle(fi, toplevel)) return false;

	CALL heif_image_handle_get_thumbnail(l->curr_toplevel_handle,
		l-> thumbnails->items[fi->frame - l->toplevel_index[toplevel] - 1],
		&h);
	CHECK_HEIF_ERROR;
FAIL:
	return h;
}

static struct heif_image_handle*
seek_image_handle( PImgLoadFileInstance fi)
{
	int i;
	struct heif_image_handle* h = NULL;

	if ( fi->frame < 0 ) return NULL;
	LoadRec * l  = ( LoadRec *) fi-> instance;

	for ( i = 0; i < l-> toplevel-> count; i++) {
		if ( l->toplevel_index[i+1] < 0 ) {
			if ( !set_toplevel_handle(fi, i)) return false;
			l->toplevel_index[i+1] = l->toplevel_index[i] + 1 + l->thumbnails->count;
		}

		if ( fi->frame == l->toplevel_index[i]) {
			if ( !set_toplevel_handle(fi, i)) return false;
			return l->curr_toplevel_handle;
		} else if ( fi->frame < l->toplevel_index[i] ) {
			if ( !( h = load_thumbnail(fi, i - 1))) return false;
			break;
		} else if ( i + 1 == l-> toplevel->count && fi->frame > l->toplevel_index[i] ) {
			if ( !( h = load_thumbnail(fi, i))) return false;
			break;
		}
	}

	if ( fi->frameCount < 0 && l->toplevel_index[l->toplevel->count] >= 0 ) 
		fi->frameCount = l->toplevel_index[l->toplevel->count];

	return h;
}

Bool
load( PImgCodec instance, PImgLoadFileInstance fi)
{
	HV * profile = fi-> frameProperties;
	PImage i     = ( PImage) fi-> object;
	LoadRec * l  = ( LoadRec *) fi-> instance;

	struct heif_image_handle* h = NULL;
	struct heif_decoding_options* hdo = NULL;
	struct heif_image* himg = NULL;

	Bool ret = false, icon;
	int y, bit_depth, width, height, src_stride, alpha_stride = 0;
	Byte *src, *dst, *alpha = NULL;

	if ( !( h = seek_image_handle(fi)))
		goto FAIL;

	/* read basics */
	bit_depth = heif_image_handle_get_luma_bits_per_pixel(h);
	if ( bit_depth < 0 ) SET_ERROR("undefined bit depth");
	width  = heif_image_handle_get_width(h);
	height = heif_image_handle_get_height(h);
	if ( fi-> loadExtras) {
		int n;
		if ( heif_image_handle_is_premultiplied_alpha(h))
			pset_i( premultiplied_alpha, 1);
		pset_i( chroma_bits_per_pixel,  heif_image_handle_get_chroma_bits_per_pixel(h));
		pset_i( luma_bits_per_pixel,    bit_depth);
		pset_i( ispe_width,             heif_image_handle_get_ispe_width(h));
		pset_i( ispe_height,            heif_image_handle_get_ispe_height(h));
		if (heif_image_handle_has_alpha_channel(h))
			pset_i( has_alpha,      1);
		if ( h == l->curr_toplevel_handle ) {
			n = heif_image_handle_get_number_of_depth_images(h);
			if (n) pset_i( depth_images, n);
			n = heif_image_handle_get_number_of_thumbnails(h);
			if (n) pset_i( thumbnails, n);
			n = heif_image_handle_get_number_of_auxiliary_images(h,
				LIBHEIF_AUX_IMAGE_FILTER_OMIT_ALPHA|
				LIBHEIF_AUX_IMAGE_FILTER_OMIT_DEPTH
			);
			if (n) pset_i( aux, n);
			if ( l->primary_index == l->toplevel->curr)
				pset_i(is_primary, 1);
		} else {
			pset_i(thumbnail_of, l->toplevel_index[l->toplevel->curr]);
		}

	}
	icon = kind_of( fi-> object, CIcon);
	if ( icon && !heif_image_handle_has_alpha_channel(h))
		icon = false;

	/* check load options */
	if ( !( hdo = heif_decoding_options_alloc()))
		SET_ERROR("not enough memory");
	{
		dPROFILE;
		HV * profile = fi->profile;
		if ( pexist(ignore_transformations))
			hdo->ignore_transformations = pget_i(ignore_transformations);
		if ( pexist(convert_hdr_to_8bit))
			hdo->convert_hdr_to_8bit = pget_i(convert_hdr_to_8bit);
	}
	if ( fi-> loadExtras)
		read_metadata(h, profile);

	if ( fi-> noImageData) {
		pset_i( width,  width);
		pset_i( height, height);
		goto SUCCESS;
	}
	i-> self-> create_empty(( Handle) i, width, height, imbpp24);
	if ( icon ) {
		PIcon(i)->self-> set_autoMasking((Handle) i, false);
		PIcon(i)->self-> set_maskType((Handle) i, imbpp8);
	}

	/* load */
	CALL heif_decode_image(h, &himg,
		heif_colorspace_RGB,
		icon ? heif_chroma_interleaved_RGBA : heif_chroma_interleaved_RGB,
		hdo);
	CHECK_HEIF_ERROR;

	src = (Byte*) heif_image_get_plane_readonly(himg, heif_channel_interleaved, &src_stride);
	if ( icon ) {
		alpha        = PIcon(i)-> mask;
		alpha_stride = PIcon(i)-> maskLine;
	}
	for (
		y = 0, src += (height - 1) * src_stride, dst = i->data;
		y < height;
		y++, src -= src_stride, dst += i->lineSize
	) {
		if ( icon ) {
			bc_rgba_bgr_a( src, dst, alpha, width);
			alpha += alpha_stride;
		} else
			cm_reverse_palette(( PRGBColor) src, (PRGBColor) dst, width);
	}

SUCCESS:
	ret = true;
FAIL:
	if (himg) heif_image_release(himg);
	if (hdo) heif_decoding_options_free(hdo);
	if ( h && h != l->curr_toplevel_handle )
		heif_image_handle_release(h);
	return ret;
}

static void
close_load( PImgCodec instance, PImgLoadFileInstance fi)
{
	LoadRec * l = ( LoadRec *) fi-> instance;
	if ( l-> curr_toplevel_handle) heif_image_handle_release(l-> curr_toplevel_handle);
	if ( l-> ctx)        heif_context_free( l-> ctx);
	if ( l-> toplevel_index) free(l-> toplevel_index);
	if ( l-> toplevel)   free( l-> toplevel );
	if ( l-> thumbnails) free(l-> thumbnails);
	free( l);
}

typedef struct _SaveRec {
	struct heif_context* ctx;
	struct heif_error    error;
	struct heif_image_handle** handles;
	struct heif_image_handle*  handlebuf[1];
} SaveRec;

static HV *
save_defaults( PImgCodec c)
{
	HV * profile = newHV();
	struct heif_encoder* encoder = NULL;
	struct heif_context* ctx;

	switch (default_compression) {
	case heif_compression_HEVC: pset_c(encoder, "HEVC"); break;
	case heif_compression_AVC:  pset_c(encoder, "AVC"); break;
	case heif_compression_AV1:  pset_c(encoder, "AV1"); break;
	default: break;
	}

	pset_i(is_primary, 0);
	pset_c(quality, "50"); /* x265.quality and aom.quality default values are 50 */
	pset_i(premultiplied_alpha, 0);
	pset_i(thumbnail_of,    -1);
	pset_sv(metadata,  newRV_noinc((SV*) newHV()));

	if (( ctx = heif_context_alloc()) != NULL) {
		struct heif_encoder_descriptor *enc[1024];
		int i, n;
		n = heif_context_get_encoder_descriptors(ctx, heif_compression_undefined, NULL,
			(const struct heif_encoder_descriptor**) enc, 1024);
		for ( i = 0; i < n; i++) {
			const char * shrt = heif_encoder_descriptor_get_id_name(enc[i]);
			if (heif_context_get_encoder(ctx, enc[i], &encoder).code == heif_error_Ok) {
				char buf[2048];
				const struct heif_encoder_parameter*const* list = heif_encoder_list_parameters(encoder);
				while ( list && *list) {
					HV *hv = newHV();
					const char* name = heif_encoder_parameter_get_name(*list);
					const char* svtype = "";
					enum heif_encoder_parameter_type type = heif_encoder_parameter_get_type(*list);
					switch (type) {
					case heif_encoder_parameter_type_integer: {
						HV *profile = hv;
						int v, have_min = 0, min = 0, max = 0;
						int have_max = 0, n = 0, *ints = NULL;
						svtype = "int";
						if ( heif_encoder_get_parameter_integer(encoder, name, &v).code == heif_error_Ok)
							pset_i(default, v);
						if ( heif_encoder_parameter_get_valid_integer_values(*list,
							&have_min, &have_max, &min, &max, &n, (const int**) &ints
							).code == heif_error_Ok) {
							if ( have_min ) pset_i(min, min);
							if ( have_max ) pset_i(max, max);
							if ( ints && n > 0 ) {
								AV * av = newAV();
								for (v = 0; v < n; v++)
									av_push(av, newSViv(ints[v]));
								pset_sv(values, newRV_noinc((SV*)av));
							}
						}
						break;
					}
					case heif_encoder_parameter_type_boolean: {
						int v;
						HV *profile = hv;
						svtype = "bool";
						if ( heif_encoder_get_parameter_boolean(encoder, name, &v).code == heif_error_Ok)
							pset_i(default, v);
						break;
					}
					case heif_encoder_parameter_type_string: {
						char v[2048], **strs;
						HV *profile = hv;
						svtype = "str";
						if ( heif_encoder_get_parameter_string(encoder, name, v, sizeof(v)).code == heif_error_Ok) 
							pset_c(default, v);
						if (
							heif_encoder_parameter_get_valid_string_values(
								*list,
								(const char* const**) &strs
							).code == heif_error_Ok
						) {
							AV * av = newAV();
							while (strs && *strs) {
								av_push(av, newSVpv(*strs, 0));
								strs++;
							}
							pset_sv(values, newRV_noinc((SV*)av));
						}
						break;
					}
					default:
						sv_free((SV*) hv);
						list++;
						continue;
					}
					snprintf(buf, 2048, "%s.%s", shrt, name);
					(void)hv_store( hv, "type", 4, newSVpv(svtype, 0), 0);
					(void)hv_store( profile, buf, (I32) strlen(buf), newRV_noinc((SV*)hv), 0);
					list++;
				}
				heif_encoder_release(encoder);
			}
		}
		heif_context_free(ctx);
	}

	return profile;
}

static void *
open_save( PImgCodec instance, PImgSaveFileInstance fi)
{
	SaveRec *l;
	int sz;

	sz = sizeof(SaveRec) + (fi->n_frames * sizeof(struct heif_image_handle*));
	if (!(l = malloc(sz))) return NULL;
	memset( l, 0, sz);
	l-> handles = l->handlebuf;

	if ( !( l-> ctx = heif_context_alloc()))
		SET_ERROR("cannot create context");

	return (void*)l;

FAIL:
	if ( l->ctx) heif_context_free(l-> ctx);
	free(l);
	return NULL;
}

static struct heif_error
heif_write(struct heif_context* ctx, const void* data, size_t size, void* userdata)
{
	struct heif_error err;
	if ( req_write(( PImgIORequest) (userdata), size, (void*)data) >= 0) {
		err.code    = heif_error_Ok;
		err.subcode = heif_suberror_Unspecified;
		err.message = "Ok";
	} else {
		err.code    = heif_error_Encoding_error;
		err.subcode = heif_suberror_Cannot_write_output_data;
		err.message = "write error";
	}
	return err;
}

struct heif_writer writer = {
	.writer_api_version = 1,
	.write              = heif_write
};

static Bool
encode_thumbnail( PImgSaveFileInstance fi, HV * profile)
{
	dPROFILE;
	SaveRec * l  = ( SaveRec *) fi-> instance;
	int thumbnail_of = -1;

	if ( !pexist(thumbnail_of))
		return true;

	thumbnail_of = pget_i(thumbnail_of);
	if ( thumbnail_of < 0 || thumbnail_of >= fi->n_frames )
		SET_ERROR("thumbnail_of must be an integer from 0 to the last frame");
	if ( thumbnail_of == fi->frame)
		SET_ERROR("thumbnail_of cannot refer to itself");
	if ( !l->handles[thumbnail_of])
		SET_ERROR("master image is not encoded yet");
	CALL heif_context_assign_thumbnail(l->ctx, l->handles[thumbnail_of], l->handles[fi->frame]);
	CHECK_HEIF_ERROR;
	return true;

FAIL:
	return false;
}

static Bool
encode_metadata( PImgSaveFileInstance fi, HV * profile)
{
	dPROFILE;
	SaveRec * l  = ( SaveRec *) fi-> instance;
	SV *sv, *content;
	AV *av;
	void * data;
	STRLEN size;
	char * item_type, * content_type;
	int i, len;

	if ( !pexist(metadata))
		return true;
	sv = pget_sv(metadata);

	if ( !SvOK(sv) || !SvROK(sv) || SvTYPE(SvRV(sv)) != SVt_PVAV)
		SET_ERROR("'metadata' is not an array");
	av = (AV*) SvRV(sv);
	len = av_len(av) + 1;
	for ( i = 0; i < len; i++) {
		SV **ssv;
		ssv = av_fetch( av, i, 0 );
		if ( !ssv || !(sv = *ssv) || !SvOK(sv) || !SvROK(sv) || SvTYPE(SvRV(sv)) != SVt_PVHV)
			SET_ERROR("metadata[index] is not a hash");

		profile = ( HV*) SvRV( sv);

		/*
		XMP:  mime=application/rdf+xml
		EXIF: (uint32_t)offset + data, Exif=
		*/

		if ( pexist(type))
			item_type = pget_c(type);
		else
			SET_ERROR("metadata.type is missing");

		if ( pexist(content_type))
			content_type = pget_c(content_type);
		else
			content_type = NULL;

		if ( pexist(content))
			content = pget_sv(content);
		else
			SET_ERROR("metadata.content is missing");
		data = SvPV(content, size);

		CALL heif_context_add_generic_metadata(
			l->ctx, l->handles[fi->frame],
			data, (int)size,
			(const char*)item_type, (const char*)content_type
		);
		CHECK_HEIF_ERROR;
	}

	return true;

FAIL:
	return false;
}

static Bool
apply_encoder_options( PImgSaveFileInstance fi, enum heif_compression_format compression, struct heif_encoder* encoder)
{
	SV **tmp;
	HV * profile = fi-> extras;
	SaveRec * l = ( SaveRec *) fi-> instance;
	const struct heif_encoder_parameter*const* list;
	const char * shrt = NULL, * enc_name;
	int i, n_descriptors;
	struct heif_encoder_descriptor* out_encoders[16];

	n_descriptors = heif_context_get_encoder_descriptors(
		l->ctx, compression, NULL,
		(const struct heif_encoder_descriptor**) out_encoders, 16
	);
	enc_name = heif_encoder_get_name(encoder);
	for ( i = 0; i < n_descriptors; i++) {
		const char *descr_name = heif_encoder_descriptor_get_name(out_encoders[i]);
		if ( descr_name && strcmp(descr_name, enc_name) == 0) {
			shrt = heif_encoder_descriptor_get_id_name(out_encoders[i]);
			break;
		}
	}
	if ( !shrt )
		SET_ERROR("cannot find encoder descriptor");

	list = heif_encoder_list_parameters(encoder);
	while ( list && *list) {
		char buf[128];
		const char* name = heif_encoder_parameter_get_name(*list);
		enum heif_encoder_parameter_type type = heif_encoder_parameter_get_type(*list);
		SV* sv;

		snprintf(buf, 128, "%s.%s", shrt, name);
		if ( !hv_exists( profile, buf, (I32) strlen( buf)))
			goto NEXT;
		if (( tmp = hv_fetch( profile, buf, (I32) strlen( buf), 0)) == NULL)
			goto NEXT;
		sv = *tmp;

		switch (type) {
		case heif_encoder_parameter_type_integer:
			CALL heif_encoder_set_parameter_integer(encoder, name, SvIV(sv));
			break;
		case heif_encoder_parameter_type_boolean:
			CALL heif_encoder_set_parameter_boolean(encoder, name, SvIV(sv));
			break;
		case heif_encoder_parameter_type_string:
			CALL heif_encoder_set_parameter_string(encoder, name, SvPV_nolen(sv));
			break;
		}
		if (l->error.code != heif_error_Ok) {
			snprintf(fi->errbuf, 256, "%s (%s)", l->error.message, buf);
			goto FAIL;
		}
	NEXT:
		list++;
	}

	return true;

FAIL:
	return false;
}

static Bool
save( PImgCodec instance, PImgSaveFileInstance fi)
{
	dPROFILE;
	SaveRec * l = ( SaveRec *) fi-> instance;
	struct heif_encoder* encoder = NULL;
	HV * profile = fi-> extras;
	enum heif_compression_format compression;
	struct heif_image* himg = NULL;
	PIcon i = ( PIcon) fi-> object;
	Bool icon;
	int y, dst_stride, alpha_stride = 0;
	Byte *src, *dst, *alpha = NULL, *mask8 = NULL;
	struct heif_encoding_options* options = NULL;
	enum heif_colorspace colorspace;
	enum heif_chroma chroma;

	compression = default_compression;
	if ( pexist(encoder)) {
		char * c = pget_c(encoder);
		if ( strcmp(c, "HEVC") == 0)
			compression = heif_compression_HEVC;
		else if ( strcmp(c, "AVC") == 0)
			compression = heif_compression_AVC;
		else if ( strcmp(c, "AV1") == 0)
			compression = heif_compression_AV1;
		else
			SET_ERROR("bad encoder, must be one of: HEVC, AVC, AV1");
	}
	if ( !heif_have_encoder_for_format(compression))
		SET_ERROR("encoder is not available");
	CALL heif_context_get_encoder_for_format(l->ctx, compression, &encoder);
	CHECK_HEIF_ERROR;

	if ( pexist(quality)) {
		char * c = pget_c(quality);
		if ( strcmp(c, "lossless") == 0) {
			CALL heif_encoder_set_lossless(encoder, 1);
		} else {
			char * err = NULL;
			int quality = strtol(c, &err, 10);
			if ( *err || quality < 0 || quality > 100 )
				SET_ERROR("quality must be set either to an integer between 0 and 100 or to 'lossless'");
			CALL heif_encoder_set_lossy_quality(encoder, quality);
		}
	}
	CHECK_HEIF_ERROR;

	if ( !apply_encoder_options(fi, compression, encoder))
		goto FAIL;

	options = heif_encoding_options_alloc();

	icon = kind_of( fi-> object, CIcon);
	if ( icon ) {
		if ( i-> maskType != 8 ) {
			if ( !( mask8 = i-> self-> convert_mask((Handle) i, 8)))
				SET_ERROR("not enough memory");
		}
		if ( i->type == imByte ) {
			colorspace = heif_colorspace_monochrome;
			chroma     = heif_chroma_monochrome;
		} else {
			colorspace = heif_colorspace_RGB;
			chroma     = heif_chroma_interleaved_RGBA;
		}
	} else {
		if ( i->type == imByte ) {
			colorspace = heif_colorspace_monochrome;
			chroma     = heif_chroma_monochrome;
		} else {
			colorspace = heif_colorspace_RGB;
			chroma     = heif_chroma_interleaved_RGB;
		}
	}
	CALL heif_image_create(i-> w, i-> h, colorspace, chroma, &himg);
	CHECK_HEIF_ERROR;

	if ( i->type == imByte ) {
		CALL heif_image_add_plane(himg, heif_channel_Y, i->w, i->h, 8);
		CHECK_HEIF_ERROR;
		dst = (Byte*) heif_image_get_plane(himg, heif_channel_Y, &dst_stride);
	} else {
		CALL heif_image_add_plane(himg, heif_channel_interleaved, i->w, i->h, 8);
		CHECK_HEIF_ERROR;
		dst = (Byte*) heif_image_get_plane(himg, heif_channel_interleaved, &dst_stride);
	}

	if ( pexist(premultiplied_alpha) && pget_i(premultiplied_alpha))
		heif_image_set_premultiplied_alpha(himg, 1);

	if ( icon ) {
		alpha        = mask8 ? mask8 : i-> mask;
		alpha_stride = mask8 ? LINE_SIZE(i->w, 8) : i-> maskLine;
	}
	for (
		y = 0, dst += (i->h - 1) * dst_stride, src = i->data;
		y < i->h;
		y++, src += i->lineSize, dst -= dst_stride
	) {
		if ( icon ) {
			if ( i->type == imByte ) {
				memcpy( dst, src, i-> w);
			} else {
				bc_bgr_a_rgba( src, alpha, dst, i->w);
				alpha += alpha_stride;
			}
		} else {
			if ( i->type == imByte ) {
				memcpy( dst, src, i-> w);
			} else {
				cm_reverse_palette(( PRGBColor) src, (PRGBColor) dst, i->w);
			}
		}
	}
	if ( icon && i->type == imByte ) {
		CALL heif_image_add_plane(himg, heif_channel_Alpha, i->w, i->h, 8);
		CHECK_HEIF_ERROR;
		dst = (Byte*) heif_image_get_plane(himg, heif_channel_Y, &dst_stride);
		for (
			y = 0, dst += (i->h - 1) * dst_stride;
			y < i->h;
			y++, alpha += alpha_stride, dst -= dst_stride
		) {
			memcpy( dst, src, i-> w);
		}
	}

	CALL heif_context_encode_image(l->ctx, himg, encoder, options, &l->handles[fi->frame]);
	CHECK_HEIF_ERROR;

	heif_encoding_options_free(options);
	heif_encoder_release(encoder);
	if ( mask8) free(mask8);
	encoder = NULL;
	options = NULL;
	mask8   = NULL;

	if ( !encode_thumbnail(fi, profile))
		goto FAIL;
	if ( !encode_metadata(fi, profile))
		goto FAIL;
	if ( pexist(is_primary) && pget_B(is_primary)) {
		CALL heif_context_set_primary_image(l->ctx, l->handles[fi->frame]);
		CHECK_HEIF_ERROR;
	}

	if ( fi-> frame == fi-> n_frames - 1 ) {
		CALL heif_context_write(l->ctx, &writer, fi->req);
		CHECK_HEIF_ERROR;
	}
	return true;

FAIL:
	if ( mask8)    free(mask8);
	if ( options ) heif_encoding_options_free(options);
	if ( encoder ) heif_encoder_release(encoder);
	return false;

}

static void
close_save( PImgCodec instance, PImgSaveFileInstance fi)
{
	SaveRec * l = ( SaveRec *) fi-> instance;
	int i;

	for ( i = 0; i < fi-> n_frames; i++) {
		if ( l->handles[i] )
			heif_image_handle_release(l->handles[i]);
	}
	if ( l-> ctx) heif_context_free( l-> ctx);
	free(l);
}

void
apc_img_codec_heif( void )
{
	struct ImgCodecVMT vmt;
	memcpy( &vmt, &CNullImgCodecVMT, sizeof( CNullImgCodecVMT));
	vmt. load_defaults = load_defaults;
	vmt. init          = init;
	vmt. open_load     = open_load;
	vmt. load          = load;
	vmt. close_load    = close_load;
	vmt. save_defaults = save_defaults;
	vmt. open_save     = open_save;
	vmt. save          = save;
	vmt. close_save    = close_save;
	vmt. done          = done;
	apc_img_register( &vmt, NULL);
}

#ifdef __cplusplus
}
#endif
