/*
** MUPage_Images.c
**
** Auxiliary functions for MUPage.c
**
** Oct2020 - ABU fecit
*/

/* See https://en.cppreference.com/w/c/experimental/dynamic/strdup */
#define __STDC_WANT_LIB_EXT2__ 1

#include "asprintf.h"   // WARNING: must be declared BEFORE <stdio.h>
#include <string.h> 
#include "xpdf_extension.h"
#include "MUPage_Images.h"

#if !defined(min)
	#define min(a,b) ((a<b)?a:b)
	#define max(a,b) ((a>b)?a:b)
#endif

/*
** ============================================================================
** ImagePattern and ImagePositionalPattern
** ============================================================================
*/
    // .. some auxiliary functions ...
enum { 
	SYM_P_NUMOFPAGES = 0, 
	SYM_p_CURRPAGE, 
	SYM_I_NUMOFIMAGES, // n. of images in the current page
	SYM_i_CURRIMAGE    // image idx in the current page
};

	/*
	**  SymPosition is a small CONSTANT array that will be initialized
	**  at runtime by Alloc_SymPosition()
	*/
static int* SymPosition = NULL;

	// side effect: SymPosition array allocated and initialized
static
TclResCode Alloc_ImagePatternSymPosition (Tcl_Interp* interp) {
	
	const char* symbols = Tcl_GetVar(interp, "mupdf::_IMG_PATTERN_SYMBOLS", TCL_GLOBAL_ONLY);
	if (!symbols) {
		Tcl_SetObjResult(interp, Tcl_NewStringObj("internal error: _IMG_PATTERN_SYMBOLS undefined",-1));
		return TCL_ERROR;
	}
	
	int N = (int)strlen(symbols);
	SymPosition = (int*)ckalloc(N*sizeof(int));
	int i;
	for (i=0 ; i < N; ++i) {
	switch (symbols[i]) {
		case 'P': SymPosition[SYM_P_NUMOFPAGES] = i ; break;
		case 'p': SymPosition[SYM_p_CURRPAGE] = i ; break;
		case 'I': SymPosition[SYM_I_NUMOFIMAGES] = i ; break;
		case 'i': SymPosition[SYM_i_CURRIMAGE] = i ; break;
		default: {
			Tcl_SetObjResult(interp, Tcl_ObjPrintf(
				"internal error: _IMG_PATTERN_SYMBOLS has an unmanaged symbol '%c'", symbols[i]));
			return TCL_ERROR;
		}
	} 
	}
	return TCL_OK;
}

static
int HasPatternThisSymbol( Tcl_Interp* interp, const char* pattern, int symbol ) {
	Tcl_Obj* usedSymbolsObj = NULL;
	if (pattern && pattern[0] != '\0') {
		// compute the positional_pattern
		Tcl_Obj* command = Tcl_ObjPrintf("mupdf::_used_symbols %s", pattern);
		Tcl_IncrRefCount(command);
		TclResCode resCode = Tcl_EvalObjEx(interp, command, TCL_EVAL_DIRECT|TCL_GLOBAL_ONLY);
		Tcl_DecrRefCount(command);
		if ( resCode == TCL_ERROR ) {
			// this is very BAD...
			// lets' say the answer is 'false'
			return 0;
		}
		usedSymbolsObj = Tcl_GetObjResult(interp);
	} else {
		// use precomputed _IMG_USED_SYMBOLS
		usedSymbolsObj = Tcl_GetVar2Ex(interp, "mupdf::_IMG_USED_SYMBOLS", NULL, TCL_GLOBAL_ONLY);
	}
	if( !usedSymbolsObj ) {
		// this is very BAD...
		// lets' say the answer is 'false'
		return 0;
	}
	Tcl_IncrRefCount(usedSymbolsObj); // retain the object !!
	// check if resObj (as a string) contains the symbol
	int hasSymbol = (strchr( Tcl_GetString(usedSymbolsObj), symbol ) == NULL ? 0 : 1);
	Tcl_DecrRefCount(usedSymbolsObj);
	
	return hasSymbol;
}

static
int nOfDigits(int n) {
	int ndigits = 1;
	if( n>=10 ) { ndigits = 2; }
	if( n>=100 ) { ndigits = 3; }
	if( n>=1000 ) { ndigits = 4; }   // WOW 
	return ndigits;
}

	/*
	** Compute or retrieve the default positional_pattern.
	**
	** Return a *new* string
	** On error return NULL.
	** NOTE: the returned string (if not NULL) should be released with free()
	*/
static
char* NewImagePositionalPattern(Tcl_Interp* interp, const char* pattern) {
	Tcl_Obj* posPatternObj = NULL;
	if (pattern && pattern[0] != '\0') {
		// compute the positional_pattern
		Tcl_Obj* command = Tcl_ObjPrintf("mupdf::_positional_pattern %s", pattern);
		Tcl_IncrRefCount(command);
		TclResCode resCode = Tcl_EvalObjEx(interp, command, TCL_EVAL_DIRECT|TCL_GLOBAL_ONLY);
		Tcl_DecrRefCount(command);
		if ( resCode == TCL_ERROR ) {
			return NULL;
		}
		posPatternObj = Tcl_GetObjResult(interp);
	} else {
		// use precomputed _IMG_POSITIONAL_PATTERN
		posPatternObj = Tcl_GetVar2Ex(interp, "mupdf::_IMG_POSITIONAL_PATTERN", NULL, TCL_GLOBAL_ONLY);
	}
	if (!posPatternObj) {
		// this is a fatal error... you should check it 
		//  .. maybe at startup ...
		Tcl_SetObjResult(interp, Tcl_NewStringObj("internal error: IMG_POSITIONAL_PATTERN undefined",-1));
		return NULL;
	}
	char *posPatternStr = strdup(Tcl_GetString(posPatternObj));
	
	return posPatternStr;
}

 // return a new string.
 // this string should be released with free()
static
char* new_fullnameWithoutExt(const char* outputDir, const char* basename) {
	char* fullnameWithoutExt = NULL;
	char *addedFileSep = "";
	
	if (outputDir == NULL) {
		outputDir = "";
		addedFileSep = "";
	} else {
	size_t dirLen = strlen(outputDir);
		if (dirLen > 0 && outputDir[dirLen-1] != '/' ) {
			addedFileSep = "/";
		} else {
			addedFileSep = "";
		}
	}
	asprintf(&fullnameWithoutExt, "%s%s%s", outputDir, addedFileSep, basename);
	return fullnameWithoutExt;
}

 // On error return NULL ( error message saved in interp's result)
 // this is safe from fz-exceptions 
static
Tcl_Obj* mu_image_extract( Tcl_Interp* interp, fz_context* ctx, pdf_document* doc, pdf_obj* imDict, int useAlpha, const char*imID, const char* imName, const char* filenameWithoutExt) {
	char* filename = NULL;
	fz_image* image = NULL;
	
	fz_var(filename);
	fz_var(image);
	fz_try(ctx) {
		image = pdf_load_image(ctx,doc,imDict);
		
		int imType = xfz_image_type(ctx,image);
		char* ext;
		if (imType == FZ_IMAGE_JPEG) {
			ext = "jpg";
		} else {
			ext = "png";
		}
		asprintf(&filename,"%s.%s", filenameWithoutExt, ext);
		
		switch (imType) {
		  case FZ_IMAGE_JPEG:
			xfz_image_savejpeg(ctx, image, filename);
			break;
		  case FZ_IMAGE_PNG:
			xfz_image_savepng(ctx,image,useAlpha,filename);
			break;
		  default: {
			fz_throw(ctx, FZ_ERROR_GENERIC, "unknown image type");
			}
		}
	} fz_always(ctx) {
		if (image)  fz_drop_image(ctx,image);
	} fz_catch(ctx) {
		free(filename);
		Tcl_SetObjResult(interp, Tcl_NewStringObj(fz_caught_message(ctx),-1));
		return NULL;
	}
	
	Tcl_Obj * listObj = Tcl_NewListObj(0,NULL);
	Tcl_ListObjAppendElement(interp, listObj, Tcl_NewStringObj(imID,-1));
	Tcl_ListObjAppendElement(interp, listObj, Tcl_NewStringObj(imName,-1));
	Tcl_ListObjAppendElement(interp, listObj, Tcl_NewStringObj(filename,-1));
	
	free(filename);
	return listObj;
}




 // return a list ...
 // load some imageattributes (without loading the image)
 
  // On error return NULL ( error message saved in interp's result)
  // this is safe from fz-exceptions
static
Tcl_Obj* mu_image_info( Tcl_Interp* interp, fz_context* ctx, pdf_obj* imDict, const char*imID) {
	int im_bpc = pdf_to_int(ctx, pdf_dict_geta(ctx, imDict, PDF_NAME(BitsPerComponent), PDF_NAME(BPC)));
	if (im_bpc == 0)  im_bpc = 8;
	
	 // code grabbed from tools/pdfinfo.c
	pdf_obj *altcolorSpaceObj = NULL;
	pdf_obj *colorSpaceObj = pdf_dict_geta(ctx, imDict, PDF_NAME(ColorSpace), PDF_NAME(CS));
	if (pdf_is_array(ctx, colorSpaceObj)) {
		pdf_obj *origCsObj = colorSpaceObj;
		colorSpaceObj = pdf_array_get(ctx, origCsObj, 0);
		if (pdf_name_eq(ctx, colorSpaceObj, PDF_NAME(DeviceN)) || pdf_name_eq(ctx, colorSpaceObj, PDF_NAME(Separation))) {
			altcolorSpaceObj = pdf_array_get(ctx, origCsObj, 2);
			if (pdf_is_array(ctx, altcolorSpaceObj))
				altcolorSpaceObj = pdf_array_get(ctx, altcolorSpaceObj, 0);
			}
	}
	
	int imWidth = pdf_to_int(ctx, pdf_dict_get(ctx, imDict, PDF_NAME(Width)));
	int imHeight= pdf_to_int(ctx, pdf_dict_get(ctx, imDict, PDF_NAME(Height)));
	int imHasMask = (pdf_dict_geta(ctx, imDict, PDF_NAME(SMask), PDF_NAME(Mask)) ? 1 : 0);
	
	Tcl_Obj *listObj = Tcl_NewListObj(0,NULL);						
	
	Tcl_ListObjAppendElement(interp, listObj, Tcl_NewStringObj(imID,-1));
	Tcl_ListObjAppendElement(interp, listObj, Tcl_NewIntObj(imWidth));
	Tcl_ListObjAppendElement(interp, listObj, Tcl_NewIntObj(imHeight));
	 // print colorspace (and altcolorspace if not NULL)
	if ( altcolorSpaceObj == NULL ) {
		Tcl_ListObjAppendElement(interp, listObj, Tcl_NewStringObj(pdf_to_name(ctx, colorSpaceObj),-1));
	} else {
		Tcl_Obj *csListObj = Tcl_NewListObj(0,NULL);
		Tcl_ListObjAppendElement(interp, csListObj, Tcl_NewStringObj(pdf_to_name(ctx, colorSpaceObj),-1));
		Tcl_ListObjAppendElement(interp, csListObj, Tcl_NewStringObj(pdf_to_name(ctx, altcolorSpaceObj),-1));
		Tcl_ListObjAppendElement(interp, listObj, csListObj);
	}
	 // number of color components can be inferred from colorspace
	Tcl_ListObjAppendElement(interp, listObj, Tcl_NewIntObj(im_bpc));
	Tcl_ListObjAppendElement(interp, listObj, Tcl_NewBooleanObj(imHasMask));
	return listObj;
}

  
// TODO - split this proc ...

Tcl_Obj* mu_images(Tcl_Interp* interp, MUPage *self, char *op, char *imageID, char *outputDir, const char* pattern, int useAlpha) {
	// to do ... define an  enum  MUPAGE_OP_EXTRACT_IMAGES , MUPAGE_OP_LISTIMAGES
	
	fz_context* const ctx = MUPage_fz_context(self);
	pdf_page*   pdfPage = MUPage_pdf_page(self);
	
	// NOTE:
	// the following parameters are meaningful only for op "extract"
	//      outputDir, pattern, useAlpha
	// the following local variables are meaningful only for op "extract"
	//  mustAddImageNumber, positionalPattern, posPatternObj, zobjv 
	// troppi !! fai struct extract_data
	
	char* posPatternStr = NULL; 
	Tcl_Obj* zobjv[4]; 
	
	// preparing some useful data for the "extract" op.
	if ( strcmp(op,"extract") == 0 ) {
		
		int nOfImages;
		fz_try(ctx) {
			nOfImages = xpdf_number_of_images(ctx, pdfPage);
		} fz_catch(ctx) {
			nOfImages = -1;
		}
		// if pattern is NULL or "" , the default pattern is used.
		//   retrieve or compute the positional-pattern string
		posPatternStr = NewImagePositionalPattern(interp, pattern);
		if (!posPatternStr) {
			// error message already in interp's result
			return NULL;
		}
		// remember to release posPatternStr and zobjv elements !!!
		
		if ( imageID == NULL  ||  imageID[0] == '\0' ) {
			// we should process all the images in this page,
			// then check if we must add the image number to the pattern.
			if ( nOfImages > 1 &&  ! HasPatternThisSymbol( interp, pattern, 'i' ) ) {
				// extend the posPatternStr for including the 'i' symbol
				char *extend=NULL;
				asprintf(&extend, "%s-%%%d$0%dd", posPatternStr, SYM_i_CURRIMAGE, nOfDigits(nOfImages));
				free(posPatternStr);
				posPatternStr=extend;
			}
		}
		zobjv[SymPosition[SYM_P_NUMOFPAGES]] = Tcl_NewIntObj(pdf_count_pages(ctx,pdfPage->doc));
		// 0th page should be "1"   ! 
		zobjv[SymPosition[SYM_p_CURRPAGE]] = Tcl_NewIntObj(1+MUPage_pageNumber(self));				
		zobjv[SymPosition[SYM_I_NUMOFIMAGES]] = Tcl_NewIntObj(nOfImages);
		zobjv[SymPosition[SYM_i_CURRIMAGE]] = Tcl_NewIntObj(0); // later ..
		
		// safety check.
		// before entering the extract loop, check if posPatternStr is valid !!
		Tcl_Obj* dummyObj = Tcl_Format(interp, posPatternStr, sizeof(zobjv)/sizeof(zobjv[0]), zobjv );
		if (dummyObj == NULL ) {
			// This is BAD !   posPattern is not valid !            
			Tcl_SetObjResult(interp,Tcl_ObjPrintf("internal error: posPatternStr \"%s\" is not valid.",posPatternStr)); 
			
			Tcl_DecrRefCount(dummyObj);
			for ( int i = 0; i < sizeof(zobjv)/sizeof(zobjv[0]); ++i ) {
				if( zobjv[i] ) Tcl_DecrRefCount(zobjv[i]);
			}
			
			return NULL;
		}
	}
	
	 ////// === scan the page ....
	Tcl_Obj * biglistObj = Tcl_NewListObj(0,NULL);
	
	pdf_obj * resourceObj = NULL;
	
	fz_try(ctx) {
		resourceObj = pdf_page_resources(ctx,pdfPage); 
		pdf_mark_obj(ctx, resourceObj); 
		pdf_obj * xObj = pdf_dict_get(ctx, resourceObj, PDF_NAME(XObject) ); 
 		if( xObj ) {
			int n = pdf_dict_len(ctx,xObj);  // number of elements (just some of these are images)
			int currImageIdx = 0;
			for( int i=0 ; i<n ; ++i ) { 
				const char *imName = pdf_to_name(ctx,pdf_dict_get_key(ctx,xObj,i));
				pdf_obj * imDict = pdf_dict_get_val(ctx,xObj,i); 
				
				if ( ! pdf_is_dict(ctx, imDict) ) 
					continue;  // !! WTF !!
				
				 // skip if imDict is not an Image
				if ( ! pdf_name_eq(ctx, pdf_dict_get(ctx, imDict, PDF_NAME(Subtype)), PDF_NAME(Image)) )
					continue;
				
				++currImageIdx;
				 // we assume there're no more than 99 images in a page.
				 // For safety, allocate a buffer imID of 11 chars (enough to hold the max interger)
				char imID[1+10+1];  // "i" + 2 digits + \0
				snprintf(imID,sizeof(imID),"i%02d",currImageIdx);
				
				 // skip this image if imageID != imID
				 // NOTE: if ImageID is NULL, then process all images
				if ( imageID && strcmp(imageID,imID)!= 0 ) continue;
				
				Tcl_Obj* listObj;
				if ( strcmp(op,"list") == 0 ) {
					// ///////////////////////////////////////////////////
					listObj = mu_image_info(interp, ctx, imDict, imID);
					// ///////////////////////////////////////////////////
					
					//  could it be NULL ??
				} else {  // op is "extract"
					
					Tcl_SetIntObj(zobjv[SymPosition[SYM_i_CURRIMAGE]],currImageIdx);
					Tcl_Obj* fnameObj = Tcl_Format(interp, posPatternStr, sizeof(zobjv)/sizeof(zobjv[0]), zobjv );
					if (fnameObj == NULL ) {
						// this was previously checked.
						// This is VERY BAD !
						// what can I do ??
						break;
					}
					char* filenameWithoutExt = new_fullnameWithoutExt(outputDir, Tcl_GetString(fnameObj));
					Tcl_DecrRefCount(fnameObj); // free fnameObj
					
					// ///////////////////////////////////////////////////
					listObj = mu_image_extract( interp, ctx, pdfPage->doc, imDict, useAlpha, imID, imName, filenameWithoutExt);
					// ///////////////////////////////////////////////////
					free(filenameWithoutExt);
				}
				// safet check,but is NULL its's really a bad story !
				if (listObj)
					Tcl_ListObjAppendElement(interp, biglistObj, listObj);
			} // end for loop
		}
	} fz_always (ctx) {
		pdf_unmark_obj(ctx, resourceObj);  // ? should I free the resources ?
		// release all the resources
		if ( strcmp(op,"extract") == 0 ) {
			if (posPatternStr) free(posPatternStr);
			for ( int i = 0; i < sizeof(zobjv)/sizeof(zobjv[0]); ++i ) {
				if( zobjv[i] ) Tcl_DecrRefCount(zobjv[i]);
			}
		}
	} fz_catch(ctx) {
		//  free biglistObj !!
		Tcl_DecrRefCount(biglistObj);
		Tcl_SetObjResult(interp, Tcl_NewStringObj(fz_caught_message(ctx),-1));
		biglistObj = NULL;
	}
	
	return biglistObj;
}


//-----------------------------------------------------------------------------
// compute image insertion matrix
//-----------------------------------------------------------------------------
static
fz_matrix calc_image_matrix(int width, int height, fz_rect targetRect, const char* anchor, int rotate, int resize) {
    float targetW = targetRect.x1 - targetRect.x0;
    float targetH = targetRect.y1 - targetRect.y0;
	float f;
    if (resize==1) {
		f = min(targetW/width,targetH/height);
	} else {
		f= 1.0;
	}

	fz_point tr;
	if ( strchr( anchor, 's' ) ) {
		tr.y = targetRect.y0;
	} else if ( strchr( anchor, 'n') ) {
		tr.y = targetRect.y0+targetH-f*height;
	} else {
		tr.y = targetRect.y0+(targetH-f*height)/2;
	}
	if ( strchr( anchor, 'w' ) ) {
		tr.x = targetRect.x0;
	} else if ( strchr( anchor, 'e') ) {
		tr.x = targetRect.x0+targetW-f*width;
	} else {
		tr.x = targetRect.x0+(targetW-f*width)/2;
	}
	
    fz_matrix mat = fz_rotate((float) rotate);
    mat = fz_concat(mat, fz_scale(f*width,f*height));
    mat = fz_concat(mat, fz_translate(tr.x,tr.y));
	
    return mat;
}


TclResCode mu_addimage(Tcl_Interp* interp, MUPage* muPage,
	const char* filename,
	fz_rect targetRect,
	const char* anchor,
	int rotate,
	int resize,
	int over,
	int* xref  //return value
) {
	fz_context* ctx     = MUPage_fz_context(muPage);
	pdf_page*   pdfPage = MUPage_pdf_page(muPage);	

	char imgname[20];
	 // generate a unique tagname (unique within the same page)
	{
	//** this is rather a dumb way ... we should be sure the new tagname is unique among all the page's Resources...
	 // use time with microsec precision 
	 Tcl_Time time;
	 Tcl_GetTime(&time);
	   // "I" + sec 10 char + "." + usec 6 char = 18 char ... 20 is enough 
	  sprintf(imgname,"I%ld.%ld", (long int)time.sec, (long int)time.usec);
	}

	pdf_obj* newref = NULL;
	fz_try(ctx) {
		float w,h;
		
		if (*xref > 0) {
			newref = pdf_new_indirect(ctx, pdfPage->doc, *xref, 0);
			if ( 
				! pdf_is_dict(ctx, newref)
				||
				! pdf_name_eq(ctx, pdf_dict_get(ctx, newref, PDF_NAME(Type)), PDF_NAME(XObject)) 			
				||
				! pdf_name_eq(ctx, pdf_dict_get(ctx, newref, PDF_NAME(Subtype)), PDF_NAME(Image)) 
			) {			
				fz_throw(ctx, FZ_ERROR_GENERIC, "invalid xref; it's not an image");
			}
			w = pdf_to_int(ctx, pdf_dict_geta(ctx, newref, PDF_NAME(Width), PDF_NAME(W)));
			h = pdf_to_int(ctx, pdf_dict_geta(ctx, newref, PDF_NAME(Height), PDF_NAME(H)));
		} else {
			fz_image* image = NULL;
			
			fz_var(image); fz_var(w) ; fz_var(h);
			fz_try(ctx) {
				image = fz_new_image_from_file(ctx,filename);		
				newref = pdf_add_image(ctx,pdfPage->doc,image);
				w = image->w;
	        	h = image->h;
	        } fz_always(ctx) {
				if (image) fz_drop_image(ctx,image);		
			} fz_catch(ctx) {
				fz_rethrow(ctx);
			}
		}

		{
		  // write the new resource name (imgname) in /Resources/XObject
         pdf_obj* resources = pdf_dict_get_inheritable(ctx, pdfPage->obj, PDF_NAME(Resources));
         if (!resources) {
            resources = pdf_dict_put_dict(ctx, pdfPage->obj, PDF_NAME(Resources), 2);
         }
         pdf_obj* xobject = pdf_dict_get(ctx, resources, PDF_NAME(XObject));
         if (!xobject) {
            xobject = pdf_dict_put_dict(ctx, resources, PDF_NAME(XObject), 2);
         }
         pdf_dict_puts_drop(ctx, xobject, imgname, newref);
		}
		{
		 // write in /Contents the instructions for showing the new imgname
         fz_buffer* newResourceBuff = fz_new_buffer(ctx, 50);
         const char *template = "\nq\n%g %g %g %g %g %g cm\n/%s Do\nQ\n";
		 fz_matrix mat = calc_image_matrix(w, h, targetRect, anchor, rotate, resize);
         fz_append_printf(ctx, newResourceBuff, template,
                                 mat.a, mat.b, mat.c, mat.d, mat.e, mat.f, imgname);
		 // update page contents ...
		 JM_insert_contents(ctx, pdfPage->doc, pdfPage->obj, newResourceBuff, over);
		 fz_drop_buffer(ctx,newResourceBuff);		
		}
	} fz_catch(ctx) {
		Tcl_SetObjResult(interp, Tcl_NewStringObj(fz_caught_message(ctx),-1));
		return TCL_ERROR;
	}

	*xref = pdf_to_num(ctx,newref);
	return TCL_OK;
}


TclResCode MUPage_Images_Setup(Tcl_Interp *interp) {
	TclResCode res;
		
	res =Alloc_ImagePatternSymPosition(interp);
	return res;
}
